<?php

declare(strict_types=1);

final class MyronUpdateAuditRepository
{
    private const LEGACY_MYSQL_TEXT_SAFE_BYTES = 60000;
    private const AUDIT_SAMPLE_LIMIT = 25;

    public function __construct(private readonly PDO $pdo)
    {
    }

    /**
     * @param array<string, mixed> $manifest
     */
    public function begin(string $actorUserId, string $packageHash, array $manifest): string
    {
        $id = myronGenerateId('upd');
        $now = myronNow();
        $statement = $this->pdo->prepare(
            'INSERT INTO application_updates (
                id,
                package_hash,
                package_id,
                target_version,
                actor_user_id,
                status,
                manifest_json,
                details_json,
                error_message,
                created_at,
                updated_at
            ) VALUES (
                :id,
                :package_hash,
                :package_id,
                :target_version,
                :actor_user_id,
                :status,
                :manifest_json,
                :details_json,
                :error_message,
                :created_at,
                :updated_at
            )'
        );
        $statement->execute([
            ':id' => $id,
            ':package_hash' => $packageHash,
            ':package_id' => (string) ($manifest['packageId'] ?? ''),
            ':target_version' => (string) ($manifest['targetVersion'] ?? ''),
            ':actor_user_id' => $actorUserId,
            ':status' => 'started',
            ':manifest_json' => $this->encodeAuditJson($this->summarizeManifest($manifest)),
            ':details_json' => '{}',
            ':error_message' => '',
            ':created_at' => $now,
            ':updated_at' => $now,
        ]);

        return $id;
    }

    /**
     * @param list<string> $errors
     */
    public function recordValidationFailure(string $actorUserId, string $packageHash, array $errors): string
    {
        $id = myronGenerateId('upd');
        $now = myronNow();
        $details = ['errors' => array_values($errors)];
        $statement = $this->pdo->prepare(
            'INSERT INTO application_updates (
                id,
                package_hash,
                package_id,
                target_version,
                actor_user_id,
                status,
                manifest_json,
                details_json,
                error_message,
                created_at,
                updated_at
            ) VALUES (
                :id,
                :package_hash,
                :package_id,
                :target_version,
                :actor_user_id,
                :status,
                :manifest_json,
                :details_json,
                :error_message,
                :created_at,
                :updated_at
            )'
        );
        $statement->execute([
            ':id' => $id,
            ':package_hash' => $packageHash,
            ':package_id' => '',
            ':target_version' => '',
            ':actor_user_id' => $actorUserId,
            ':status' => 'failed',
            ':manifest_json' => '{}',
            ':details_json' => $this->encodeAuditJson($details),
            ':error_message' => $this->limitText(implode(' ', $errors)),
            ':created_at' => $now,
            ':updated_at' => $now,
        ]);

        return $id;
    }

    /**
     * @param array<string, mixed> $details
     */
    public function finish(string $id, string $status, array $details = [], string $errorMessage = ''): void
    {
        if (!in_array($status, ['succeeded', 'failed'], true)) {
            throw new InvalidArgumentException('Update audit status must be succeeded or failed.');
        }

        $statement = $this->pdo->prepare(
            'UPDATE application_updates
             SET status = :status,
                 details_json = :details_json,
                 error_message = :error_message,
                 updated_at = :updated_at
             WHERE id = :id'
        );
        $statement->execute([
            ':status' => $status,
            ':details_json' => $this->encodeAuditJson($this->summarizeDetails($details)),
            ':error_message' => $this->limitText($errorMessage),
            ':updated_at' => myronNow(),
            ':id' => $id,
        ]);
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listRecent(int $limit = 5): array
    {
        $limit = max(1, min(25, $limit));
        $statement = $this->pdo->query(
            'SELECT id, package_hash, package_id, target_version, actor_user_id, status, details_json, error_message, created_at, updated_at
             FROM application_updates
             ORDER BY updated_at DESC, created_at DESC
             LIMIT ' . $limit
        );

        $rows = $statement ? $statement->fetchAll() : [];
        return array_values(array_filter($rows, 'is_array'));
    }

    /**
     * @param array<string, mixed> $manifest
     * @return array<string, mixed>
     */
    private function summarizeManifest(array $manifest): array
    {
        $files = is_array($manifest['files'] ?? null)
            ? array_values($manifest['files'])
            : (is_array($manifest['operations'] ?? null) ? array_values($manifest['operations']) : []);
        $operationCounts = [];
        $sampledPaths = [];

        foreach ($files as $index => $file) {
            if (!is_array($file)) {
                continue;
            }

            $operation = trim((string) ($file['operation'] ?? 'replace'));
            $operationCounts[$operation] = ($operationCounts[$operation] ?? 0) + 1;

            if ($index < self::AUDIT_SAMPLE_LIMIT) {
                $sampledPaths[] = trim((string) ($file['path'] ?? ''));
            }
        }

        ksort($operationCounts);
        $manifestJson = json_encode($manifest, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);

        return [
            'auditFormat' => 'manifest-summary-v1',
            'schemaVersion' => (int) ($manifest['schemaVersion'] ?? 0),
            'packageType' => (string) ($manifest['packageType'] ?? ''),
            'packageId' => (string) ($manifest['packageId'] ?? ''),
            'targetVersion' => (string) ($manifest['targetVersion'] ?? ''),
            'targetRoot' => (string) ($manifest['targetRoot'] ?? ''),
            'envelopeRef' => (string) ($manifest['envelopeRef'] ?? ''),
            'sourceVersionConstraints' => array_values((array) ($manifest['sourceVersionConstraints'] ?? [])),
            'runMigrations' => (bool) ($manifest['runMigrations'] ?? true),
            'reseedRuntimeDefinitions' => (bool) ($manifest['reseedRuntimeDefinitions'] ?? true),
            'fileOperationCount' => count($files),
            'operationCounts' => $operationCounts,
            'manifestSha256' => hash('sha256', $manifestJson),
            'manifestBytes' => strlen($manifestJson),
            'sampledPaths' => array_values(array_filter($sampledPaths, static fn (string $path): bool => $path !== '')),
            'sampleLimit' => self::AUDIT_SAMPLE_LIMIT,
        ];
    }

    /**
     * @param array<string, mixed> $details
     * @return array<string, mixed>
     */
    private function summarizeDetails(array $details): array
    {
        $summary = $details;

        if (is_array($details['appliedOperations'] ?? null)) {
            $summary['appliedOperationsSummary'] = $this->summarizeOperationList($details['appliedOperations']);
            unset($summary['appliedOperations']);
        }

        if (is_array($details['restoredFiles'] ?? null)) {
            $restoredFiles = array_values(array_filter(array_map('strval', $details['restoredFiles'])));
            $summary['restoredFilesSummary'] = [
                'count' => count($restoredFiles),
                'sampledPaths' => array_slice($restoredFiles, 0, self::AUDIT_SAMPLE_LIMIT),
                'sampleLimit' => self::AUDIT_SAMPLE_LIMIT,
            ];
            unset($summary['restoredFiles']);
        }

        return $summary;
    }

    /**
     * @param array<int, mixed> $operations
     * @return array<string, mixed>
     */
    private function summarizeOperationList(array $operations): array
    {
        $operationCounts = [];
        $sampledPaths = [];

        foreach (array_values($operations) as $index => $operation) {
            if (!is_array($operation)) {
                continue;
            }

            $operationName = trim((string) ($operation['operation'] ?? 'replace'));
            $operationCounts[$operationName] = ($operationCounts[$operationName] ?? 0) + 1;

            if ($index < self::AUDIT_SAMPLE_LIMIT) {
                $sampledPaths[] = trim((string) ($operation['path'] ?? ''));
            }
        }

        ksort($operationCounts);

        return [
            'count' => count($operations),
            'operationCounts' => $operationCounts,
            'sampledPaths' => array_values(array_filter($sampledPaths, static fn (string $path): bool => $path !== '')),
            'sampleLimit' => self::AUDIT_SAMPLE_LIMIT,
        ];
    }

    /**
     * @param array<string, mixed> $payload
     */
    private function encodeAuditJson(array $payload): string
    {
        $encoded = json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
        if (strlen($encoded) <= self::LEGACY_MYSQL_TEXT_SAFE_BYTES) {
            return $encoded;
        }

        $fallback = [
            'auditFormat' => 'audit-summary-truncated-v1',
            'truncated' => true,
            'payloadSha256' => hash('sha256', $encoded),
            'payloadBytes' => strlen($encoded),
            'message' => 'Audit payload exceeded legacy MySQL TEXT-safe storage and was summarized.',
        ];

        return json_encode($fallback, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
    }

    private function limitText(string $value): string
    {
        if (strlen($value) <= self::LEGACY_MYSQL_TEXT_SAFE_BYTES) {
            return $value;
        }

        return substr($value, 0, self::LEGACY_MYSQL_TEXT_SAFE_BYTES - 120)
            . ' ... [truncated; sha256=' . hash('sha256', $value) . '; bytes=' . strlen($value) . ']';
    }
}
