<?php

declare(strict_types=1);

final class MyronBundleActivationRepository
{
    public function __construct(
        private readonly PDO $pdo,
        private readonly ?MyronSiteTypeRegistryRepository $siteTypes = null,
    ) {
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listBundleState(): array
    {
        $statement = $this->pdo->query(
            'SELECT *
             FROM bundle_activation_state
             ORDER BY is_primary DESC, bundle_id ASC'
        );
        $rows = $statement ? $statement->fetchAll() : [];

        if (!is_array($rows)) {
            return [];
        }

        return array_map(fn (array $row): array => $this->hydrateState($row), $rows);
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listActiveBundles(): array
    {
        return array_values(array_filter(
            $this->listBundleState(),
            static fn (array $state): bool => ($state['is_enabled'] ?? false) === true
        ));
    }

    public function findBundleState(string $bundleId): ?array
    {
        $statement = $this->pdo->prepare('SELECT * FROM bundle_activation_state WHERE bundle_id = :bundle_id LIMIT 1');
        $statement->execute([':bundle_id' => trim($bundleId)]);
        $row = $statement->fetch();

        return is_array($row) ? $this->hydrateState($row) : null;
    }

    public function getPrimaryBundleId(): ?string
    {
        $statement = $this->pdo->query(
            'SELECT bundle_id
             FROM bundle_activation_state
             WHERE is_primary = 1
             LIMIT 1'
        );
        $bundleId = $statement ? $statement->fetchColumn() : false;

        return $bundleId === false ? null : (string) $bundleId;
    }

    public function isBundleActive(string $bundleId): bool
    {
        return $this->findBundleState($bundleId) !== null;
    }

    /**
     * @param list<array<string, mixed>> $templates
     * @return array<string, array{available: bool, status: string, reason: string}>
     */
    public function templateAvailabilityIndex(array $templates): array
    {
        $activeState = $this->listActiveBundles();
        $activeIds = array_fill_keys(array_map(static fn (array $state): string => (string) ($state['bundle_id'] ?? ''), $activeState), true);
        $activeSiteTypes = [];
        $defaultTemplateIds = [];
        $optionalTemplateIds = [];

        if ($this->siteTypes !== null) {
            foreach (array_keys($activeIds) as $bundleId) {
                $siteType = $this->siteTypes->findSiteTypeById((string) $bundleId);
                if (!is_array($siteType)) {
                    continue;
                }
                $activeSiteTypes[] = strtoupper((string) ($siteType['code'] ?? ''));
                foreach (($siteType['defaultTemplates'] ?? []) as $templateId) {
                    $defaultTemplateIds[(string) $templateId] = (string) $bundleId;
                }
                foreach (($siteType['optionalTemplates'] ?? []) as $templateId) {
                    $optionalTemplateIds[(string) $templateId] = (string) $bundleId;
                }
            }
        }

        $index = [];
        foreach ($templates as $template) {
            $templateId = (string) ($template['id'] ?? '');
            if ($templateId === '') {
                continue;
            }
            $registry = is_array($template['_registry'] ?? null) ? $template['_registry'] : [];
            $bundleId = (string) ($registry['bundleId'] ?? $template['bundleId'] ?? '');
            $siteType = strtoupper((string) ($registry['siteType'] ?? $template['siteType'] ?? ''));
            $appliesTo = array_map('strtoupper', array_map('strval', is_array($template['appliesToSiteTypes'] ?? null) ? $template['appliesToSiteTypes'] : []));

            if (isset($defaultTemplateIds[$templateId])) {
                $index[$templateId] = ['available' => true, 'status' => 'Default', 'reason' => 'Default for active bundle ' . $defaultTemplateIds[$templateId]];
                continue;
            }

            if (isset($optionalTemplateIds[$templateId])) {
                $index[$templateId] = ['available' => true, 'status' => 'Optional', 'reason' => 'Optional for active bundle ' . $optionalTemplateIds[$templateId]];
                continue;
            }

            if ($bundleId === 'site-type.universal' || $siteType === 'UNIVERSAL') {
                $matchesActiveSiteType = $appliesTo === [] || array_intersect($appliesTo, $activeSiteTypes) !== [];
                $index[$templateId] = [
                    'available' => $matchesActiveSiteType,
                    'status' => $matchesActiveSiteType ? 'Universal' : 'Inactive universal',
                    'reason' => $matchesActiveSiteType ? 'Universal template applies to the active site type' : 'Universal template does not apply to the active site type',
                ];
                continue;
            }

            if (isset($activeIds[$bundleId])) {
                $index[$templateId] = ['available' => true, 'status' => 'Active bundle', 'reason' => 'Template belongs to active bundle ' . $bundleId];
                continue;
            }

            $index[$templateId] = ['available' => false, 'status' => 'Inactive bundle', 'reason' => 'Template bundle is not active or optional'];
        }

        return $index;
    }

    /**
     * @param list<string> $activeBundleIds
     * @return list<array<string, mixed>>
     */
    public function replaceState(?string $primaryBundleId, array $activeBundleIds): array
    {
        $primary = $primaryBundleId === null ? null : trim($primaryBundleId);
        $active = $this->normalizeIds($activeBundleIds);

        if ($primary !== null && $primary !== '' && !in_array($primary, $active, true)) {
            $active[] = $primary;
        }

        sort($active, SORT_STRING);

        $this->pdo->beginTransaction();

        try {
            $this->pdo->exec('DELETE FROM bundle_activation_state');

            foreach ($active as $bundleId) {
                $this->saveBundleState($bundleId, true, $primary !== null && $bundleId === $primary);
            }

            if ($this->pdo->inTransaction()) {
                $this->pdo->commit();
            }
        } catch (Throwable $exception) {
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }

            throw $exception;
        }

        return $this->listBundleState();
    }

    /**
     * @param list<string> $activeBundleIds
     * @return list<array<string, mixed>>
     */
    public function replaceStateWithAudit(?string $primaryBundleId, array $activeBundleIds, string $actorId, string $context): array
    {
        $actor = trim($actorId);
        if ($actor === '') {
            throw new InvalidArgumentException('An actor id is required for bundle activation changes.');
        }

        $normalizedContext = trim($context) ?: 'bundle-activation';
        $primary = $primaryBundleId === null ? null : trim($primaryBundleId);
        $active = $this->normalizeIds($activeBundleIds);

        $this->assertValidActivation($primary, $active);
        $before = $this->listBundleState();
        $after = $this->replaceState($primary, $active);
        $this->recordAudit($actor, $normalizedContext, $primary, $before, $after);

        return $after;
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function clearStateWithAudit(string $actorId, string $context = 'site-reset'): array
    {
        $actor = trim($actorId);
        if ($actor === '') {
            throw new InvalidArgumentException('An actor id is required for bundle activation changes.');
        }

        $normalizedContext = trim($context) ?: 'site-reset';
        $before = $this->listBundleState();
        $after = $this->replaceState(null, []);
        $this->recordAudit($actor, $normalizedContext, null, $before, $after);

        return $after;
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listAuditEvents(int $limit = 10): array
    {
        $boundedLimit = max(1, min(100, $limit));
        $statement = $this->pdo->query(
            'SELECT *
             FROM bundle_activation_audit
             ORDER BY created_at DESC, id DESC
             LIMIT ' . $boundedLimit
        );
        $rows = $statement ? $statement->fetchAll() : [];

        if (!is_array($rows)) {
            return [];
        }

        return array_map(fn (array $row): array => $this->hydrateAuditEvent($row), $rows);
    }

    /**
     * @return array<string, mixed>|null
     */
    public function saveBundleState(string $bundleId, bool $isEnabled, bool $isPrimary = false): ?array
    {
        $normalizedBundleId = trim($bundleId);
        if ($normalizedBundleId === '') {
            throw new InvalidArgumentException('Bundle id is required for bundle state persistence.');
        }

        if ($isPrimary) {
            $statement = $this->pdo->prepare('UPDATE bundle_activation_state SET is_primary = 0 WHERE bundle_id != :bundle_id');
            $statement->execute([':bundle_id' => $normalizedBundleId]);
        }

        if (!$isEnabled) {
            $statement = $this->pdo->prepare('DELETE FROM bundle_activation_state WHERE bundle_id = :bundle_id');
            $statement->execute([':bundle_id' => $normalizedBundleId]);
            return null;
        }

        $existing = $this->findBundleState($normalizedBundleId);
        $now = myronNow();

        if ($existing === null) {
            $statement = $this->pdo->prepare(
                'INSERT INTO bundle_activation_state (
                    bundle_id,
                    is_enabled,
                    is_primary,
                    activated_at,
                    updated_at
                 ) VALUES (
                    :bundle_id,
                    :is_enabled,
                    :is_primary,
                    :activated_at,
                    :updated_at
                 )'
            );
            $statement->execute([
                ':bundle_id' => $normalizedBundleId,
                ':is_enabled' => 1,
                ':is_primary' => $isPrimary ? 1 : 0,
                ':activated_at' => $now,
                ':updated_at' => $now,
            ]);
        } else {
            $statement = $this->pdo->prepare(
                'UPDATE bundle_activation_state
                 SET is_enabled = :is_enabled,
                     is_primary = :is_primary,
                     updated_at = :updated_at
                 WHERE bundle_id = :bundle_id'
            );
            $statement->execute([
                ':is_enabled' => 1,
                ':is_primary' => $isPrimary ? 1 : 0,
                ':updated_at' => $now,
                ':bundle_id' => $normalizedBundleId,
            ]);
        }

        return $this->findBundleState($normalizedBundleId);
    }

    /**
     * @param array<string, mixed> $row
     * @return array<string, mixed>
     */
    private function hydrateState(array $row): array
    {
        return [
            'bundle_id' => (string) ($row['bundle_id'] ?? ''),
            'is_enabled' => (int) ($row['is_enabled'] ?? 0) === 1,
            'is_primary' => (int) ($row['is_primary'] ?? 0) === 1,
            'activated_at' => (string) ($row['activated_at'] ?? ''),
            'updated_at' => (string) ($row['updated_at'] ?? ''),
        ];
    }

    /**
     * @param list<string> $activeBundleIds
     */
    private function assertValidActivation(?string $primaryBundleId, array $activeBundleIds): void
    {
        if ($this->siteTypes === null) {
            return;
        }

        $active = array_fill_keys($activeBundleIds, true);
        if ($primaryBundleId !== null && $primaryBundleId !== '') {
            $active[$primaryBundleId] = true;
        }

        if ($primaryBundleId === null || $primaryBundleId === '') {
            throw new InvalidArgumentException('A primary bundle is required.');
        }

        $primary = $this->siteTypes->findSiteTypeById($primaryBundleId);
        if (!is_array($primary)) {
            throw new InvalidArgumentException('Primary bundle `' . $primaryBundleId . '` does not exist.');
        }
        if ((string) ($primary['bundleType'] ?? '') !== 'primary') {
            throw new InvalidArgumentException('Primary bundle `' . $primaryBundleId . '` must be a primary site type.');
        }

        foreach (array_keys($active) as $bundleId) {
            $record = $this->siteTypes->findSiteTypeById((string) $bundleId);
            if (!is_array($record)) {
                throw new InvalidArgumentException('Bundle `' . (string) $bundleId . '` does not exist.');
            }

            $bundleType = (string) ($record['bundleType'] ?? '');
            if ($bundleType !== 'subtype') {
                continue;
            }

            $parentBundles = $this->parentBundleIds($record);
            if ($parentBundles === [] || count(array_intersect_key(array_fill_keys($parentBundles, true), $active)) === 0) {
                throw new InvalidArgumentException('Subtype bundle `' . (string) $bundleId . '` requires active parent bundle `' . implode('` or `', $parentBundles) . '`.');
            }
        }
    }

    /**
     * @param array<string, mixed> $record
     * @return list<string>
     */
    private function parentBundleIds(array $record): array
    {
        $parents = [];
        foreach (is_array($record['parentBundles'] ?? null) ? $record['parentBundles'] : [] as $parentBundle) {
            $normalized = trim((string) $parentBundle);
            if ($normalized !== '') {
                $parents[$normalized] = true;
            }
        }

        $legacyParent = $record['parentBundle'] ?? null;
        if (is_array($legacyParent)) {
            foreach ($legacyParent as $parentBundle) {
                $normalized = trim((string) $parentBundle);
                if ($normalized !== '') {
                    $parents[$normalized] = true;
                }
            }
        } else {
            $normalized = trim((string) $legacyParent);
            if ($normalized !== '') {
                $parents[$normalized] = true;
            }
        }

        return array_keys($parents);
    }

    /**
     * @param list<array<string, mixed>> $before
     * @param list<array<string, mixed>> $after
     */
    private function recordAudit(string $actorId, string $context, ?string $primaryBundleId, array $before, array $after): void
    {
        $statement = $this->pdo->prepare(
            'INSERT INTO bundle_activation_audit (
                id,
                actor_id,
                context,
                primary_bundle_id,
                before_json,
                after_json,
                created_at
             ) VALUES (
                :id,
                :actor_id,
                :context,
                :primary_bundle_id,
                :before_json,
                :after_json,
                :created_at
             )'
        );
        $statement->execute([
            ':id' => myronGenerateId('baa'),
            ':actor_id' => $actorId,
            ':context' => $context,
            ':primary_bundle_id' => $primaryBundleId,
            ':before_json' => json_encode($before, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES),
            ':after_json' => json_encode($after, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES),
            ':created_at' => myronNow(),
        ]);
    }

    /**
     * @param array<string, mixed> $row
     * @return array<string, mixed>
     */
    private function hydrateAuditEvent(array $row): array
    {
        $before = json_decode((string) ($row['before_json'] ?? '[]'), true, 512, JSON_THROW_ON_ERROR);
        $after = json_decode((string) ($row['after_json'] ?? '[]'), true, 512, JSON_THROW_ON_ERROR);

        return [
            'id' => (string) ($row['id'] ?? ''),
            'actor_id' => (string) ($row['actor_id'] ?? ''),
            'context' => (string) ($row['context'] ?? ''),
            'primary_bundle_id' => (string) ($row['primary_bundle_id'] ?? ''),
            'before' => is_array($before) ? $before : [],
            'after' => is_array($after) ? $after : [],
            'created_at' => (string) ($row['created_at'] ?? ''),
        ];
    }

    /**
     * @param list<string> $ids
     * @return list<string>
     */
    private function normalizeIds(array $ids): array
    {
        $normalized = [];
        foreach ($ids as $id) {
            $value = trim((string) $id);
            if ($value === '') {
                continue;
            }

            $normalized[$value] = true;
        }

        return array_keys($normalized);
    }
}
