<?php

declare(strict_types=1);

namespace PpPhp\Core\Migrate;

/**
 * Talks to the migration agent running on the destination over HTTPS.
 *
 * Every call is a JSON POST carrying the raw token; the agent verifies it against
 * the stored hash. The client never throws — a transport-level failure is returned
 * as `['ok' => false, 'error' => …]` so the runner can record it against the job
 * and stop cleanly instead of crashing mid-migration.
 */
final class AgentClient
{
    /**
     * @param string $endpoint full URL to pp-migrate-agent.php on the destination
     * @param string $token    the raw one-time token (from AgentBuilder::install)
     */
    public function __construct(
        private string $endpoint,
        private string $token,
        private int $timeout = 120,
    ) {}

    /** Build the endpoint from a base URL + job id. */
    public static function endpointFor(string $baseUrl, string $jobId): string
    {
        return rtrim($baseUrl, '/') . '/' . AgentBuilder::remoteSubdir($jobId) . '/' . AgentBuilder::AGENT_FILE;
    }

    public function ping(): array                 { return $this->call('ping'); }
    public function verify(): array               { return $this->call('verify'); }

    /** @param list<array<string,mixed>> $databases */
    public function provision(array $databases, bool $dryRun = false): array
    {
        return $this->call('provision', ['databases' => $databases, 'dryRun' => $dryRun]);
    }

    /** @param list<array<string,mixed>> $databases each with file+creds+dbname */
    public function restore(array $databases, bool $confirm, bool $dryRun = false): array
    {
        return $this->call('restore', ['databases' => $databases, 'confirm' => $confirm, 'dryRun' => $dryRun]);
    }

    public function unpack(string $targetRoot, bool $confirm, bool $dryRun = false): array
    {
        return $this->call('unpack', ['targetRoot' => $targetRoot, 'confirm' => $confirm, 'dryRun' => $dryRun]);
    }

    public function rewrite(string $targetRoot, array $db, string $baseUrl, bool $confirm, bool $dryRun = false): array
    {
        return $this->call('rewrite', ['targetRoot' => $targetRoot, 'db' => $db, 'baseUrl' => $baseUrl,
                                        'confirm' => $confirm, 'dryRun' => $dryRun]);
    }

    public function cleanup(): array              { return $this->call('cleanup'); }

    /** Perform one agent call. Never throws. */
    private function call(string $action, array $params = []): array
    {
        if (!function_exists('curl_init')) {
            return ['ok' => false, 'error' => 'curl is not available on the source server.'];
        }
        $payload = json_encode($params + ['action' => $action, 'token' => $this->token], JSON_UNESCAPED_SLASHES);
        $ch = curl_init($this->endpoint);
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $payload,
            CURLOPT_HTTPHEADER     => ['Content-Type: application/json', 'X-PP-Token: ' . $this->token],
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => $this->timeout,
            CURLOPT_CONNECTTIMEOUT => 20,
            // The destination may still be serving a self-signed / provisional
            // certificate during migration; verification is the operator's call.
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => 0,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS      => 3,
        ]);
        $body = curl_exec($ch);
        $errno = curl_errno($ch);
        $err  = curl_error($ch);
        $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        // curl_close() is a no-op since PHP 8.0 (deprecated in 8.5); the handle is
        // freed when it goes out of scope.
        unset($ch);

        if ($errno !== 0 || $body === false) {
            return ['ok' => false, 'error' => 'Could not reach the agent: ' . $err, 'httpCode' => $code];
        }
        $data = json_decode((string) $body, true);
        if (!is_array($data)) {
            return ['ok' => false, 'error' => 'Agent returned a non-JSON response (HTTP ' . $code . ').',
                    'httpCode' => $code, 'raw' => substr((string) $body, 0, 300)];
        }
        $data['httpCode'] = $code;
        return $data;
    }
}
