<?php

declare(strict_types=1);

namespace PpPhp\Modules\Deploy\Controllers;

use PpPhp\Core\Config;
use PpPhp\Core\DB;
use PpPhp\Core\Security;
use PpPhp\Core\ProjectNav;
use PpPhp\Core\ModuleState;
use PpPhp\Core\Deploy\DeployManager;
use PpPhp\Core\Deploy\TransportFactory;
use PpPhp\Core\Deploy\TransportException;
use PpPhp\Core\Deploy\ConnectionRepository;
use PpPhp\Core\Deploy\DbConnectionRepository;
use PpPhp\Core\Deploy\BuildRecorder;
use PpPhp\Core\Deploy\DeploySchema;

/**
 * The FTP/SFTP client — a full deployment workstation: manage connection
 * profiles, browse and manage the live server's files, pick the live database
 * connection, and upload the built project with the chosen file scope.
 */
class DeployController
{
    private array $project = [];
    private string $slug = '';

    public function __construct(
        private Config $config,
    ) {}

    // ── Page ────────────────────────────────────────────────────────

    public function index(string $slug): string
    {
        $this->initProject($slug);
        $project = $this->project;

        $repo = $this->repo();
        $this->migrateLegacyConnections($repo, $slug);

        $state       = ModuleState::load($slug, 'deploy') ?: $this->defaultState();
        $outputState = ModuleState::load($slug, 'output') ?: [];

        $navItems  = ProjectNav::items('/deploy');
        $baseUrl   = $this->config->get('base_url');
        $assetsUrl = $this->config->get('assets_url');
        $user      = $_SESSION['user'];
        $pageTitle = 'Deploy';

        $bootJson = json_encode($this->bootData($repo, $state, $outputState));

        ob_start();
        include __DIR__ . '/../Views/index.php';
        return ob_get_clean();
    }

    // ── State load ──────────────────────────────────────────────────

    public function load(string $slug): void
    {
        $this->json();
        $this->initProject($slug);
        $repo = $this->repo();
        $state       = ModuleState::load($slug, 'deploy') ?: $this->defaultState();
        $outputState = ModuleState::load($slug, 'output') ?: [];
        echo json_encode(['success' => true] + $this->bootData($repo, $state, $outputState));
        exit;
    }

    public function save(string $slug): void
    {
        $this->json();
        $this->initProject($slug);
        \PpPhp\Core\Security::RequireCsrf();
        $input = $this->input();
        $data  = is_array($input['data'] ?? null) ? $input['data'] : [];
        ModuleState::save($slug, 'deploy', $data);
        echo json_encode(['success' => true, 'message' => 'Preferences saved.']);
        exit;
    }

    // ── Connection CRUD ─────────────────────────────────────────────

    public function saveConnection(string $slug): void
    {
        $this->json();
        $this->initProject($slug);
        \PpPhp\Core\Security::RequireCsrf();
        $input   = $this->input();
        $profile = is_array($input['profile'] ?? null) ? $input['profile'] : [];

        if (trim((string) ($profile['name'] ?? '')) === '') {
            echo json_encode(['success' => false, 'message' => 'Give the connection a name.']);
            exit;
        }
        if (trim((string) ($profile['host'] ?? '')) === '') {
            echo json_encode(['success' => false, 'message' => 'Enter a host.']);
            exit;
        }

        $repo = $this->repo();
        $id   = $repo->save($profile);
        echo json_encode(['success' => true, 'message' => 'Connection saved.', 'id' => $id, 'connections' => $repo->forClient()]);
        exit;
    }

    public function deleteConnection(string $slug): void
    {
        $this->json();
        $this->initProject($slug);
        \PpPhp\Core\Security::RequireCsrf();
        $input = $this->input();
        $id    = (int) ($input['id'] ?? 0);
        $repo  = $this->repo();
        $repo->delete($id);
        echo json_encode(['success' => true, 'message' => 'Connection deleted.', 'connections' => $repo->forClient()]);
        exit;
    }

    // ── Live database connection CRUD ───────────────────────────────

    public function saveDbConnection(string $slug): void
    {
        $this->json();
        $this->initProject($slug);
        \PpPhp\Core\Security::RequireCsrf();
        $input = $this->input();
        $db    = is_array($input['db'] ?? null) ? $input['db'] : [];

        if (trim((string) ($db['name'] ?? '')) === '') {
            echo json_encode(['success' => false, 'message' => 'Give the database connection a name.']);
            exit;
        }

        $repo = $this->dbRepo();
        $id   = $repo->save($db);
        echo json_encode(['success' => true, 'message' => 'Live database connection saved.', 'id' => $id, 'dbConnections' => $repo->forClient()]);
        exit;
    }

    public function deleteDbConnection(string $slug): void
    {
        $this->json();
        $this->initProject($slug);
        \PpPhp\Core\Security::RequireCsrf();
        $id   = (int) ($this->input()['id'] ?? 0);
        $repo = $this->dbRepo();
        $repo->delete($id);
        echo json_encode(['success' => true, 'message' => 'Live database connection deleted.', 'dbConnections' => $repo->forClient()]);
        exit;
    }

    /**
     * Verify a live-database connection before it is saved/used. Connects with
     * PDO and runs SELECT 1. Note: this runs from THIS machine, so a production
     * DB that only accepts the app server's IP may still fail here even with
     * correct credentials — the message says so.
     */
    public function testDbConnection(string $slug): void
    {
        $this->json();
        $this->initProject($slug);
        \PpPhp\Core\Security::RequireCsrf();
        $db = is_array($this->input()['db'] ?? null) ? $this->input()['db'] : [];

        // Fill a blank password from the stored record when editing.
        $id = (int) ($db['id'] ?? 0);
        if (($db['pass'] ?? '') === '' && $id > 0) {
            $stored = $this->dbRepo()->dbProfile($id);
            if ($stored) {
                $db['pass'] = $stored['pass'];
            }
        }

        $type = in_array(($db['type'] ?? 'mysql'), ['mysql', 'pgsql', 'sqlite', 'sqlsrv'], true) ? $db['type'] : 'mysql';
        $config = [
            'type'    => $type,
            'host'    => trim((string) ($db['host'] ?? '127.0.0.1')),
            'port'    => trim((string) ($db['port'] ?? '3306')),
            'dbname'  => trim((string) ($db['dbname'] ?? '')),
            'user'    => (string) ($db['user'] ?? ''),
            'pass'    => (string) ($db['pass'] ?? ''),
            'charset' => trim((string) ($db['charset'] ?? 'utf8mb4')) ?: 'utf8mb4',
        ];

        if ($config['dbname'] === '') {
            echo json_encode(['success' => false, 'message' => 'Enter a database name to test.']);
            exit;
        }

        // Fast reachability pre-check for networked drivers so we fail quickly
        // (and clearly) instead of waiting on a long PDO connect timeout.
        if ($type !== 'sqlite' && $config['host'] !== '') {
            $errno = 0; $errstr = '';
            $port = (int) $config['port'] ?: ($type === 'pgsql' ? 5432 : ($type === 'sqlsrv' ? 1433 : 3306));
            $sock = @fsockopen($config['host'], $port, $errno, $errstr, 6);
            if (!$sock) {
                echo json_encode([
                    'success' => false,
                    'message' => "Cannot reach {$config['host']}:{$port} — {$errstr}. Tip: production databases often only accept connections from the app server, not from this machine — the settings can still be correct for the deployed app.",
                ]);
                exit;
            }
            fclose($sock);
        }

        $result = \PpPhp\Core\DatabaseConnection::test($config, constant('BASE_PATH'));
        echo json_encode([
            'success' => (bool) $result['success'],
            'message' => $result['success']
                ? 'Connected — the database is reachable and the credentials work.'
                : (string) $result['message'],
        ]);
        exit;
    }

    // ── Verbose connection test ─────────────────────────────────────

    public function test(string $slug): void
    {
        $this->json();
        $this->initProject($slug);
        \PpPhp\Core\Security::RequireCsrf();
        $profile = $this->resolveProfile($this->input());

        if (empty($profile['host'])) {
            echo json_encode(['success' => false, 'message' => 'Enter a host before testing.', 'report' => null]);
            exit;
        }

        try {
            $transport = TransportFactory::make($profile);
            $report = $transport->diagnose();
        } catch (\Throwable $e) {
            echo json_encode(['success' => false, 'message' => $e->getMessage(), 'report' => null]);
            exit;
        }

        echo json_encode([
            'success' => $report['ok'],
            'message' => $report['summary'],
            'report'  => $report,
        ]);
        exit;
    }

    // ── Remote file browser ─────────────────────────────────────────

    public function remoteList(string $slug): void
    {
        $this->json();
        $this->initProject($slug);
        $input   = $this->input();
        $profile = $this->resolveProfile($input);
        $path    = $this->normalizeRemote((string) ($input['path'] ?? ''), $profile);

        $this->withTransport($profile, function ($t) use ($path) {
            $entries = $t->listDir($path);
            echo json_encode(['success' => true, 'path' => $path, 'entries' => $entries]);
        });
        exit;
    }

    public function remoteMkdir(string $slug): void
    {
        $this->json();
        $this->initProject($slug);
        \PpPhp\Core\Security::RequireCsrf();
        $input   = $this->input();
        $profile = $this->resolveProfile($input);
        $path    = $this->normalizeRemote((string) ($input['path'] ?? ''), $profile);
        $name    = $this->safeSegment((string) ($input['name'] ?? ''));
        if ($name === '') {
            echo json_encode(['success' => false, 'message' => 'Enter a folder name.']);
            exit;
        }
        $target = rtrim($path, '/') . '/' . $name;
        $this->withTransport($profile, function ($t) use ($target, $name) {
            $ok = $t->makeDir($target);
            echo json_encode(['success' => $ok, 'message' => $ok ? "Created “{$name}”." : "Could not create “{$name}” — check write permissions."]);
        });
        exit;
    }

    public function remoteDelete(string $slug): void
    {
        $this->json();
        $this->initProject($slug);
        \PpPhp\Core\Security::RequireCsrf();
        $input   = $this->input();
        $profile = $this->resolveProfile($input);
        $target  = $this->normalizeRemote((string) ($input['path'] ?? ''), $profile);
        $isDir   = !empty($input['isDir']);
        if (trim($target, '/') === '') {
            echo json_encode(['success' => false, 'message' => 'Refusing to delete the server root.']);
            exit;
        }
        $this->withTransport($profile, function ($t) use ($target, $isDir) {
            $ok = $isDir ? $t->removeDir($target) : $t->delete($target);
            echo json_encode(['success' => $ok, 'message' => $ok ? 'Deleted.' : 'Delete failed — the folder may not be empty or you lack permission.']);
        });
        exit;
    }

    public function remoteRename(string $slug): void
    {
        $this->json();
        $this->initProject($slug);
        \PpPhp\Core\Security::RequireCsrf();
        $input   = $this->input();
        $profile = $this->resolveProfile($input);
        $from    = $this->normalizeRemote((string) ($input['from'] ?? ''), $profile);
        $newName = $this->safeSegment((string) ($input['to'] ?? ''));
        if ($newName === '' || trim($from, '/') === '') {
            echo json_encode(['success' => false, 'message' => 'Invalid rename request.']);
            exit;
        }
        $to = rtrim(dirname($from), '/') . '/' . $newName;
        $this->withTransport($profile, function ($t) use ($from, $to) {
            $ok = $t->rename($from, $to);
            echo json_encode(['success' => $ok, 'message' => $ok ? 'Renamed.' : 'Rename failed.']);
        });
        exit;
    }

    // ── Local file browser ──────────────────────────────────────────

    public function localList(string $slug): void
    {
        $this->json();
        $this->initProject($slug);
        $input     = $this->input();
        $outputDir = $this->resolveOutputDir($slug);
        $rel       = trim(str_replace(['..', '\\'], ['', '/'], (string) ($input['path'] ?? '')), '/');
        $dir       = $outputDir . ($rel !== '' ? '/' . $rel : '');

        if (!is_dir($dir)) {
            echo json_encode(['success' => false, 'message' => 'Not built yet — run a build first.', 'entries' => [], 'path' => $rel]);
            exit;
        }

        $entries = [];
        foreach (scandir($dir) ?: [] as $name) {
            if ($name === '.' || $name === '..' || $name === '.git' || $name === '.DS_Store') {
                continue;
            }
            $full = $dir . '/' . $name;
            $entries[] = [
                'name'  => $name,
                'type'  => is_dir($full) ? 'dir' : 'file',
                'size'  => is_file($full) ? (int) filesize($full) : 0,
                'mtime' => @filemtime($full) ?: null,
            ];
        }
        usort($entries, static fn($a, $b) => $a['type'] !== $b['type'] ? ($a['type'] === 'dir' ? -1 : 1) : strcasecmp($a['name'], $b['name']));
        echo json_encode(['success' => true, 'path' => $rel, 'entries' => $entries]);
        exit;
    }

    // ── Upload ──────────────────────────────────────────────────────

    public function upload(string $slug): void
    {
        $this->json();
        $this->initProject($slug);
        \PpPhp\Core\Security::RequireCsrf();
        $input   = $this->input();
        $profile = $this->resolveProfile($input);
        $mode    = (string) ($input['mode'] ?? DeployManager::MODE_CHANGED);
        $dryRun  = !empty($input['dryRun']);
        $dbKey   = (string) ($input['dbConnection'] ?? '');

        if (empty($profile['host'])) {
            echo json_encode(['success' => false, 'message' => 'Select or configure a server connection first.']);
            exit;
        }

        $outputDir = $this->resolveOutputDir($slug);

        // The chosen live-server database is a Deploy-managed connection (by id).
        // It is written ONLY into the uploaded config/database.php — never the
        // local project's own database configuration.
        $dbProfile = null;
        $dbLabel   = '';
        if ($dbKey !== '' && $dbKey !== '__keep__') {
            $dbProfile = $this->dbRepo()->dbProfile((int) $dbKey);
            $dbLabel   = $dbProfile['name'] ?? '';
        }

        $manager = new DeployManager();
        $result  = $manager->deploy($slug, $profile, [
            'mode'         => $mode,
            'outputDir'    => $outputDir,
            'connectionId' => (string) ($profile['id'] ?? 'default'),
            'projectId'    => (int) ($this->project['id'] ?? 0),
            'createdBy'    => (int) (Security::CurrentUserData()['id'] ?? 0) ?: null,
            'dbProfile'    => $dbProfile,
            'dbLabel'      => $dbLabel,
            'dryRun'       => $dryRun,
        ]);

        if ($result['success'] && !$dryRun) {
            $siteUrl = trim((string) ($profile['siteUrl'] ?? ''));
            if ($siteUrl !== '') {
                $result['openUrl'] = $siteUrl;
            }
            $result['history'] = $this->recentUploads();
        }

        echo json_encode($result);
        exit;
    }

    public function history(string $slug): void
    {
        $this->json();
        $this->initProject($slug);
        echo json_encode(['success' => true, 'history' => $this->recentUploads()]);
        exit;
    }

    // ── Boot payload ────────────────────────────────────────────────

    private function bootData(ConnectionRepository $repo, array $state, array $outputState): array
    {
        $projectId = (int) ($this->project['id'] ?? 0);
        $latest    = BuildRecorder::latest($projectId);
        return [
            'connections'   => $repo->forClient(),
            'dbConnections' => $this->dbRepo()->forClient(),
            'options'       => $state['options'] ?? $this->defaultState()['options'],
            'activeId'      => $state['activeId'] ?? 0,
            'lastBuild'     => $this->lastBuildInfo($projectId, $latest),
            'stats'         => [
                'builds'  => BuildRecorder::count($projectId),
                'uploads' => $this->uploadsCount(),
            ],
            'history'       => $this->recentUploads(),
            'outputDir'     => $this->resolveOutputDir($this->slug),
        ];
    }

    private function lastBuildInfo(int $projectId, ?array $latest): ?array
    {
        // Prefer the recorded build; fall back to Finished module state.
        if ($latest) {
            return [
                'time'  => $latest['created_at'],
                'files' => (int) $latest['files'],
                'size'  => $this->humanBytes((int) $latest['size_bytes']),
            ];
        }
        $finished = ModuleState::load($this->slug, 'finished') ?: [];
        $lb = $finished['lastBuild'] ?? null;
        return $lb ? ['time' => $lb['time'] ?? null, 'files' => $lb['files'] ?? 0, 'size' => $lb['size'] ?? '—'] : null;
    }

    // ── Profile resolution ──────────────────────────────────────────

    /**
     * Resolve a transport-ready profile from the request. A saved connection is
     * referenced by id (secrets fetched + decrypted from the DB); an unsaved
     * one comes inline from the editor. Blank secrets fall back to the stored
     * values so "test without retyping the password" works.
     */
    private function resolveProfile(array $input): array
    {
        $profile = is_array($input['profile'] ?? null) ? $input['profile'] : [];
        $id = (int) ($profile['id'] ?? $input['connectionId'] ?? 0);

        if ($id > 0) {
            $stored = $this->repo()->transportProfile($id);
            if ($stored) {
                if (empty($profile['host'])) {
                    return $stored; // browsing/upload by id — use the stored profile
                }
                foreach (['pass', 'privateKeyPass'] as $secret) {
                    if (($profile[$secret] ?? '') === '' && ($stored[$secret] ?? '') !== '') {
                        $profile[$secret] = $stored[$secret];
                    }
                }
            }
        }
        return $profile;
    }

    private function withTransport(array $profile, callable $fn): void
    {
        if (empty($profile['host'])) {
            echo json_encode(['success' => false, 'message' => 'No server connection selected.']);
            return;
        }
        $transport = null;
        try {
            $transport = TransportFactory::make($profile);
            $transport->connect();
            $fn($transport);
        } catch (TransportException $e) {
            echo json_encode(['success' => false, 'message' => $e->getMessage()]);
        } catch (\Throwable $e) {
            echo json_encode(['success' => false, 'message' => 'Unexpected error: ' . $e->getMessage()]);
        } finally {
            if ($transport) {
                $transport->close();
            }
        }
    }

    // ── Helpers ─────────────────────────────────────────────────────

    private function repo(): ConnectionRepository
    {
        return new ConnectionRepository((int) ($this->project['id'] ?? 0));
    }

    private function dbRepo(): DbConnectionRepository
    {
        return new DbConnectionRepository((int) ($this->project['id'] ?? 0));
    }

    private function migrateLegacyConnections(ConnectionRepository $repo, string $slug): void
    {
        $state = ModuleState::load($slug, 'deploy') ?: [];
        if (!empty($state['profiles']) && is_array($state['profiles'])) {
            $repo->importFromState($state['profiles']);
            unset($state['profiles'], $state['activeProfileId']);
            ModuleState::save($slug, 'deploy', $state);
        }
    }

    private function recentUploads(): array
    {
        try {
            DeploySchema::ensureSchema();
            DB::Init();
            DB::SetConnection();
            $rows = DB::FetchAll(
                'SELECT id, connection_name, host, protocol, mode, status, files_uploaded, files_failed, bytes, duration, db_connection, created_at
                 FROM `deploy_uploads` WHERE `project_id` = ? ORDER BY id DESC LIMIT 25',
                [(int) ($this->project['id'] ?? 0)]
            ) ?: [];
            return array_map(static fn($r) => [
                'id'        => (int) $r['id'],
                'name'      => $r['connection_name'],
                'host'      => $r['host'],
                'protocol'  => $r['protocol'],
                'mode'      => $r['mode'],
                'status'    => $r['status'],
                'uploaded'  => (int) $r['files_uploaded'],
                'failed'    => (int) $r['files_failed'],
                'bytes'     => (int) $r['bytes'],
                'duration'  => (float) $r['duration'],
                'db'        => $r['db_connection'],
                'time'      => $r['created_at'],
            ], $rows);
        } catch (\Throwable $e) {
            return [];
        }
    }

    private function uploadsCount(): int
    {
        try {
            DeploySchema::ensureSchema();
            DB::Init();
            DB::SetConnection();
            return (int) DB::FetchOne('SELECT COUNT(*) FROM `deploy_uploads` WHERE `project_id` = ?', [(int) ($this->project['id'] ?? 0)]);
        } catch (\Throwable $e) {
            return 0;
        }
    }

    private function resolveOutputDir(string $slug): string
    {
        $outputState   = ModuleState::load($slug, 'output') ?: [];
        $finishedState = ModuleState::load($slug, 'finished') ?: [];
        $outputDir = $outputState['general']['outputDir'] ?? '';
        if (!empty($finishedState['lastBuild']['outputDir'])) {
            $outputDir = $finishedState['lastBuild']['outputDir'];
        }
        if (empty($outputDir)) {
            $outputDir = constant('BASE_PATH') . '/output/' . $slug;
        } elseif (!str_starts_with($outputDir, '/')) {
            $outputDir = constant('BASE_PATH') . '/' . $outputDir;
        }
        return rtrim($outputDir, '/');
    }

    private function normalizeRemote(string $path, array $profile): string
    {
        $path = trim($path);
        if ($path === '' ) {
            $path = '/' . trim((string) ($profile['remotePath'] ?? ''), '/');
        }
        $path = '/' . ltrim($path, '/');
        // Collapse any ".." attempts.
        $parts = [];
        foreach (explode('/', $path) as $seg) {
            if ($seg === '' || $seg === '.') continue;
            if ($seg === '..') { array_pop($parts); continue; }
            $parts[] = $seg;
        }
        return '/' . implode('/', $parts);
    }

    private function safeSegment(string $s): string
    {
        $s = trim(str_replace(['/', '\\', "\0"], '', $s));
        return $s === '.' || $s === '..' ? '' : $s;
    }

    private function humanBytes(int $bytes): string
    {
        if ($bytes < 1024) return $bytes . ' B';
        $u = ['KB', 'MB', 'GB', 'TB']; $n = $bytes / 1024; $i = 0;
        while ($n >= 1024 && $i < count($u) - 1) { $n /= 1024; $i++; }
        return round($n, $n < 10 ? 1 : 0) . ' ' . $u[$i];
    }

    private function json(): void
    {
        header('Content-Type: application/json');
        // These endpoints do real network I/O (connect, list, upload) that can
        // legitimately exceed PHP's default 30s cap; each transport enforces its
        // own per-operation timeouts, so lift the overall script limit.
        @set_time_limit(300);
        // Never let a stray PHP warning corrupt the JSON body.
        ini_set('display_errors', '0');
    }

    private function input(): array
    {
        $raw = file_get_contents('php://input');
        $decoded = json_decode($raw ?: '', true);
        return is_array($decoded) ? $decoded : $_POST;
    }

    private function initProject(string $slug): void
    {
        Security::RequireAuth();
        $this->slug = $slug;

        DB::Init();
        $project = DB::Select('projects', ['slug' => $slug, 'created_by' => Security::CurrentUserData()['id']]);
        if (empty($project) && Security::IsAdmin()) {
            $project = DB::Select('projects', ['slug' => $slug]);
        }
        if (empty($project)) {
            http_response_code(404);
            echo 'Project not found.';
            exit;
        }
        $this->project = $project[0];
        // Standalone (database-free) projects have no project DB — deploying the
        // built files still works. Skip the project connection so it doesn't fatal
        // into a blank page (matches Finished/CustomFiles).
        if (\PpPhp\Core\ProjectDatabaseProfile::isStandaloneSlug($slug)) {
            return;
        }
        DB::Init($slug);
        DB::SetConnection($slug);
    }

    private function defaultState(): array
    {
        return [
            'options'  => [
                'mode'          => DeployManager::MODE_CHANGED,
                'dbConnection'  => '__keep__',
                'openInBrowser' => true,
            ],
            'activeId' => 0,
        ];
    }
}
