<?php

declare(strict_types=1);

const BACKUP_FTP_SETTING_KEY = 'backup_ftp_json';

/**
 * @return array{
 *   enabled:bool,
 *   host:string,
 *   port:int,
 *   username:string,
 *   password:string,
 *   remote_path:string,
 *   passive:bool,
 *   use_ssl:bool,
 *   files_auto_ftp:bool,
 *   files_remote_path:string
 * }
 */
function backup_ftp_default_config(): array
{
    return [
        'enabled' => false,
        'host' => '',
        'port' => 21,
        'username' => '',
        'password' => '',
        'remote_path' => 'backups',
        'passive' => true,
        'use_ssl' => false,
        'files_auto_ftp' => false,
        'files_remote_path' => '',
    ];
}

/**
 * @return array{
 *   enabled:bool,
 *   host:string,
 *   port:int,
 *   username:string,
 *   password:string,
 *   remote_path:string,
 *   passive:bool,
 *   use_ssl:bool,
 *   files_auto_ftp:bool,
 *   files_remote_path:string
 * }
 */
function backup_ftp_load_config(PDO $pdo): array
{
    $base = backup_ftp_default_config();
    $stmt = $pdo->prepare('SELECT setting_value FROM app_settings WHERE setting_key = ? LIMIT 1');
    $stmt->execute([BACKUP_FTP_SETTING_KEY]);
    $raw = $stmt->fetchColumn();
    if (!is_string($raw) || $raw === '') {
        return $base;
    }
    $decoded = json_decode($raw, true);
    if (!is_array($decoded)) {
        return $base;
    }
    return backup_ftp_merge_config($base, $decoded);
}

/**
 * @param array<string,mixed> $patch
 * @return array{
 *   enabled:bool,
 *   host:string,
 *   port:int,
 *   username:string,
 *   password:string,
 *   remote_path:string,
 *   passive:bool,
 *   use_ssl:bool,
 *   files_auto_ftp:bool,
 *   files_remote_path:string
 * }
 */
function backup_ftp_merge_config(array $base, array $patch): array
{
    $out = $base;
    if (array_key_exists('enabled', $patch)) {
        $out['enabled'] = filter_var($patch['enabled'], FILTER_VALIDATE_BOOLEAN);
    }
    if (isset($patch['host'])) {
        $out['host'] = trim((string) $patch['host']);
    }
    if (isset($patch['port'])) {
        $out['port'] = max(1, min(65535, (int) $patch['port']));
    }
    if (isset($patch['username'])) {
        $out['username'] = trim((string) $patch['username']);
    }
    if (array_key_exists('password', $patch) && $patch['password'] !== null && $patch['password'] !== '') {
        $out['password'] = (string) $patch['password'];
    }
    if (isset($patch['remote_path'])) {
        $out['remote_path'] = trim(str_replace('\\', '/', (string) $patch['remote_path']), '/');
    }
    if (array_key_exists('passive', $patch)) {
        $out['passive'] = filter_var($patch['passive'], FILTER_VALIDATE_BOOLEAN);
    }
    if (array_key_exists('use_ssl', $patch)) {
        $out['use_ssl'] = filter_var($patch['use_ssl'], FILTER_VALIDATE_BOOLEAN);
    }
    if (array_key_exists('files_auto_ftp', $patch)) {
        $out['files_auto_ftp'] = filter_var($patch['files_auto_ftp'], FILTER_VALIDATE_BOOLEAN);
    }
    if (array_key_exists('files_remote_path', $patch)) {
        $out['files_remote_path'] = trim(str_replace('\\', '/', (string) $patch['files_remote_path']), '/');
    }
    return $out;
}

/**
 * @param array<string,mixed> $fromPost
 */
function backup_ftp_save_config(PDO $pdo, array $fromPost): void
{
    $current = backup_ftp_load_config($pdo);
    $newPassword = trim((string) ($fromPost['password'] ?? ''));
    $merged = backup_ftp_merge_config($current, [
        'enabled' => !empty($fromPost['enabled']),
        'host' => $fromPost['host'] ?? '',
        'port' => $fromPost['port'] ?? 21,
        'username' => $fromPost['username'] ?? '',
        'remote_path' => $fromPost['remote_path'] ?? 'backups',
        'passive' => !empty($fromPost['passive']),
        'use_ssl' => !empty($fromPost['use_ssl']),
        'files_auto_ftp' => !empty($fromPost['ftp_files_auto']),
        'files_remote_path' => (string) ($fromPost['ftp_files_remote_path'] ?? ''),
    ]);
    if ($newPassword !== '') {
        $merged['password'] = $newPassword;
    }
    $json = json_encode($merged, 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_FTP_SETTING_KEY, $json, 'FTP backup target (JSON)']);
}

/**
 * FTP config with remote_path pointing at the files-ZIP folder (or main remote_path if files_remote_path is empty).
 *
 * @param array $cfg from backup_ftp_load_config
 * @return array $cfg (shallow copy; do not rely on reference equality)
 */
function backup_ftp_config_for_files_upload(array $cfg): array
{
    $out = $cfg;
    $p = trim(str_replace('\\', '/', (string) ($cfg['files_remote_path'] ?? '')), '/');
    if ($p !== '') {
        $out['remote_path'] = $p;
    }
    return $out;
}

/**
 * @param array{
 *   host:string,
 *   port:int,
 *   username:string,
 *   password:string,
 *   passive:bool,
 *   use_ssl:bool
 * } $cfg
 * @return array{ok:bool,message:string}
 */
function backup_ftp_test_connection(array $cfg): array
{
    if (!function_exists('ftp_connect')) {
        return ['ok' => false, 'message' => 'ماژول FTP در PHP نصب/فعال نیست (افزونه ftp).'];
    }
    $host = trim($cfg['host']);
    if ($host === '') {
        return ['ok' => false, 'message' => 'نام میزبان (Host) را وارد کنید.'];
    }
    $port = (int) ($cfg['port'] ?? 21);
    $user = $cfg['username'] ?? '';
    $pass = $cfg['password'] ?? '';
    $timeout = 20;
    $useSsl = !empty($cfg['use_ssl']) && function_exists('ftp_ssl_connect');
    $conn = $useSsl
        ? @ftp_ssl_connect($host, $port, $timeout)
        : @ftp_connect($host, $port, $timeout);
    if ($conn === false) {
        return ['ok' => false, 'message' => 'برقراری اتصال TCP به سرور FTP ممکن نشد.'];
    }
    if (!@ftp_login($conn, $user, $pass)) {
        @ftp_close($conn);
        return ['ok' => false, 'message' => 'ورود نامعتبر است (نام کاربری یا رمز).'];
    }
    @ftp_pasv($conn, !empty($cfg['passive']));
    // LIST uses the data channel; login alone succeeding does not prove uploads will work.
    $listProbe = @ftp_nlist($conn, '.');
    if ($listProbe === false) {
        @ftp_close($conn);
        return [
            'ok' => false,
            'message' => 'ورود موفق بود اما کانال داده FTP (مثلاً Passive یا فایروال) برای لیست پوشه کار نکرد؛ آپلود هم معمولاً همین‌جا خطا می‌خورد. Passive را برعکس کنید، پورت‌های داده را نزد هاست باز کنید، یا با FileZilla همان حساب را تست کنید.',
        ];
    }
    @ftp_close($conn);
    return ['ok' => true, 'message' => 'اتصال، ورود و کانال داده (لیست پوشه) با موفقیت انجام شد.'];
}

/**
 * Create nested directory from FTP cwd. $path is relative, no leading slash.
 */
function backup_ftp_ensure_remote_dir($conn, string $path): bool
{
    $path = trim(str_replace('\\', '/', $path), '/');
    if ($path === '') {
        return true;
    }
    foreach (explode('/', $path) as $segment) {
        if ($segment === '') {
            continue;
        }
        if (@ftp_chdir($conn, $segment)) {
            continue;
        }
        @ftp_mkdir($conn, $segment);
        if (!@ftp_chdir($conn, $segment)) {
            return false;
        }
    }
    return true;
}

/**
 * @param resource $conn
 */
function backup_ftp_put_binary($conn, string $remoteName, string $localPath): bool
{
    clearstatcache(true, $localPath);
    $size = (int) (@filesize($localPath) ?: 0);
    if ($size > 0) {
        @ftp_alloc($conn, $size);
    }
    return (bool) @ftp_put($conn, $remoteName, $localPath, FTP_BINARY);
}

/**
 * Compare local file size to SIZE on server (if supported).
 *
 * @param resource $conn
 */
function backup_ftp_upload_size_matches($conn, string $remoteName, string $localPath): bool
{
    clearstatcache(true, $localPath);
    $local = (int) (@filesize($localPath) ?: 0);
    $remote = @ftp_size($conn, $remoteName);
    if ($remote === -1) {
        return true;
    }
    return $remote === $local;
}

/**
 * Upload one file to FTP. Never deletes remote files.
 *
 * @param array $cfg same shape as backup_ftp_load_config
 * @return array{ok:bool,message:string}
 */
function backup_ftp_upload_file(string $localPath, array $cfg): array
{
    if (empty($cfg['enabled'])) {
        return ['ok' => true, 'message' => 'آپلود FTP غیرفعال است.'];
    }
    if (!function_exists('ftp_connect')) {
        return ['ok' => false, 'message' => 'ماژول FTP در PHP فعال نیست.'];
    }
    if (!is_file($localPath) || !is_readable($localPath)) {
        return ['ok' => false, 'message' => 'فایل پشتیبان محلی برای ارسال یافت نشد.'];
    }
    $localBytes = (int) (@filesize($localPath) ?: 0);
    if ($localBytes < 1) {
        return ['ok' => false, 'message' => 'فایل پشتیبان محلی خالی است (۰ بایت)؛ قبل از FTP ارسال نشد.'];
    }
    $host = trim($cfg['host']);
    if ($host === '') {
        return ['ok' => false, 'message' => 'میزبان FTP تنظیم نشده است.'];
    }
    $port = (int) ($cfg['port'] ?? 21);
    $fileSize = $localBytes;
    $timeout = (int) min(900, max(180, 120 + (int) ($fileSize / (200 * 1024))));
    $useSsl = !empty($cfg['use_ssl']) && function_exists('ftp_ssl_connect');
    $conn = $useSsl
        ? @ftp_ssl_connect($host, $port, $timeout)
        : @ftp_connect($host, $port, $timeout);
    if ($conn === false) {
        return ['ok' => false, 'message' => 'اتصال FTP برقرار نشد.'];
    }
    if (!@ftp_login($conn, $cfg['username'], $cfg['password'])) {
        @ftp_close($conn);
        return ['ok' => false, 'message' => 'ورود به FTP ناموفق بود.'];
    }
    $preferPassive = !empty($cfg['passive']);
    @ftp_pasv($conn, $preferPassive);
    $remotePath = trim(str_replace('\\', '/', (string) $cfg['remote_path']), '/');
    if (!backup_ftp_ensure_remote_dir($conn, $remotePath)) {
        @ftp_close($conn);
        return ['ok' => false, 'message' => 'ایجاد یا ورود به پوشهٔ راه‌دور روی FTP ممکن نشد (مسیر: ' . ($remotePath !== '' ? $remotePath : '(ریشه)') . ').'];
    }
    $remoteName = basename($localPath);
    $cwd = @ftp_pwd($conn);
    $cwdTxt = is_string($cwd) ? $cwd : '(نامشخص)';

    $usedAltPassive = false;
    $verified = false;
    $lastPutOk = false;
    for ($attempt = 0; $attempt < 4; $attempt++) {
        if ($attempt > 0) {
            usleep(300000);
        }
        // After a failed or size-mismatched attempt, flip Passive — flaky data channels often cause empty/partial STOR.
        $passiveNow = $preferPassive;
        if ($attempt > 0) {
            $passiveNow = !$preferPassive;
            $usedAltPassive = true;
        }
        @ftp_pasv($conn, $passiveNow);
        $lastPutOk = backup_ftp_put_binary($conn, $remoteName, $localPath);
        if (!$lastPutOk) {
            @ftp_pasv($conn, !$passiveNow);
            $usedAltPassive = true;
            $lastPutOk = backup_ftp_put_binary($conn, $remoteName, $localPath);
        }
        if ($lastPutOk && backup_ftp_upload_size_matches($conn, $remoteName, $localPath)) {
            $verified = true;
            break;
        }
    }

    if (!$verified) {
        clearstatcache(true, $localPath);
        $localSz = (int) (@filesize($localPath) ?: 0);
        $remoteSz = @ftp_size($conn, $remoteName);
        @ftp_close($conn);
        $rs = ($remoteSz === false || $remoteSz === -1) ? 'نامشخص' : (string) (int) $remoteSz;
        $baseMsg = 'آپلود FTP ناموفق یا ناقص (فایل: ' . $remoteName . '، مسیر: ' . $cwdTxt . '). '
            . 'حجم محلی: ' . $localSz . ' بایت؛ حجم گزارش‌شده روی سرور: ' . $rs . '. ';
        if ($lastPutOk && $localSz > 0) {
            return [
                'ok' => false,
                'message' => $baseMsg . 'این حالت معمولاً از تداخل دو کران هم‌زمان، قطع کانال داده، یا نوشتن ناقص است؛ بعد از دیپلوی اخیر نام فایل یکتا و چک حجم اضافه شده است.',
            ];
        }
        return [
            'ok' => false,
            'message' => $baseMsg
                . 'اگر در FileZilla به همان پوشه می‌توانید آپلود کنید، از پشتیبانی هاست بپرسید کانال داده از IP سرور وب باز باشد؛ در غیر این صورت حق نوشتن یا سهمیه را چک کنید.',
        ];
    }

    @ftp_close($conn);
    $msg = 'فایل روی FTP بارگذاری شد: ' . $remoteName;
    if ($usedAltPassive) {
        $msg .= ' (با حالت Passive معکوس نسبت به تنظیمات ذخیره‌شده؛ در پنل همان حالت را ذخیره کنید تا کران بعدی پایدارتر شود).';
    }
    return ['ok' => true, 'message' => $msg];
}
