<?php

declare(strict_types=1);

namespace PpPhp\Core\Migrate;

/**
 * Durable state for a single migration run.
 *
 * A migration spans several requests (the browser polls for progress while the
 * work happens), so its state cannot live in memory. Each job owns a folder under
 * `storage/migrations/<id>/`:
 *
 *   job.json      – this state object (status, phases, progress, log tail)
 *   plan.json     – the MigrationPlan (scope + destination, no secrets)
 *   migrate.log   – the full human log, appended line by line
 *   bundle/       – the packaged snapshot the transport phase pushes
 *
 * TIME: every timestamp here is an internal epoch from PHP's `time()` and is only
 * ever shown relative to other job timestamps or the current PHP clock. It is
 * never compared against a MySQL value, so the platform's PHP↔MySQL clock skew
 * does not apply. (DB *content* timestamps that DbCloner copies are opaque bytes.)
 */
final class MigrationJob
{
    public const PENDING = 'pending';
    public const RUNNING = 'running';
    public const DONE    = 'done';
    public const FAILED  = 'failed';
    /** A phase whose executor is not built yet — shown as a roadmap step, not a failure. */
    public const STAGED  = 'staged';

    /** The ordered pipeline. */
    public const PHASES = [
        'preflight' => 'Preflight',
        'snapshot'  => 'Snapshot & package',
        'transport' => 'Transport to destination',
        'provision' => 'Provision databases',
        'restore'   => 'Restore on destination',
        'verify'    => 'Verify',
        'flip'      => 'Cut over',
    ];

    /** Phases whose executors ship in this build; the rest are STAGED roadmap steps. */
    public const IMPLEMENTED = ['preflight', 'snapshot', 'transport', 'provision', 'restore', 'verify'];

    private array $data;

    private function __construct(array $data)
    {
        $this->data = $data;
    }

    // ── Locations ─────────────────────────────────────────────────────

    public static function root(): string
    {
        return rtrim(constant('BASE_PATH'), '/') . '/storage/migrations';
    }

    public function dir(): string
    {
        return self::root() . '/' . $this->id();
    }

    public function bundleDir(): string
    {
        return $this->dir() . '/bundle';
    }

    /**
     * The raw agent token, kept in the job folder (NOT the bundle) so it never
     * travels to the destination. Read/written as a plain file with tight perms.
     */
    public function setAgentToken(string $token): void
    {
        @file_put_contents($this->dir() . '/agent-token.txt', $token, LOCK_EX);
        @chmod($this->dir() . '/agent-token.txt', 0600);
    }

    public function agentToken(): string
    {
        return trim((string) @file_get_contents($this->dir() . '/agent-token.txt'));
    }

    // ── Lifecycle ─────────────────────────────────────────────────────

    /** Create a fresh job for a plan and persist its initial state. */
    public static function create(MigrationPlan $plan): self
    {
        $id = self::newId();
        // The transfer phases only apply when a destination is configured; a
        // package-only run stages them so progress is measured over the two local
        // phases and still reaches 100%.
        $hasDest = trim((string) ($plan->destination['host'] ?? '')) !== '';
        $localOnly = ['preflight', 'snapshot'];
        $phases = [];
        foreach (self::PHASES as $key => $label) {
            $active = in_array($key, self::IMPLEMENTED, true)
                && ($hasDest || in_array($key, $localOnly, true));
            $status = $active ? self::PENDING : self::STAGED;
            $detail = $status === self::STAGED ? 'Runs once a destination is connected' : '';
            $phases[$key] = ['label' => $label, 'status' => $status, 'detail' => $detail, 'at' => null];
        }
        $job = new self([
            'id'        => $id,
            'status'    => self::PENDING,
            'phase'     => '',
            'progress'  => 0,
            'scope'     => $plan->scopeSummary(),
            'created'   => time(),
            'updated'   => time(),
            'phases'    => $phases,
            'log'       => [],
            'error'     => '',
            'bundle'    => ['bytes' => 0, 'files' => 0],
        ]);
        @mkdir($job->bundleDir(), 0700, true);
        @file_put_contents($job->dir() . '/plan.json',
            json_encode($plan->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
        $job->save();
        return $job;
    }

    public static function load(string $id): ?self
    {
        $id = self::safeId($id);
        if ($id === '') { return null; }
        $file = self::root() . '/' . $id . '/job.json';
        if (!is_file($file)) { return null; }
        $data = json_decode((string) @file_get_contents($file), true);
        return is_array($data) ? new self($data) : null;
    }

    public function plan(): ?MigrationPlan
    {
        $file = $this->dir() . '/plan.json';
        if (!is_file($file)) { return null; }
        $a = json_decode((string) @file_get_contents($file), true);
        return is_array($a) ? MigrationPlan::fromArray($a) : null;
    }

    /** Most-recent-first list of jobs, for the console history. */
    public static function recent(int $limit = 10): array
    {
        $out = [];
        foreach (glob(self::root() . '/*/job.json') ?: [] as $f) {
            $d = json_decode((string) @file_get_contents($f), true);
            if (is_array($d)) { $out[] = $d; }
        }
        usort($out, static fn ($a, $b) => (int) ($b['created'] ?? 0) <=> (int) ($a['created'] ?? 0));
        return array_slice($out, 0, $limit);
    }

    // ── Mutation ──────────────────────────────────────────────────────

    public function start(): void
    {
        $this->data['status'] = self::RUNNING;
        $this->save();
    }

    public function setPhase(string $key, string $status, string $detail = ''): void
    {
        if (isset($this->data['phases'][$key])) {
            $this->data['phases'][$key]['status'] = $status;
            $this->data['phases'][$key]['detail'] = $detail;
            $this->data['phases'][$key]['at']     = time();
        }
        if ($status === self::RUNNING) {
            $this->data['phase'] = $key;
        }
        // Progress = share of the ACTIVE phases that have completed. A phase is
        // active when it is not STAGED, so the bar measures only the path this job
        // actually runs (package-only vs full transfer) and reaches 100% on finish.
        $done = 0; $total = 0;
        foreach ($this->data['phases'] as $p) {
            if (($p['status'] ?? '') === self::STAGED) { continue; }
            $total++;
            if (($p['status'] ?? '') === self::DONE) { $done++; }
        }
        $this->data['progress'] = (int) round($done / max(1, $total) * 100);
        $this->save();
    }

    public function log(string $line): void
    {
        $stamp = gmdate('H:i:s');
        $entry = '[' . $stamp . '] ' . $line;
        $this->data['log'][] = $entry;
        // Keep the in-state tail small; the full log lives in the file.
        if (count($this->data['log']) > 40) {
            $this->data['log'] = array_slice($this->data['log'], -40);
        }
        @file_put_contents($this->dir() . '/migrate.log', $entry . "\n", FILE_APPEND);
        $this->save();
    }

    public function setBundleStats(int $bytes, int $files): void
    {
        $this->data['bundle'] = ['bytes' => $bytes, 'files' => $files];
        $this->save();
    }

    public function fail(string $message): void
    {
        $this->data['status'] = self::FAILED;
        $this->data['error']  = $message;
        if ($this->data['phase'] && isset($this->data['phases'][$this->data['phase']])) {
            $this->data['phases'][$this->data['phase']]['status'] = self::FAILED;
            $this->data['phases'][$this->data['phase']]['detail'] = $message;
        }
        $this->save();
    }

    public function finish(string $message = 'Migration completed.'): void
    {
        $this->data['status'] = self::DONE;
        $this->data['progress'] = 100;
        $this->log($message);
    }

    public function save(): void
    {
        $this->data['updated'] = time();
        @file_put_contents(
            $this->dir() . '/job.json',
            json_encode($this->data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
            LOCK_EX
        );
    }

    // ── Accessors ─────────────────────────────────────────────────────

    public function id(): string        { return (string) $this->data['id']; }
    public function status(): string    { return (string) $this->data['status']; }
    public function toArray(): array    { return $this->data; }

    // ── Ids ───────────────────────────────────────────────────────────

    private static function newId(): string
    {
        return gmdate('Ymd-His') . '-' . substr(bin2hex(random_bytes(4)), 0, 6);
    }

    private static function safeId(string $id): string
    {
        return preg_match('/^[A-Za-z0-9\-]{1,40}$/', $id) ? $id : '';
    }
}
