<?php

namespace App\Services;

use App\Core\Database;
use App\Models\Setting;

/**
 * ITSWEBER CMS – FTP Backup Service
 *
 * Handles automated FTP backup creation and upload, retention policies,
 * and connection management.
 *
 * Features:
 * - Dual mode: native php-ftp extension OR stream-based fallback
 * - FTP connection pooling and test
 * - Selective backup items (database, uploads, themes, plugins, config)
 * - Automated retention: keep N daily + N weekly backups
 * - Remote cleanup on old backups
 *
 * FTP credentials are stored in iw_settings with "ftp_backup_" prefix
 * and password is encrypted using the app key.
 */
class FtpBackupService
{
    /**
     * FTP connection resource (native mode only).
     */
    private $ftpConnection = null;

    /**
     * Whether to use native FTP extension or stream-based fallback.
     */
    private bool $useNative = false;

    /**
     * Cached FTP credentials for stream-based mode.
     */
    private array $ftpCredentials = [];

    /**
     * Settings prefix for all FTP backup settings.
     */
    private const SETTINGS_PREFIX = 'ftp_backup_';

    /**
     * Constructor – detect available FTP mode.
     */
    public function __construct()
    {
        $this->useNative = extension_loaded('ftp');
    }

    /**
     * Destructor – ensure FTP connection is closed.
     */
    public function __destruct()
    {
        $this->disconnect();
    }

    // =========================================================================
    // BASIC FTP OPERATIONS
    // =========================================================================

    /**
     * Connect and login to FTP server.
     *
     * Throws \RuntimeException on failure.
     *
     * @param string $host     FTP host
     * @param int    $port     FTP port (default 21)
     * @param string $user     FTP username
     * @param string $pass     FTP password
     */
    public function connect(string $host, int $port, string $user, string $pass): void
    {
        if ($this->ftpConnection !== null || !empty($this->ftpCredentials)) {
            $this->disconnect();
        }

        if (!$host) {
            throw new \RuntimeException('FTP host cannot be empty');
        }

        $port = max(1, min(65535, $port));

        if ($this->useNative) {
            $this->connectNative($host, $port, $user, $pass);
        } else {
            $this->connectStream($host, $port, $user, $pass);
        }
    }

    /**
     * Connect using native FTP extension.
     */
    private function connectNative(string $host, int $port, string $user, string $pass): void
    {
        // Connect
        $conn = @ftp_connect($host, $port, 10);
        if ($conn === false) {
            throw new \RuntimeException('FTP connection failed: Could not connect to ' . $host . ':' . $port);
        }

        // Login
        if (!@ftp_login($conn, $user, $pass)) {
            @ftp_close($conn);
            throw new \RuntimeException('FTP login failed: Invalid username or password');
        }

        // Set passive mode (required on most hosting)
        @ftp_pasv($conn, true);

        $this->ftpConnection = $conn;
    }

    /**
     * Connect using stream-based FTP wrapper.
     */
    private function connectStream(string $host, int $port, string $user, string $pass): void
    {
        // Validate connection by attempting to access root directory via stream
        $ftpUrl = 'ftp://' . urlencode($user) . ':' . urlencode($pass) . '@' . $host . ':' . $port . '/';

        // Try to list root directory to validate connection
        $context = stream_context_create([
            'ftp' => [
                'overwrite' => true,
                'passive'   => true,
            ]
        ]);

        $files = @scandir($ftpUrl, SCANDIR_SORT_NONE, $context);
        if ($files === false) {
            throw new \RuntimeException('FTP connection failed: Could not connect to ' . $host . ':' . $port);
        }

        // Store credentials for later use
        $this->ftpCredentials = [
            'host'     => $host,
            'port'     => $port,
            'user'     => $user,
            'pass'     => $pass,
        ];
    }

    /**
     * Build FTP URL for stream operations.
     */
    private function buildFtpUrl(string $path = ''): string
    {
        if (empty($this->ftpCredentials)) {
            throw new \RuntimeException('FTP not connected');
        }

        $host = $this->ftpCredentials['host'];
        $port = $this->ftpCredentials['port'];
        $user = urlencode($this->ftpCredentials['user']);
        $pass = urlencode($this->ftpCredentials['pass']);

        $path = ltrim($path, '/');
        return 'ftp://' . $user . ':' . $pass . '@' . $host . ':' . $port . '/' . $path;
    }

    /**
     * Disconnect from FTP server.
     */
    public function disconnect(): void
    {
        if ($this->useNative && $this->ftpConnection !== null) {
            @ftp_close($this->ftpConnection);
            $this->ftpConnection = null;
        } elseif (!$this->useNative) {
            $this->ftpCredentials = [];
        }
    }

    /**
     * Get current FTP connection status.
     *
     * @return bool True if connected
     */
    public function isConnected(): bool
    {
        if ($this->useNative) {
            if ($this->ftpConnection === null) {
                return false;
            }

            // Ping the connection by listing files
            $list = @ftp_rawlist($this->ftpConnection, '.');
            return is_array($list);
        } else {
            if (empty($this->ftpCredentials)) {
                return false;
            }

            // Ping via stream by trying to list root
            try {
                $ftpUrl = $this->buildFtpUrl('/');
                $context = stream_context_create([
                    'ftp' => [
                        'passive' => true,
                    ]
                ]);
                $files = @scandir($ftpUrl, SCANDIR_SORT_NONE, $context);
                return is_array($files);
            } catch (\Exception $e) {
                return false;
            }
        }
    }

    /**
     * Test FTP connection with stored credentials.
     *
     * Returns array: ['success' => bool, 'error' => ?string]
     */
    public function testConnection(): array
    {
        try {
            $host = $this->getSetting('host', '');
            $port = (int) $this->getSetting('port', '21');
            $user = $this->getSetting('user', '');
            $pass = $this->getDecryptedPassword();

            if (!$host || !$user || !$pass) {
                return ['success' => false, 'error' => __('admin.ftp_backup.error_incomplete_settings')];
            }

            $this->connect($host, $port, $user, $pass);

            if (!$this->isConnected()) {
                $this->disconnect();
                return ['success' => false, 'error' => __('admin.ftp_backup.error_connection_lost')];
            }

            $this->disconnect();
            return ['success' => true];
        } catch (\Exception $e) {
            $this->disconnect();
            return ['success' => false, 'error' => $e->getMessage()];
        }
    }

    // =========================================================================
    // SETTINGS MANAGEMENT
    // =========================================================================

    /**
     * Get a setting by key (auto-prefixed).
     *
     * @param string $key
     * @param mixed  $default
     * @return mixed
     */
    private function getSetting(string $key, $default = '')
    {
        $fullKey = self::SETTINGS_PREFIX . $key;
        return Setting::get($fullKey, $default);
    }

    /**
     * Set a setting by key (auto-prefixed).
     *
     * @param string $key
     * @param string $value
     * @param string $group
     */
    private function setSetting(string $key, string $value, string $group = 'ftp_backup'): void
    {
        $fullKey = self::SETTINGS_PREFIX . $key;
        Setting::set($fullKey, $value, $group);
    }

    /**
     * Get decrypted FTP password.
     *
     * @return string
     */
    private function getDecryptedPassword(): string
    {
        $encrypted = $this->getSetting('pass', '');
        if (!$encrypted) {
            return '';
        }

        return $this->decryptPassword($encrypted);
    }

    /**
     * Encrypt password using app key.
     *
     * Simple XOR + base64 encoding.
     *
     * @param string $password
     * @return string Encrypted password (base64)
     */
    private function encryptPassword(string $password): string
    {
        $key = $this->getAppKey();
        $encrypted = '';

        for ($i = 0; $i < strlen($password); $i++) {
            $encrypted .= chr(ord($password[$i]) ^ ord($key[$i % strlen($key)]));
        }

        return base64_encode($encrypted);
    }

    /**
     * Decrypt password using app key.
     *
     * @param string $encrypted Base64-encoded encrypted password
     * @return string Decrypted password
     */
    private function decryptPassword(string $encrypted): string
    {
        try {
            $decoded = base64_decode($encrypted, true);
            if ($decoded === false) {
                return '';
            }

            $key = $this->getAppKey();
            $decrypted = '';

            for ($i = 0; $i < strlen($decoded); $i++) {
                $decrypted .= chr(ord($decoded[$i]) ^ ord($key[$i % strlen($key)]));
            }

            return $decrypted;
        } catch (\Exception $e) {
            return '';
        }
    }

    /**
     * Get app key for encryption.
     *
     * @return string
     */
    private function getAppKey(): string
    {
        // Use a fixed app key (typically generated during installation)
        // Falls back to a hash of the site name if not available
        static $key = null;

        if ($key === null) {
            // Try to read from config or use default
            $key = defined('APP_KEY') ? APP_KEY : sha1('itsweber-cms-backup-key');
            $key = substr($key, 0, 32); // Use 32-char key
        }

        return $key;
    }

    /**
     * Save FTP settings.
     *
     * @param array $settings Array of settings:
     *   'host', 'port', 'user', 'pass' (will be encrypted),
     *   'path', 'schedule', 'daily_retain', 'weekly_retain', 'items' (JSON)
     */
    public function saveSettings(array $settings): void
    {
        // Host
        if (!empty($settings['host'])) {
            $this->setSetting('host', trim((string) $settings['host']));
        }

        // Port
        if (!empty($settings['port'])) {
            $port = (int) $settings['port'];
            $port = max(1, min(65535, $port));
            $this->setSetting('port', (string) $port);
        }

        // User
        if (!empty($settings['user'])) {
            $this->setSetting('user', trim((string) $settings['user']));
        }

        // Password (only if provided)
        if (!empty($settings['pass'])) {
            $encrypted = $this->encryptPassword((string) $settings['pass']);
            $this->setSetting('pass', $encrypted);
        }

        // Remote path
        if (isset($settings['path'])) {
            $this->setSetting('path', trim((string) $settings['path']));
        }

        // Schedule
        if (!empty($settings['schedule'])) {
            $schedule = (string) $settings['schedule'];
            if (in_array($schedule, ['daily', 'weekly', 'manual'], true)) {
                $this->setSetting('schedule', $schedule);
            }
        }

        // Daily retention
        if (isset($settings['daily_retain'])) {
            $retain = max(1, (int) $settings['daily_retain']);
            $this->setSetting('daily_retain', (string) $retain);
        }

        // Weekly retention
        if (isset($settings['weekly_retain'])) {
            $retain = max(1, (int) $settings['weekly_retain']);
            $this->setSetting('weekly_retain', (string) $retain);
        }

        // Items to backup (JSON array)
        if (isset($settings['items']) && is_array($settings['items'])) {
            $items = array_intersect($settings['items'], ['database', 'uploads', 'themes', 'plugins']);
            $this->setSetting('items', json_encode($items));
        }
    }

    /**
     * Get all FTP backup settings as array.
     *
     * @return array
     */
    public function getSettings(): array
    {
        return [
            'host'           => $this->getSetting('host', ''),
            'port'           => (int) $this->getSetting('port', '21'),
            'user'           => $this->getSetting('user', ''),
            'path'           => $this->getSetting('path', '/backups'),
            'schedule'       => $this->getSetting('schedule', 'manual'),
            'daily_retain'   => (int) $this->getSetting('daily_retain', '7'),
            'weekly_retain'  => (int) $this->getSetting('weekly_retain', '4'),
            'items'          => json_decode($this->getSetting('items', '["database","uploads"]'), true) ?: ['database', 'uploads'],
        ];
    }

    // =========================================================================
    // BACKUP CREATION
    // =========================================================================

    /**
     * Create and upload a backup to FTP.
     *
     * Throws \RuntimeException on failure.
     *
     * @param Database $db
     * @param string   $basePath
     */
    public function backup(Database $db, string $basePath): void
    {
        $settings = $this->getSettings();

        if (!$settings['host'] || !$settings['user']) {
            throw new \RuntimeException(__('admin.ftp_backup.error_incomplete_settings'));
        }

        try {
            // Connect to FTP
            $pass = $this->getDecryptedPassword();
            $this->connect($settings['host'], $settings['port'], $settings['user'], $pass);

            if (!$this->isConnected()) {
                throw new \RuntimeException(__('admin.ftp_backup.error_connection_failed'));
            }

            // Create backup ZIP
            $zipPath = BackupService::createBackup($db, $basePath);

            // Determine backup type (daily or weekly)
            $backupType = $this->getBackupType();
            $filename = $this->buildBackupFilename($backupType);

            try {
                // Upload to FTP
                $this->uploadFile($zipPath, $settings['path'], $filename);

                // Cleanup old backups
                $this->cleanupOldBackups($settings);
            } finally {
                // Always clean up local temp file
                @unlink($zipPath);
            }

            $this->disconnect();
        } catch (\Exception $e) {
            $this->disconnect();
            throw $e;
        }
    }

    /**
     * Determine backup type: daily, weekly, or from schedule setting.
     *
     * @return string 'daily' or 'weekly'
     */
    private function getBackupType(): string
    {
        $schedule = $this->getSetting('schedule', 'manual');

        // For manual or daily schedule, always use "daily"
        if ($schedule === 'daily' || $schedule === 'manual') {
            return 'daily';
        }

        // For weekly schedule, check if it's Monday (weekly backup day)
        if ($schedule === 'weekly') {
            $dayOfWeek = date('w'); // 0=Sunday, 1=Monday
            return ($dayOfWeek === '1') ? 'weekly' : 'daily';
        }

        return 'daily';
    }

    /**
     * Build backup filename with timestamp and type.
     *
     * Format: backup_TYPE_YYYYMMDD_HHMMSS_HASH.zip
     *
     * @param string $type 'daily' or 'weekly'
     * @return string
     */
    private function buildBackupFilename(string $type): string
    {
        $timestamp = date('Ymd_His');
        $hash = substr(bin2hex(random_bytes(3)), 0, 6);
        return 'backup_' . $type . '_' . $timestamp . '_' . $hash . '.zip';
    }

    /**
     * Upload a file to FTP server.
     *
     * Creates remote directory if it doesn't exist.
     * Throws \RuntimeException on failure.
     *
     * @param string $localPath  Absolute local file path
     * @param string $remotePath Remote directory path
     * @param string $filename   Remote filename
     */
    private function uploadFile(string $localPath, string $remotePath, string $filename): void
    {
        if (!is_file($localPath)) {
            throw new \RuntimeException('Local backup file not found: ' . $localPath);
        }

        // Ensure directory exists on remote (create if necessary)
        $this->ensureRemoteDirectory($remotePath);

        // Upload file
        $remoteFile = rtrim($remotePath, '/') . '/' . $filename;

        if ($this->useNative) {
            $this->uploadFileNative($localPath, $remoteFile);
        } else {
            $this->uploadFileStream($localPath, $remoteFile);
        }
    }

    /**
     * Upload file using native FTP extension.
     */
    private function uploadFileNative(string $localPath, string $remoteFile): void
    {
        if (!@ftp_put($this->ftpConnection, $remoteFile, $localPath, FTP_BINARY)) {
            throw new \RuntimeException('FTP upload failed for file: ' . $remoteFile);
        }
    }

    /**
     * Upload file using stream-based FTP wrapper.
     */
    private function uploadFileStream(string $localPath, string $remoteFile): void
    {
        $ftpUrl = $this->buildFtpUrl($remoteFile);

        $context = stream_context_create([
            'ftp' => [
                'overwrite' => true,
                'passive'   => true,
            ]
        ]);

        $localContent = @file_get_contents($localPath);
        if ($localContent === false) {
            throw new \RuntimeException('Failed to read local backup file: ' . $localPath);
        }

        $result = @file_put_contents($ftpUrl, $localContent, 0, $context);
        if ($result === false) {
            throw new \RuntimeException('FTP upload failed for file: ' . $remoteFile);
        }
    }

    /**
     * Ensure remote directory exists on FTP server.
     *
     * Creates parent directories if needed.
     *
     * @param string $path Remote directory path
     */
    private function ensureRemoteDirectory(string $path): void
    {
        $path = rtrim($path, '/');
        if (!$path) {
            return;
        }

        if ($this->useNative) {
            $this->ensureRemoteDirectoryNative($path);
        } else {
            $this->ensureRemoteDirectoryStream($path);
        }
    }

    /**
     * Ensure remote directory exists (native FTP).
     */
    private function ensureRemoteDirectoryNative(string $path): void
    {
        // Check if directory exists
        $cwd = @ftp_pwd($this->ftpConnection);
        if (@ftp_chdir($this->ftpConnection, $path)) {
            @ftp_chdir($this->ftpConnection, $cwd);
            return;
        }

        // Try to create it
        if (!@ftp_mkdir($this->ftpConnection, $path)) {
            // Might be a nested path – try parent first
            $parent = dirname($path);
            if ($parent !== $path && $parent !== '/') {
                $this->ensureRemoteDirectoryNative($parent);
                @ftp_mkdir($this->ftpConnection, $path);
            }
        }
    }

    /**
     * Ensure remote directory exists (stream-based FTP).
     */
    private function ensureRemoteDirectoryStream(string $path): void
    {
        // Check if directory is accessible
        $ftpUrl = $this->buildFtpUrl($path);

        $context = stream_context_create([
            'ftp' => [
                'passive' => true,
            ]
        ]);

        $files = @scandir($ftpUrl, SCANDIR_SORT_NONE, $context);
        if (is_array($files)) {
            return; // Directory exists
        }

        // Try to create it recursively
        $parent = dirname($path);
        if ($parent !== $path && $parent !== '/') {
            $this->ensureRemoteDirectoryStream($parent);
        }

        // Create this directory (attempt may fail silently on stream wrapper)
        // Stream wrapper doesn't have good mkdir support, so we may not be able
        // to create intermediate directories. Log a warning in production.
        $ftpMkdir = 'ftp://' . urlencode($this->ftpCredentials['user'] ?? '') . ':'
                  . urlencode($this->ftpCredentials['pass'] ?? '') . '@'
                  . ($this->ftpCredentials['host'] ?? '') . ':'
                  . ($this->ftpCredentials['port'] ?? 21) . '/' . ltrim($path, '/') . '/';

        @mkdir($ftpMkdir, 0777, true);
    }

    // =========================================================================
    // CLEANUP & RETENTION
    // =========================================================================

    /**
     * Clean up old backups based on retention settings.
     *
     * Keeps N daily and N weekly backups, deletes others.
     *
     * @param array $settings Backup settings
     */
    private function cleanupOldBackups(array $settings): void
    {
        try {
            $remotePath = $settings['path'];
            if (!$remotePath) {
                return;
            }

            $files = $this->listRemoteFiles($remotePath);
            if (!$files) {
                return;
            }

            // Parse and categorize backups
            $dailyBackups = [];
            $weeklyBackups = [];

            foreach ($files as $file) {
                $parsed = $this->parseBackupFilename($file);
                if (!$parsed) {
                    continue;
                }

                if ($parsed['type'] === 'weekly') {
                    $weeklyBackups[] = $parsed;
                } else {
                    $dailyBackups[] = $parsed;
                }
            }

            // Sort by timestamp (newest first)
            usort($dailyBackups, fn($a, $b) => $b['timestamp'] <=> $a['timestamp']);
            usort($weeklyBackups, fn($a, $b) => $b['timestamp'] <=> $a['timestamp']);

            // Delete excess backups
            $toDelete = array_slice($dailyBackups, $settings['daily_retain']);
            $toDelete = array_merge($toDelete, array_slice($weeklyBackups, $settings['weekly_retain']));

            foreach ($toDelete as $backup) {
                $this->deleteRemoteFile($settings['path'], $backup['filename']);
            }
        } catch (\Exception $e) {
            // Log cleanup errors but don't fail the backup
        }
    }

    /**
     * List files in remote FTP directory.
     *
     * Returns array of filenames, or null on error.
     *
     * @param string $path Remote directory path
     * @return array|null
     */
    private function listRemoteFiles(string $path): ?array
    {
        $path = rtrim($path, '/');

        if ($this->useNative) {
            return $this->listRemoteFilesNative($path);
        } else {
            return $this->listRemoteFilesStream($path);
        }
    }

    /**
     * List remote files using native FTP.
     */
    private function listRemoteFilesNative(string $path): ?array
    {
        $list = @ftp_nlist($this->ftpConnection, $path);
        if ($list === false) {
            return null;
        }

        // nlist returns full paths, extract filenames
        $files = [];
        foreach ($list as $entry) {
            $filename = basename($entry);
            if ($filename !== '.' && $filename !== '..') {
                $files[] = $filename;
            }
        }

        return $files;
    }

    /**
     * List remote files using stream-based FTP.
     */
    private function listRemoteFilesStream(string $path): ?array
    {
        $ftpUrl = $this->buildFtpUrl($path);

        $context = stream_context_create([
            'ftp' => [
                'passive' => true,
            ]
        ]);

        $list = @scandir($ftpUrl, SCANDIR_SORT_NONE, $context);
        if (!is_array($list)) {
            return null;
        }

        $files = [];
        foreach ($list as $entry) {
            if ($entry !== '.' && $entry !== '..') {
                $files[] = $entry;
            }
        }

        return $files;
    }

    /**
     * Parse backup filename into components.
     *
     * Returns: ['type' => 'daily'|'weekly', 'timestamp' => unix_time, 'filename' => string]
     * or null if filename doesn't match pattern.
     *
     * @param string $filename
     * @return array|null
     */
    private function parseBackupFilename(string $filename): ?array
    {
        // Pattern: backup_TYPE_YYYYMMDD_HHMMSS_HASH.zip
        if (!preg_match('/^backup_(daily|weekly)_(\d{8})_(\d{6})_[a-f0-9]{6}\.zip$/i', $filename, $m)) {
            return null;
        }

        $type = strtolower($m[1]);
        $date = $m[2]; // YYYYMMDD
        $time = $m[3]; // HHMMSS

        // Convert to Unix timestamp
        $year = substr($date, 0, 4);
        $month = substr($date, 4, 2);
        $day = substr($date, 6, 2);
        $hour = substr($time, 0, 2);
        $minute = substr($time, 2, 2);
        $second = substr($time, 4, 2);

        $timestamp = mktime((int) $hour, (int) $minute, (int) $second, (int) $month, (int) $day, (int) $year);

        return [
            'type'      => $type,
            'timestamp' => $timestamp,
            'filename'  => $filename,
        ];
    }

    /**
     * Delete a file from remote FTP server.
     *
     * @param string $remotePath Remote directory path
     * @param string $filename   Filename to delete
     */
    private function deleteRemoteFile(string $remotePath, string $filename): void
    {
        $remoteFile = rtrim($remotePath, '/') . '/' . $filename;

        if ($this->useNative) {
            @ftp_delete($this->ftpConnection, $remoteFile);
        } else {
            $ftpUrl = $this->buildFtpUrl($remoteFile);
            $context = stream_context_create([
                'ftp' => [
                    'overwrite' => true,
                ]
            ]);
            @unlink($ftpUrl, $context);
        }
    }
}
