<?php

declare(strict_types=1);

require_once __DIR__ . '/backup_ftp.php';

const BACKUP_FILES_STATE_KEY = 'backup_files_ftp_state';
const BACKUP_FILES_FTP_INTERVAL_SEC = 86400;
const BACKUP_FILES_FTP_FAIL_COOLDOWN_SEC = 3600;

/**
 * @return array{last_success_ts:int,last_attempt_ts:int,last_ok:bool}
 */
function files_backup_state_load(PDO $pdo): array
{
    $defaults = ['last_success_ts' => 0, 'last_attempt_ts' => 0, 'last_ok' => true];
    $stmt = $pdo->prepare('SELECT setting_value FROM app_settings WHERE setting_key = ? LIMIT 1');
    $stmt->execute([BACKUP_FILES_STATE_KEY]);
    $raw = $stmt->fetchColumn();
    if (!is_string($raw) || $raw === '') {
        return $defaults;
    }
    $decoded = json_decode($raw, true);
    if (!is_array($decoded)) {
        return $defaults;
    }
    $out = $defaults;
    if (isset($decoded['last_success_ts'])) {
        $out['last_success_ts'] = max(0, (int) $decoded['last_success_ts']);
    }
    if (isset($decoded['last_attempt_ts'])) {
        $out['last_attempt_ts'] = max(0, (int) $decoded['last_attempt_ts']);
    }
    if (array_key_exists('last_ok', $decoded)) {
        $out['last_ok'] = filter_var($decoded['last_ok'], FILTER_VALIDATE_BOOLEAN);
    }
    return $out;
}

/**
 * @param array{last_success_ts:int,last_attempt_ts:int,last_ok:bool} $state
 */
function files_backup_state_save(PDO $pdo, array $state): void
{
    $payload = [
        'last_success_ts' => max(0, (int) ($state['last_success_ts'] ?? 0)),
        'last_attempt_ts' => max(0, (int) ($state['last_attempt_ts'] ?? 0)),
        'last_ok' => !empty($state['last_ok']),
    ];
    $json = json_encode($payload, JSON_UNESCAPED_UNICODE);
    if ($json === false) {
        return;
    }
    $stmt = $pdo->prepare('INSERT INTO app_settings (setting_key, setting_value, description) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)');
    $stmt->execute([BACKUP_FILES_STATE_KEY, $json, 'Files zip FTP schedule state (JSON)']);
}

/**
 * Relative paths under $root (forward slashes). Only whitelisted top-level dirs + root *.php.
 *
 * @return list<string>
 */
function files_backup_collect_relative_paths(string $root): array
{
    $root = rtrim(str_replace('\\', '/', $root), '/');
    $rels = [];
    $bases = ['admin', 'api', 'config', 'cron', 'includes', 'database'];
    foreach ($bases as $base) {
        $full = $root . '/' . $base;
        if (!is_dir($full)) {
            continue;
        }
        try {
            $iterator = new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator($full, FilesystemIterator::SKIP_DOTS),
                RecursiveIteratorIterator::LEAVES_ONLY
            );
        } catch (Throwable $e) {
            continue;
        }
        foreach ($iterator as $info) {
            if (!$info instanceof SplFileInfo || !$info->isFile()) {
                continue;
            }
            $abs = $info->getRealPath();
            if ($abs === false) {
                $abs = $info->getPathname();
            }
            $abs = str_replace('\\', '/', $abs);
            if (strpos($abs, $root . '/') !== 0) {
                continue;
            }
            $rel = substr($abs, strlen($root) + 1);
            if ($rel === false || $rel === '') {
                continue;
            }
            $low = strtolower($rel);
            if (strpos($low, '/.git/') !== false
                || strpos($low, '/node_modules/') !== false
                || strpos($low, '/.idea/') !== false
                || strpos($low, 'backups/') === 0
                || strpos($low, 'app/') === 0) {
                continue;
            }
            $rels[] = $rel;
        }
    }
    foreach (glob($root . '/*.php', GLOB_NOSORT) ?: [] as $abs) {
        $abs = str_replace('\\', '/', $abs);
        if (strpos($abs, $root . '/') !== 0) {
            continue;
        }
        $rels[] = substr($abs, strlen($root) + 1);
    }
    $rels = array_values(array_unique(array_filter($rels, static fn ($r) => is_string($r) && $r !== '')));
    sort($rels);
    return $rels;
}

/**
 * @return array{ok:bool,error?:string}
 */
function files_backup_create_zip(string $projectRoot, string $zipPath): array
{
    if (!class_exists(ZipArchive::class)) {
        return ['ok' => false, 'error' => 'افزونهٔ zip در PHP فعال نیست.'];
    }
    $root = realpath($projectRoot);
    if ($root === false) {
        return ['ok' => false, 'error' => 'مسیر ریشهٔ پروژه معتبر نیست.'];
    }
    $root = str_replace('\\', '/', $root);
    $rels = files_backup_collect_relative_paths($root);
    if ($rels === []) {
        return ['ok' => false, 'error' => 'هیچ فایلی برای فشرده‌سازی پیدا نشد.'];
    }
    $part = $zipPath . '.part';
    @unlink($part);
    $zip = new ZipArchive();
    if ($zip->open($part, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
        return ['ok' => false, 'error' => 'باز کردن فایل zip برای نوشتن ممکن نشد.'];
    }
    foreach ($rels as $rel) {
        $full = $root . '/' . $rel;
        if (!is_file($full) || !is_readable($full)) {
            continue;
        }
        if (!$zip->addFile($full, $rel)) {
            $zip->close();
            @unlink($part);
            return ['ok' => false, 'error' => 'افزودن فایل به zip ناموفق بود: ' . $rel];
        }
    }
    $zip->close();
    if (is_file($zipPath)) {
        @unlink($zipPath);
    }
    if (!@rename($part, $zipPath)) {
        if (@copy($part, $zipPath)) {
            @unlink($part);
        } else {
            @unlink($part);
            return ['ok' => false, 'error' => 'تکمیل فایل zip (rename) ناموفق بود.'];
        }
    }
    return ['ok' => true];
}

function files_backup_rotate_zips(string $backupDir, int $keep = 5): void
{
    $keep = max(0, $keep);
    if ($keep === 0 || !is_dir($backupDir)) {
        return;
    }
    $dir = rtrim($backupDir, '/\\');
    $pattern = $dir . DIRECTORY_SEPARATOR . 'files_backup_*.zip';
    $files = glob($pattern);
    if (!is_array($files) || $files === []) {
        return;
    }
    usort($files, static function ($a, $b) {
        $ma = @filemtime($a) ?: 0;
        $mb = @filemtime($b) ?: 0;
        return $mb <=> $ma;
    });
    foreach (array_slice($files, $keep) as $path) {
        if (is_file($path)) {
            @unlink($path);
        }
    }
}

/**
 * Build zip of site PHP trees, optionally upload to FTP (same credentials as DB backup).
 *
 * @return array{
 *   ok:bool,
 *   skipped:bool,
 *   skipped_reason:?string,
 *   message:string,
 *   zip_path:?string,
 *   ftp_ok:?bool,
 *   ftp_message:?string
 * }
 */
function scheduled_files_backup_run(PDO $pdo, bool $forceManual): array
{
    $cfg = backup_ftp_load_config($pdo);
    if (empty($cfg['enabled'])) {
        return [
            'ok' => $forceManual ? false : true,
            'skipped' => !$forceManual,
            'skipped_reason' => $forceManual ? null : 'ftp_disabled',
            'message' => $forceManual ? 'ابتدا آپلود FTP را در همین صفحه فعال و ذخیره کنید.' : 'FTP غیرفعال است؛ پشتیبان فایل‌ها ارسال نشد.',
            'zip_path' => null,
            'ftp_ok' => null,
            'ftp_message' => null,
        ];
    }
    if (!$forceManual && empty($cfg['files_auto_ftp'])) {
        return [
            'ok' => true,
            'skipped' => true,
            'skipped_reason' => 'files_auto_off',
            'message' => 'پشتیبان خودکار فایل‌ها (۲۴ ساعت) غیرفعال است.',
            'zip_path' => null,
            'ftp_ok' => null,
            'ftp_message' => null,
        ];
    }

    $state = files_backup_state_load($pdo);
    $now = time();
    if (!$forceManual) {
        if (!empty($state['last_success_ts']) && ($now - (int) $state['last_success_ts']) < BACKUP_FILES_FTP_INTERVAL_SEC) {
            return [
                'ok' => true,
                'skipped' => true,
                'skipped_reason' => 'interval',
                'message' => 'از آخرین پشتیبان موفق فایل‌ها روی FTP کمتر از ۲۴ ساعت گذشته است.',
                'zip_path' => null,
                'ftp_ok' => null,
                'ftp_message' => null,
            ];
        }
        if (empty($state['last_ok'])
            && !empty($state['last_attempt_ts'])
            && ($now - (int) $state['last_attempt_ts']) < BACKUP_FILES_FTP_FAIL_COOLDOWN_SEC) {
            return [
                'ok' => true,
                'skipped' => true,
                'skipped_reason' => 'cooldown',
                'message' => 'پس از خطای قبلی FTP فایل‌ها، تا یک ساعت تلاش خودکار متوقف است.',
                'zip_path' => null,
                'ftp_ok' => null,
                'ftp_message' => null,
            ];
        }
    }

    $state['last_attempt_ts'] = $now;
    $state['last_ok'] = false;
    files_backup_state_save($pdo, $state);

    $root = realpath(__DIR__ . '/..');
    if ($root === false) {
        $root = dirname(__DIR__);
    }
    $backupDir = $root . DIRECTORY_SEPARATOR . 'backups' . DIRECTORY_SEPARATOR;
    if (!is_dir($backupDir) && !@mkdir($backupDir, 0755, true)) {
        return [
            'ok' => false,
            'skipped' => false,
            'skipped_reason' => null,
            'message' => 'پوشه backups قابل ایجاد نیست.',
            'zip_path' => null,
            'ftp_ok' => null,
            'ftp_message' => null,
        ];
    }

    files_backup_rotate_zips($backupDir, 5);
    $zipName = 'files_backup_' . date('Y-m-d_H-i-s') . '_' . bin2hex(random_bytes(3)) . '.zip';
    $zipPath = $backupDir . $zipName;

    $built = files_backup_create_zip($root, $zipPath);
    if (empty($built['ok'])) {
        $msg = $built['error'] ?? 'نامشخص';
        files_backup_state_save($pdo, $state);
        return [
            'ok' => false,
            'skipped' => false,
            'skipped_reason' => null,
            'message' => 'ساخت zip فایل‌ها ناموفق: ' . $msg,
            'zip_path' => null,
            'ftp_ok' => null,
            'ftp_message' => null,
        ];
    }

    clearstatcache(true, $zipPath);
    $up = backup_ftp_upload_file($zipPath, backup_ftp_config_for_files_upload($cfg));
    if (!empty($up['ok'])) {
        @unlink($zipPath);
        $state['last_success_ts'] = time();
        $state['last_ok'] = true;
        files_backup_state_save($pdo, $state);
        return [
            'ok' => true,
            'skipped' => false,
            'skipped_reason' => null,
            'message' => 'پشتیبان فایل‌ها روی FTP بارگذاری شد (' . $zipName . ').',
            'zip_path' => null,
            'ftp_ok' => true,
            'ftp_message' => $up['message'] ?? '',
        ];
    }

    files_backup_rotate_zips($backupDir, 5);
    files_backup_state_save($pdo, $state);
    return [
        'ok' => false,
        'skipped' => false,
        'skipped_reason' => null,
        'message' => 'zip ساخته شد اما FTP ناموفق بود؛ فایل محلی نگه داشته شد: ' . $zipName,
        'zip_path' => $zipPath,
        'ftp_ok' => false,
        'ftp_message' => $up['message'] ?? '',
    ];
}
