<?php

declare(strict_types=1);

/**
 * APO-197 Card 1 — AI Integration value object + repository.
 *
 * Represents one operator-managed AI integration: a labelled, scope-gated,
 * bcrypt-hashed bearer-token identity that can call the action API.
 *
 * Token format: `myr_ai_{base64url-32-bytes}` (50 chars total). The raw
 * value is shown ONCE on creation; `token_hash` stores the bcrypt cost-12
 * digest; `token_prefix` is the first 8 chars of the base64url portion for
 * fast O(1) lookup before the bcrypt verify.
 */
final class MyronAiIntegration
{
    /**
     * @param list<string> $scopes
     * @param array<string, mixed>|null $rateLimitConfig
     */
    public function __construct(
        public readonly string  $id,
        public readonly string  $label,
        public readonly string  $tokenPrefix,
        public readonly array   $scopes,
        public readonly string  $siteScope,
        public readonly string  $createdAt,
        public readonly string  $createdByUserId,
        public readonly ?string $lastUsedAt,
        public readonly ?string $revokedAt,
        public readonly int     $requestCount,
        public readonly ?array  $rateLimitConfig,
    ) {
    }

    public function isRevoked(): bool
    {
        return $this->revokedAt !== null && $this->revokedAt !== '';
    }

    /**
     * @return array<string, mixed>
     */
    public function toPublicArray(): array
    {
        return [
            'id'              => $this->id,
            'label'           => $this->label,
            'tokenPrefix'     => $this->tokenPrefix,
            'scopes'          => $this->scopes,
            'siteScope'       => $this->siteScope,
            'createdAt'       => $this->createdAt,
            'createdByUserId' => $this->createdByUserId,
            'lastUsedAt'      => $this->lastUsedAt,
            'revokedAt'       => $this->revokedAt,
            'requestCount'    => $this->requestCount,
            'rateLimitConfig' => $this->rateLimitConfig,
        ];
    }
}

/**
 * Token generator + verifier helpers.
 *
 * Token format: `myr_ai_<base64url-32-bytes>`. The literal `myr_ai_` prefix
 * marks the token in operator-facing copy; the base64url portion is what's
 * indexed and bcrypt-verified.
 */
final class MyronAiTokenCodec
{
    public const TOKEN_PREFIX_LITERAL = 'myr_ai_';

    public const PREFIX_LENGTH = 8; // chars of the base64url portion stored in token_prefix

    /**
     * @return array{token: string, prefix: string, hash: string}
     */
    public static function generate(): array
    {
        $bytes = random_bytes(32);
        $base64 = rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
        $token = self::TOKEN_PREFIX_LITERAL . $base64;
        $prefix = substr($base64, 0, self::PREFIX_LENGTH);
        $hash = password_hash($token, PASSWORD_BCRYPT, ['cost' => 12]);
        return ['token' => $token, 'prefix' => $prefix, 'hash' => $hash];
    }

    public static function prefixFromToken(string $token): ?string
    {
        if (!str_starts_with($token, self::TOKEN_PREFIX_LITERAL)) {
            return null;
        }
        $base64 = substr($token, strlen(self::TOKEN_PREFIX_LITERAL));
        if (strlen($base64) < self::PREFIX_LENGTH) {
            return null;
        }
        return substr($base64, 0, self::PREFIX_LENGTH);
    }

    public static function verify(string $token, string $hash): bool
    {
        if (!str_starts_with($token, self::TOKEN_PREFIX_LITERAL)) {
            return false;
        }
        return password_verify($token, $hash);
    }
}

/**
 * Repository for `ai_integrations`. Append-on-create; mutate
 * (update label/scopes/rate-limit, mark revoked, bump last-used) by id.
 */
final class MyronAiIntegrationRepository
{
    public function __construct(private readonly PDO $pdo)
    {
    }

    /**
     * @param list<string> $scopes
     * @param array<string, mixed>|null $rateLimitConfig
     * @return array{integration: MyronAiIntegration, rawToken: string}
     */
    public function create(
        string $label,
        array $scopes,
        string $createdByUserId,
        ?array $rateLimitConfig = null,
        string $siteScope = ''
    ): array {
        $label = trim($label);
        if ($label === '') {
            throw new InvalidArgumentException('label is required.');
        }
        if ($scopes === []) {
            throw new InvalidArgumentException('at least one scope must be granted.');
        }
        $id = myronGenerateId('aii');
        $token = MyronAiTokenCodec::generate();
        $now = myronNow();

        $stmt = $this->pdo->prepare(
            'INSERT INTO ai_integrations (
                id, label, token_hash, token_prefix, scopes_json, site_scope,
                created_at, created_by_user_id, last_used_at, revoked_at,
                request_count, rate_limit_config
            ) VALUES (
                :id, :label, :token_hash, :token_prefix, :scopes_json, :site_scope,
                :created_at, :created_by_user_id, NULL, NULL, 0, :rate_limit_config
            )'
        );
        $stmt->execute([
            ':id'                => $id,
            ':label'             => $label,
            ':token_hash'        => $token['hash'],
            ':token_prefix'      => $token['prefix'],
            ':scopes_json'       => json_encode(array_values($scopes), JSON_UNESCAPED_SLASHES),
            ':site_scope'        => trim($siteScope),
            ':created_at'        => $now,
            ':created_by_user_id' => trim($createdByUserId),
            ':rate_limit_config' => $rateLimitConfig === null ? null : json_encode($rateLimitConfig, JSON_UNESCAPED_SLASHES),
        ]);

        $integration = new MyronAiIntegration(
            id:              $id,
            label:           $label,
            tokenPrefix:     $token['prefix'],
            scopes:          array_values($scopes),
            siteScope:       trim($siteScope),
            createdAt:       $now,
            createdByUserId: trim($createdByUserId),
            lastUsedAt:      null,
            revokedAt:       null,
            requestCount:    0,
            rateLimitConfig: $rateLimitConfig,
        );

        return ['integration' => $integration, 'rawToken' => $token['token']];
    }

    /**
     * @return list<MyronAiIntegration>
     */
    public function listAll(): array
    {
        $rows = $this->pdo->query('SELECT * FROM ai_integrations ORDER BY created_at DESC')->fetchAll(PDO::FETCH_ASSOC);
        return array_map([$this, 'hydrate'], $rows ?: []);
    }

    public function findById(string $id): ?MyronAiIntegration
    {
        $stmt = $this->pdo->prepare('SELECT * FROM ai_integrations WHERE id = :id LIMIT 1');
        $stmt->execute([':id' => trim($id)]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return is_array($row) ? $this->hydrate($row) : null;
    }

    /**
     * @return list<MyronAiIntegration>
     */
    public function findByPrefix(string $prefix): array
    {
        $stmt = $this->pdo->prepare('SELECT * FROM ai_integrations WHERE token_prefix = :prefix AND revoked_at IS NULL');
        $stmt->execute([':prefix' => trim($prefix)]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
        return array_map([$this, 'hydrate'], $rows ?: []);
    }

    public function getTokenHash(string $id): ?string
    {
        $stmt = $this->pdo->prepare('SELECT token_hash FROM ai_integrations WHERE id = :id');
        $stmt->execute([':id' => trim($id)]);
        $hash = $stmt->fetchColumn();
        return is_string($hash) ? $hash : null;
    }

    /**
     * @param list<string> $scopes
     */
    public function updateMetadata(string $id, string $label, array $scopes, ?array $rateLimitConfig): void
    {
        $stmt = $this->pdo->prepare(
            'UPDATE ai_integrations SET label = :label, scopes_json = :scopes_json, rate_limit_config = :rate_limit_config WHERE id = :id'
        );
        $stmt->execute([
            ':id'                => trim($id),
            ':label'             => trim($label),
            ':scopes_json'       => json_encode(array_values($scopes), JSON_UNESCAPED_SLASHES),
            ':rate_limit_config' => $rateLimitConfig === null ? null : json_encode($rateLimitConfig, JSON_UNESCAPED_SLASHES),
        ]);
    }

    public function revoke(string $id): void
    {
        $stmt = $this->pdo->prepare('UPDATE ai_integrations SET revoked_at = :revoked_at WHERE id = :id AND revoked_at IS NULL');
        $stmt->execute([':id' => trim($id), ':revoked_at' => myronNow()]);
    }

    /**
     * Rotate the token: generate a new value, replace hash + prefix.
     *
     * @return array{integration: MyronAiIntegration, rawToken: string}
     */
    public function rotateToken(string $id): array
    {
        $current = $this->findById($id);
        if ($current === null) {
            throw new InvalidArgumentException('Integration not found: ' . $id);
        }
        if ($current->isRevoked()) {
            throw new RuntimeException('Cannot rotate a revoked integration; create a new one.');
        }
        $token = MyronAiTokenCodec::generate();
        $stmt = $this->pdo->prepare(
            'UPDATE ai_integrations SET token_hash = :token_hash, token_prefix = :token_prefix WHERE id = :id'
        );
        $stmt->execute([
            ':id'           => $id,
            ':token_hash'   => $token['hash'],
            ':token_prefix' => $token['prefix'],
        ]);
        $refreshed = $this->findById($id);
        if ($refreshed === null) {
            throw new RuntimeException('Integration vanished during rotation: ' . $id);
        }
        return ['integration' => $refreshed, 'rawToken' => $token['token']];
    }

    public function recordUse(string $id): void
    {
        $stmt = $this->pdo->prepare(
            'UPDATE ai_integrations SET last_used_at = :last_used_at, request_count = request_count + 1 WHERE id = :id'
        );
        $stmt->execute([':id' => trim($id), ':last_used_at' => myronNow()]);
    }

    /**
     * @param array<string, mixed> $row
     */
    private function hydrate(array $row): MyronAiIntegration
    {
        $scopes = (array) (json_decode((string) ($row['scopes_json'] ?? '[]'), true) ?: []);
        $rateRaw = (string) ($row['rate_limit_config'] ?? '');
        $rateLimitConfig = $rateRaw !== '' ? (array) (json_decode($rateRaw, true) ?: []) : null;
        return new MyronAiIntegration(
            id:              (string) ($row['id'] ?? ''),
            label:           (string) ($row['label'] ?? ''),
            tokenPrefix:     (string) ($row['token_prefix'] ?? ''),
            scopes:          array_values(array_map('strval', $scopes)),
            siteScope:       (string) ($row['site_scope'] ?? ''),
            createdAt:       (string) ($row['created_at'] ?? ''),
            createdByUserId: (string) ($row['created_by_user_id'] ?? ''),
            lastUsedAt:      isset($row['last_used_at']) && $row['last_used_at'] !== null ? (string) $row['last_used_at'] : null,
            revokedAt:       isset($row['revoked_at']) && $row['revoked_at'] !== null ? (string) $row['revoked_at'] : null,
            requestCount:    (int) ($row['request_count'] ?? 0),
            rateLimitConfig: $rateLimitConfig,
        );
    }
}
