<?php

declare(strict_types=1);

namespace PpPhp\Core\Migrate;

use PpPhp\Core\Deploy\TransportFactory;

/**
 * Drives a MigrationJob through the pipeline.
 *
 * Two entry points:
 *   • run()          — preflight + snapshot (package a verified bundle, install the
 *                      agent). Non-destructive; always safe.
 *   • runTransfer()  — transport the bundle to the destination and drive the agent
 *                      through verify → provision → restore/unpack/rewrite → verify.
 *                      Defaults to a REHEARSAL (dry run: nothing on the target is
 *                      changed); pass $commit = true for the real cutover, which the
 *                      agent still refuses without per-action confirmation.
 *
 * The final FLIP (promote the new server, retire the old) stays a deliberate,
 * operator-driven step rather than something automated here.
 *
 * Each phase updates the job so the browser's progress panel reflects reality even
 * though the work spans a single long request. Nothing throws — failures are
 * captured on the job.
 */
final class MigrationRunner
{
    public function __construct(private MigrationJob $job, private MigrationPlan $plan) {}

    /**
     * Package a verified snapshot. When a destination is configured this also
     * transports it and runs a full non-destructive REHEARSAL against the agent.
     * Never throws — any failure is captured on the job.
     */
    /** Target database credentials for a real commit (empty for a rehearsal). */
    private array $target = [];

    public function setTarget(array $target): void { $this->target = $target; }

    public function run(?string $destPassword = null, bool $commit = false): void
    {
        $this->job->start();
        try {
            if (!$this->preflight($destPassword)) {
                return; // preflight already marked the job failed
            }
            if (!$this->snapshot()) {
                return;
            }
            // With a real destination, continue straight into transport + agent
            // orchestration (rehearsal unless the operator asked to commit).
            if (trim((string) ($this->plan->destination['host'] ?? '')) !== '') {
                $this->runTransfer($destPassword, $commit);
            }
        } catch (\Throwable $e) {
            $this->job->fail('Unexpected error: ' . $e->getMessage());
        }
    }

    /** Probe the destination without changing anything. */
    private function preflight(?string $destPassword): bool
    {
        $this->job->setPhase('preflight', MigrationJob::RUNNING);
        $dest = $this->plan->destination;

        if (($dest['host'] ?? '') === '' || ($dest['user'] ?? '') === '') {
            $msg = 'No destination host/username — snapshot will be packaged locally, ready to connect a destination later.';
            $this->job->log($msg);
            $this->job->setPhase('preflight', MigrationJob::DONE, 'Local-only (no destination yet)');
            return true;
        }

        $profile = [
            'protocol' => (string) ($dest['protocol'] ?? 'sftp'),
            'host'     => (string) $dest['host'],
            'port'     => (int) ($dest['port'] ?? 0) ?: TransportFactory::defaultPort((string) ($dest['protocol'] ?? 'sftp')),
            'user'     => (string) $dest['user'],
            'pass'     => (string) ($destPassword ?? ''),
            'passive'  => true,
            'timeout'  => 20,
        ];

        $this->job->log('Preflight → ' . $profile['protocol'] . '://' . $profile['host'] . ':' . $profile['port']);
        try {
            $transport = TransportFactory::make($profile);
            $report = $transport->diagnose();   // {ok, summary, stages}
            $transport->close();
            foreach ((array) ($report['stages'] ?? []) as $stage) {
                $mark = ($stage['ok'] ?? false) ? '✓' : '✗';
                $this->job->log('  ' . $mark . ' ' . ($stage['stage'] ?? '') . ' — ' . ($stage['detail'] ?? ''));
            }
            if (!($report['ok'] ?? false)) {
                $this->job->setPhase('preflight', MigrationJob::FAILED, (string) ($report['summary'] ?? 'Preflight failed'));
                $this->job->fail('Preflight failed: ' . ($report['summary'] ?? 'destination not reachable'));
                return false;
            }
            $this->job->setPhase('preflight', MigrationJob::DONE, (string) ($report['summary'] ?? 'Reachable'));
            return true;
        } catch (\Throwable $e) {
            $this->job->setPhase('preflight', MigrationJob::FAILED, $e->getMessage());
            $this->job->fail('Preflight error: ' . $e->getMessage());
            return false;
        }
    }

    /** Package the selected scope into the job's bundle. Returns false on failure. */
    private function snapshot(): bool
    {
        $this->job->setPhase('snapshot', MigrationJob::RUNNING);
        $this->job->log('Building snapshot bundle…');

        $builder = new SnapshotBuilder($this->job, $this->plan);
        $result  = $builder->build();

        $warnings = (array) ($result['manifest']['warnings'] ?? []);
        $human    = MigrationInventory::human((int) ($result['bytes'] ?? 0));
        $detail   = $human . ' · ' . (int) ($result['files'] ?? 0) . ' file(s)';

        if (!($result['ok'] ?? false)) {
            $this->job->setPhase('snapshot', MigrationJob::FAILED, 'Nothing could be packaged');
            $this->job->fail('Snapshot produced no usable data: ' . implode('; ', $warnings));
            return false;
        }

        // Plant the self-contained agent + one-time token so the destination can be
        // driven over HTTP once the bundle lands there.
        $agent = AgentBuilder::install($this->job->bundleDir(), $this->job->id(), $this->sourceHost());
        $this->job->setAgentToken($agent['token']);
        $this->job->log('  → agent installed (' . AgentBuilder::AGENT_FILE . ').');

        if ($warnings) {
            foreach ($warnings as $w) { $this->job->log('  ! ' . $w); }
            $detail .= ' · ' . count($warnings) . ' warning(s)';
        }

        $this->job->setPhase('snapshot', MigrationJob::DONE, $detail);
        if (trim((string) ($this->plan->destination['host'] ?? '')) === '') {
            $this->job->finish('Snapshot packaged (' . $detail . '). Connect a destination to transport & cut over.');
        } else {
            $this->job->log('Snapshot packaged (' . $detail . ').');
        }
        return true;
    }

    // ── Transfer (destination-side, agent-driven) ─────────────────────

    /**
     * Transport the bundle and orchestrate the destination agent.
     *
     * @param bool $commit false = rehearsal (dry run, target untouched);
     *                     true  = real cutover (still per-action confirmed by the agent).
     */
    public function runTransfer(?string $destPassword, bool $commit): void
    {
        $dest = $this->plan->destination;

        // 1) TRANSPORT — push the bundle over SFTP/FTP.
        $this->job->setPhase('transport', MigrationJob::RUNNING);
        $profile = [
            'protocol' => (string) ($dest['protocol'] ?? 'sftp'),
            'host'     => (string) ($dest['host'] ?? ''),
            'port'     => (int) ($dest['port'] ?? 0) ?: TransportFactory::defaultPort((string) ($dest['protocol'] ?? 'sftp')),
            'user'     => (string) ($dest['user'] ?? ''),
            'pass'     => (string) ($destPassword ?? ''),
            'passive'  => true,
            'timeout'  => 30,
        ];
        $remoteBase = rtrim((string) ($dest['basePath'] ?? ''), '/') . '/' . AgentBuilder::remoteSubdir($this->job->id());
        $this->job->log('Transporting bundle → ' . $profile['host'] . ':' . $remoteBase);
        try {
            $transport = TransportFactory::make($profile);
            $tx = new BundleTransporter($transport, $this->job->bundleDir(), $remoteBase);
            $res = $tx->upload(fn (string $l) => $this->job->log($l));
        } catch (\Throwable $e) {
            $this->job->setPhase('transport', MigrationJob::FAILED, $e->getMessage());
            $this->job->fail('Transport error: ' . $e->getMessage());
            return;
        }
        if (!($res['ok'] ?? false)) {
            $this->job->setPhase('transport', MigrationJob::FAILED, count($res['failed']) . ' file(s) failed');
            $this->job->fail('Transport failed for: ' . implode(', ', $res['failed']));
            return;
        }
        $this->job->setPhase('transport', MigrationJob::DONE,
            $res['uploaded'] . ' sent · ' . $res['skipped'] . ' skipped · ' . MigrationInventory::human((int) $res['bytes']));

        // The agent must be reachable over HTTP at the new site URL. Without one we
        // cannot orchestrate the destination — stop cleanly with the bundle in place.
        $newUrl = (string) ($dest['newBaseUrl'] ?? '');
        if ($newUrl === '') {
            $this->job->log('No "New site URL" set — bundle is on the destination but the agent cannot be driven automatically yet.');
            $this->job->finish('Bundle transported. Set the new site URL to run verify & restore.');
            return;
        }
        $client = new AgentClient(AgentClient::endpointFor($newUrl, $this->job->id()), $this->job->agentToken());

        // 2) VERIFY (integrity) — agent re-hashes every artifact against the manifest.
        $this->job->setPhase('verify', MigrationJob::RUNNING, 'Checking uploaded files');
        $ping = $client->ping();
        if (!($ping['ok'] ?? false)) {
            $this->job->setPhase('verify', MigrationJob::FAILED, (string) ($ping['error'] ?? 'agent unreachable'));
            $this->job->fail('Destination agent unreachable at ' . $newUrl . ': ' . ($ping['error'] ?? ''));
            return;
        }
        $this->job->log('Agent alive · PHP ' . ($ping['php'] ?? '?') . ' · pdo_mysql=' . var_export($ping['ext']['pdo_mysql'] ?? false, true));
        $targetRoot = (string) ($ping['suggestedRoot'] ?? '');
        $ver = $client->verify();
        $bad = array_values(array_filter((array) ($ver['artifacts'] ?? []), fn ($a) => !($a['ok'] ?? false)));
        if (!($ver['ok'] ?? false)) {
            $this->job->setPhase('verify', MigrationJob::FAILED, count($bad) . ' artifact(s) mismatched');
            foreach ($bad as $a) { $this->job->log('  ✗ ' . ($a['file'] ?? '') . ' checksum mismatch'); }
            $this->job->fail('Integrity check failed on the destination.');
            return;
        }
        $this->job->setPhase('verify', MigrationJob::DONE, count($ver['artifacts'] ?? []) . ' artifact(s) verified');

        // 3) PROVISION + 4) RESTORE are the destructive half. In a rehearsal we drive
        // the agent in dry-run so the whole chain is exercised without changing the
        // target; a commit run needs target DB credentials + explicit confirmation,
        // surfaced by the console. Here we run the safe rehearsal end-to-end.
        $this->rehearseOrCommit($client, $targetRoot, $commit);
    }

    /**
     * Drive provision → restore → unpack → rewrite.
     *
     * A REHEARSAL (no target credentials, or $commit = false) exercises the whole
     * chain in dry-run: the target is never changed. A COMMIT (real target DB
     * credentials + $commit = true) provisions the database, restores the platform
     * dump into it, unpacks the files and rewrites the config for real — each step
     * still confirmed at the agent.
     */
    private function rehearseOrCommit(AgentClient $client, string $targetRoot, bool $commit): void
    {
        $man = json_decode((string) @file_get_contents($this->job->bundleDir() . '/manifest.json'), true) ?: [];
        $dbs = (array) ($man['databases'] ?? []);
        $platform = null;
        foreach ($dbs as $d) { if (($d['kind'] ?? '') === 'platform') { $platform = $d; break; } }

        $creds = $this->targetCreds();               // host/port/user/pass/dbname/create
        $doCommit = $commit && $creds['user'] !== '' && $creds['dbname'] !== '';
        $mode = $doCommit ? 'commit' : 'rehearsal';

        // ── PROVISION ─────────────────────────────────────────────────
        $this->job->setPhase('provision', MigrationJob::RUNNING, ucfirst($mode));
        if ($doCommit) {
            $pr = $client->provision([$creds], false);
            $row = ((array) ($pr['databases'] ?? []))[0] ?? [];
            if (!($row['connected'] ?? false)) {
                $this->job->setPhase('provision', MigrationJob::FAILED, (string) ($row['error'] ?? 'cannot reach target DB'));
                $this->job->fail('Could not provision the target database: ' . ($row['error'] ?? 'unknown'));
                return;
            }
            $this->job->setPhase('provision', MigrationJob::DONE,
                'Database `' . $creds['dbname'] . '` ready' . (($row['created'] ?? false) ? ' (created)' : ''));
        } else {
            $this->job->setPhase('provision', MigrationJob::DONE, 'Rehearsed (no changes)');
            $this->job->log('Provision: rehearsal only (supply target DB credentials to commit).');
        }

        // ── RESTORE + unpack + rewrite ────────────────────────────────
        $this->job->setPhase('restore', MigrationJob::RUNNING, ucfirst($mode));

        if ($doCommit && $platform) {
            // Restore the platform dump into the target database.
            $entry = ['file' => $platform['file'], 'dbname' => $creds['dbname']] + $creds;
            $r = $client->restore([$entry], true, false);
            $ok = ((array) ($r['databases'] ?? []))[0]['ok'] ?? false;
            if (!$ok) {
                $err = ((array) ($r['databases'] ?? []))[0]['error'] ?? 'restore failed';
                $this->job->setPhase('restore', MigrationJob::FAILED, (string) $err);
                $this->job->fail('Platform restore failed: ' . $err);
                return;
            }
            $stmts = ((array) ($r['databases'] ?? []))[0]['statements'] ?? 0;
            $this->job->log('Restored platform database → ' . $creds['dbname'] . ' (' . $stmts . ' statements).');

            // Unpack files + rewrite config on the target.
            if ($targetRoot !== '') {
                $u = $client->unpack($targetRoot, true, false);
                $this->job->log('Unpacked ' . count((array) ($u['files'] ?? [])) . ' archive(s) into ' . $targetRoot . '.');
                $rw = $client->rewrite($targetRoot, [
                    'host' => $creds['host'], 'port' => (string) $creds['port'],
                    'dbname' => $creds['dbname'], 'user' => $creds['user'], 'pass' => $creds['pass'],
                ], (string) ($this->plan->destination['newBaseUrl'] ?? ''), true, false);
                $this->job->log('Rewrote config on target: ' . ($rw['ok'] ? 'ok' : (string) ($rw['error'] ?? 'failed')));
            }

            $managed = array_values(array_filter($dbs, fn ($d) => ($d['kind'] ?? '') === 'managed'));
            if ($managed) {
                $this->job->log('Note: ' . count($managed) . ' managed user database(s) were transported and verified; restore each with its own target credentials from the console.');
            }
            $this->job->setPhase('restore', MigrationJob::DONE, 'Committed · platform DB + files restored');
            $this->job->finish('Migration committed: platform database restored, files unpacked, config rewritten on ' . ($this->plan->destination['newBaseUrl'] ?? 'the destination') . '. Verify the new site, then flip DNS to cut over.');
        } else {
            // Rehearsal: prove statement counts, extraction and writability, no writes.
            $dbFiles = array_map(fn ($d) => ['file' => $d['file'], 'dbname' => $d['name'] ?? ''], $dbs);
            $r = $client->restore($dbFiles, false, true);
            $stmts = array_sum(array_map(fn ($x) => (int) ($x['statements'] ?? 0), (array) ($r['databases'] ?? [])));
            $this->job->log('Restore rehearsal: ' . $stmts . ' statement(s) across ' . count($dbFiles) . ' database(s).');
            if ($targetRoot !== '') {
                $u = $client->unpack($targetRoot, false, true);
                $entries = array_sum(array_map(fn ($x) => (int) ($x['entries'] ?? 0), (array) ($u['files'] ?? [])));
                $this->job->log('Unpack rehearsal: ' . $entries . ' file(s) would extract into ' . $targetRoot . '.');
                $rw = $client->rewrite($targetRoot, [], '', false, true);
                $this->job->log('Config rewrite rehearsal: ' . (($rw['configDirWritable'] ?? false) ? 'config writable' : 'config NOT writable') . '.');
            }
            $this->job->setPhase('restore', MigrationJob::DONE, 'Rehearsed · ' . $stmts . ' statement(s)');
            $this->job->finish('Full rehearsal complete — bundle transported, integrity verified, restore plan validated. Nothing on the destination was changed. Supply target DB credentials and commit to cut over.');
        }
    }

    /** Resolve the target DB credentials supplied for a commit. */
    private function targetCreds(): array
    {
        $t = $this->target;
        return [
            'host'   => trim((string) ($t['host'] ?? 'localhost')) ?: 'localhost',
            'port'   => (int) ($t['port'] ?? 3306) ?: 3306,
            'user'   => trim((string) ($t['user'] ?? '')),
            'pass'   => (string) ($t['pass'] ?? ''),
            'dbname' => trim((string) ($t['dbname'] ?? '')),
            'create' => !empty($t['create']),
        ];
    }

    private function sourceHost(): string
    {
        return (string) ($_SERVER['HTTP_HOST'] ?? gethostname() ?: '');
    }
}
