<?php

declare(strict_types=1);

/**
 * APO-193 Card 3 — Compatibility document composer.
 *
 * Two modes:
 *
 * - default(siteTypeKey): static per-site-type description of what activates
 *   out-of-the-box for a fresh install of that site type. No DB reads required
 *   beyond the site-type registry. Used by GTM material, site-setup wizards,
 *   and as a reference for "what does a BIZ install look like?"
 *
 * - configured(): live state of the running install — uses the snapshot
 *   composer to gather every snapshotInclude:true action's result, plus
 *   computed metadata (current footer/navigation defaults, registered action
 *   count, manifest fingerprint).
 *
 * Cross-reference: full-parity-mcp-2026-05-10.md §12.
 */
final class MyronCompatibilityDocumentService
{
    public function __construct(
        private readonly MyronSystemSnapshotService $snapshotService,
        private readonly MyronSiteTypeRegistryRepository $siteTypes,
        private readonly MyronFooterDesignRepository $footers,
        private readonly MyronBundleActivationRepository $bundles,
    ) {
    }

    /**
     * Default compatibility document for a site type.
     *
     * @return array<string, mixed>
     */
    public function default(string $siteTypeKey): array
    {
        $siteTypeKey = strtoupper(trim($siteTypeKey));
        $allTypes    = (array) $this->siteTypes->listSiteTypes();
        $matched     = null;
        foreach ($allTypes as $row) {
            $code = strtoupper(trim((string) ($row['code'] ?? $row['bundle_type'] ?? '')));
            if ($code === $siteTypeKey) {
                $matched = $row;
                break;
            }
        }
        if ($matched === null) {
            return [
                'mode'        => 'default',
                'error'       => 'unknown_site_type',
                'siteTypeKey' => $siteTypeKey,
            ];
        }

        $bundleId = (string) ($matched['bundle_id'] ?? $matched['bundleId'] ?? '');
        // APO-205 Card 2 — JSONM site-type records use `name` (not `label`)
        // as the human-readable field. Fall back to `label` for legacy rows,
        // then to the key as a last resort.
        $siteTypeLabel = (string) ($matched['name'] ?? $matched['label'] ?? $siteTypeKey);
        // The recommended footer for this site type is encoded in `footerVariant`.
        $defaultFooterId = trim((string) ($matched['footerVariant'] ?? MyronFooterDesignRepository::DEFAULT_VARIANT));
        if ($defaultFooterId === '') {
            $defaultFooterId = MyronFooterDesignRepository::DEFAULT_VARIANT;
        }
        return [
            'mode'              => 'default',
            'generatedAt'       => date('Y-m-d\TH:i:s\Z'),
            'siteType'          => [
                'key'         => $siteTypeKey,
                'bundleId'    => $bundleId,
                'label'       => $siteTypeLabel,
                'description' => $matched['description'] ?? null,
                'subtypes'    => $matched['subtypes'] ?? [],
            ],
            'recommendedBundle' => [
                'bundleId' => $bundleId,
                'label'    => $siteTypeLabel,
            ],
            // APO-205 Card 2 — default footer surfaced as variantId + label.
            'defaultFooter'     => [
                'variantId' => $defaultFooterId,
                'label'     => function_exists('myronFooterVariantCompositionService')
                    ? myronFooterVariantCompositionService()->labelForVariant($defaultFooterId)
                    : 'Standard',
            ],
            // APO-205 Card 3 — design surface enumerated so AI clients can
            // answer "what archetypes does the system ship with?" without
            // synthesising from compatibility-doc bundle defaults. The
            // `recommendedArchetypeForActiveBundle` block is computed from
            // this site type's `recommendedArchetypes` JSONM hint.
            'design'            => $this->describeDesignSurfaceForSiteTypeRecord($matched),
            'supportedActions'  => $this->summariseSupportedActions(),
            'tokenAuthority'    => [
                'description' => 'Per-site-type token authority overlay; tokens are layered: site → bundle → component → page.',
            ],
            'liveContentResolution' => [
                'description' => 'Components resolve content from page sections; image references resolve via files asset IDs; tokens layer per the design system overlay.',
            ],
        ];
    }

    /**
     * Configured compatibility document — the LIVE state of the install.
     *
     * @return array<string, mixed>
     */
    public function configured(): array
    {
        $snapshot = $this->snapshotService->compose();

        // APO-205 Card 2 — `currentDefaultFooter` is returned as an object
        // (variantId + label + isDefault + isActivated) rather than a bare
        // variant code. Bare codes force AI clients to maintain a parallel
        // lookup table just to answer "what footer is active?".
        $defaultFooter = [
            'variantId'   => MyronFooterDesignRepository::DEFAULT_VARIANT,
            'label'       => 'Standard',
            'isDefault'   => true,
            'isActivated' => true,
        ];
        try {
            $defaultRow = $this->footers->getDefault();
            $defaultFooter = [
                'variantId'   => (string) ($defaultRow['variant_id'] ?? MyronFooterDesignRepository::DEFAULT_VARIANT),
                'label'       => (string) ($defaultRow['variant_label'] ?? ''),
                'isDefault'   => (bool) ($defaultRow['is_default'] ?? true),
                'isActivated' => (bool) ($defaultRow['is_activated'] ?? true),
            ];
        } catch (Throwable) {
            // Keep the safe fallback above.
        }

        $primaryBundleId = (string) ($this->bundles->getPrimaryBundleId() ?? '');
        $siteTypeKey     = $this->resolveActiveSiteTypeKey($primaryBundleId);

        // APO-205 Card 2 — `activeSiteType` returned as an object with
        // code + label + description rather than a bare uppercase code.
        $activeSiteType = $this->describeActiveSiteType($siteTypeKey);

        return [
            'mode'                    => 'configured',
            'generatedAt'             => $snapshot['generatedAt'] ?? date('Y-m-d\TH:i:s\Z'),
            'install'                 => $snapshot['install'] ?? [],
            'activeSiteType'          => $activeSiteType,
            'activeBundle'            => $primaryBundleId,
            'currentDefaultFooter'    => $defaultFooter,
            'design'                  => $this->describeDesignSurface($primaryBundleId),
            'manifest'                => $snapshot['manifest'] ?? [],
            'snapshot'                => $this->stripMetaKeys($snapshot),
            'liveContentResolution'   => [
                'description' => 'Per-component resolver behaviour for the current install: surfaces register reads under snapshotInclude:true; mutations are visible in the manifest but excluded from snapshot composition.',
            ],
        ];
    }

    /**
     * APO-205 Card 2 — pair the active site-type code with its human-readable label.
     *
     * JSONM site-type records use `name` for the operator-facing label
     * (e.g. "Portfolio / Creative"); legacy rows may use `label`.
     *
     * @return array{code:string,label:string,description:string,bundleId?:string}
     */
    private function describeActiveSiteType(string $siteTypeCode): array
    {
        $code = strtoupper(trim($siteTypeCode));
        if ($code === '') {
            return ['code' => '', 'label' => '', 'description' => ''];
        }

        try {
            foreach ((array) $this->siteTypes->listSiteTypes() as $row) {
                $rowCode = strtoupper(trim((string) ($row['code'] ?? $row['bundle_type'] ?? '')));
                if ($rowCode === $code) {
                    return [
                        'code'        => $code,
                        'label'       => (string) ($row['name'] ?? $row['label'] ?? $code),
                        'description' => (string) ($row['description'] ?? ''),
                        'bundleId'    => (string) ($row['bundle_id'] ?? $row['bundleId'] ?? ''),
                    ];
                }
            }
        } catch (Throwable) {
            // fall through
        }

        return ['code' => $code, 'label' => $code, 'description' => ''];
    }

    /**
     * APO-205 Card 3 — describe the design surface for a specific site-type
     * record, computing the recommended archetype from its `recommendedArchetypes`
     * JSONM hint (an array of {code, default}). Used by the default()
     * compatibility document where the active bundle is the site-type bundle.
     *
     * @param array<string, mixed> $siteTypeRecord
     * @return array<string, mixed>
     */
    private function describeDesignSurfaceForSiteTypeRecord(array $siteTypeRecord): array
    {
        $bundleId = (string) ($siteTypeRecord['bundle_id'] ?? $siteTypeRecord['bundleId'] ?? '');
        $surface  = $this->describeDesignSurface($bundleId);

        // Recompute recommended from the JSONM hint when present (more
        // specific than the bundle-level lookup).
        if ($surface['recommendedArchetypeForActiveBundle'] === null) {
            $hints = $siteTypeRecord['recommendedArchetypes'] ?? null;
            if (is_array($hints)) {
                $defaultCode = '';
                foreach ($hints as $hint) {
                    if (is_array($hint) && !empty($hint['default'])) {
                        $defaultCode = strtolower(trim((string) ($hint['code'] ?? '')));
                        break;
                    }
                }
                if ($defaultCode === '' && isset($hints[0]) && is_array($hints[0])) {
                    $defaultCode = strtolower(trim((string) ($hints[0]['code'] ?? '')));
                }
                if ($defaultCode !== '') {
                    $name = $defaultCode;
                    foreach ($surface['availableArchetypes'] as $a) {
                        if (strcasecmp((string) ($a['id'] ?? ''), $defaultCode) === 0) {
                            $name = (string) ($a['name'] ?? $defaultCode);
                            break;
                        }
                    }
                    $surface['recommendedArchetypeForActiveBundle'] = [
                        'id'     => $defaultCode,
                        'name'   => $name,
                        'reason' => 'site-type default for ' . (string) ($siteTypeRecord['code'] ?? '') . ' (JSONM recommendedArchetypes)',
                        'kind'   => 'packaged',
                    ];
                }
            }
        }

        return $surface;
    }

    /**
     * APO-205 Card 3 — describe the design surface (currentDesignSystem +
     * availableArchetypes + storedDefinitions + recommended-for-active-bundle).
     *
     * The packaged archetype + stored-definition enumeration comes from the
     * JSONM DesignSystemRegistry. The recommended archetype for the active
     * bundle is resolved through the site-type record's design hints.
     *
     * @return array<string, mixed>
     */
    private function describeDesignSurface(string $primaryBundleId): array
    {
        $registry = $this->resolveDesignSystemRegistry();
        if ($registry === null) {
            return [
                'currentDesignSystem'                  => null,
                'availableArchetypes'                  => [],
                'storedDefinitions'                    => [],
                'recommendedArchetypeForActiveBundle'  => null,
            ];
        }

        if (method_exists($registry, 'ensureStorage')) {
            try { $registry->ensureStorage(); } catch (Throwable) { /* non-fatal */ }
        }

        $current = null;
        if (method_exists($registry, 'currentDefinition')) {
            try {
                $row = (array) $registry->currentDefinition();
                $current = [
                    'id'          => (string) ($row['id'] ?? ($row['code'] ?? '')),
                    'name'        => (string) ($row['name'] ?? ''),
                    'description' => (string) ($row['description'] ?? ''),
                    'kind'        => (string) ($row['kind'] ?? $this->inferDesignKind($row)),
                ];
            } catch (Throwable) {
                $current = null;
            }
        }

        $available = [];
        if (method_exists($registry, 'listPackagedArchetypes')) {
            try {
                foreach ((array) $registry->listPackagedArchetypes() as $a) {
                    if (!is_array($a)) continue;
                    $available[] = [
                        'id'          => (string) ($a['id'] ?? ($a['code'] ?? '')),
                        'name'        => (string) ($a['name'] ?? ''),
                        'description' => (string) ($a['description'] ?? ''),
                        'kind'        => 'packaged',
                    ];
                }
            } catch (Throwable) {
                $available = [];
            }
        }

        // Cap available at packaged set (10 today). Stored definitions are
        // operator-saved; defensively cap at 50 most-recently-modified.
        $stored = [];
        if (method_exists($registry, 'listDefinitions')) {
            try {
                $rows = (array) $registry->listDefinitions();
                foreach ($rows as $r) {
                    if (!is_array($r)) continue;
                    $kind = (string) ($r['kind'] ?? '');
                    if ($kind === 'packaged') continue; // packaged already in available
                    $stored[] = [
                        'id'          => (string) ($r['id'] ?? ''),
                        'name'        => (string) ($r['name'] ?? ''),
                        'description' => (string) ($r['description'] ?? ''),
                        'kind'        => $kind !== '' ? $kind : 'stored',
                        'updatedAt'   => (string) ($r['updatedAt'] ?? $r['updated_at'] ?? ''),
                    ];
                }
                // Most-recently-modified first; cap at 50.
                usort($stored, static fn(array $a, array $b): int => strcmp((string) ($b['updatedAt'] ?? ''), (string) ($a['updatedAt'] ?? '')));
                $stored = array_slice($stored, 0, 50);
            } catch (Throwable) {
                $stored = [];
            }
        }

        $recommended = $this->resolveRecommendedArchetypeForActiveBundle($primaryBundleId, $available);

        return [
            'currentDesignSystem'                 => $current,
            'availableArchetypes'                 => $available,
            'storedDefinitions'                   => $stored,
            'recommendedArchetypeForActiveBundle' => $recommended,
        ];
    }

    private function resolveDesignSystemRegistry(): ?object
    {
        $regClass = '\\DesignSystemRegistry';
        if (!class_exists($regClass)) {
            $candidate = dirname(__DIR__, 2) . '/jsonm/php/lib/DesignSystemRegistry.php';
            if (is_file($candidate)) {
                require_once $candidate;
            }
        }
        if (!class_exists($regClass)) {
            return null;
        }
        try {
            /** @var object $instance */
            $instance = new $regClass();
            return $instance;
        } catch (Throwable) {
            return null;
        }
    }

    /**
     * @param array<string, mixed> $row
     */
    private function inferDesignKind(array $row): string
    {
        if (!empty($row['archetypeId']) || !empty($row['archetype_id']) || !empty($row['archetypeCode'])) {
            return 'packaged';
        }
        if (!empty($row['baseline']) || !empty($row['isBaseline'])) {
            return 'baseline';
        }
        return 'stored';
    }

    /**
     * @param list<array<string, mixed>> $available
     * @return array{id:string,name:string,reason:string,kind:string}|null
     */
    private function resolveRecommendedArchetypeForActiveBundle(string $primaryBundleId, array $available): ?array
    {
        if ($primaryBundleId === '') {
            return null;
        }
        try {
            $row = $this->siteTypes->findSiteTypeById($primaryBundleId);
        } catch (Throwable) {
            return null;
        }
        if (!is_array($row)) {
            return null;
        }
        // Sources of truth for "recommended archetype": prefer an explicit
        // siteType.design.recommendedArchetype hint, then design.recommendedArchetype,
        // then any `archetype` key in the design record. None is fine — emit null.
        $hint = (array) ($row['design'] ?? []);
        $code = trim((string) (
            $hint['recommendedArchetype']
            ?? $hint['archetype']
            ?? $row['recommendedArchetype']
            ?? ''
        ));
        if ($code === '') {
            return null;
        }
        // Resolve name from packaged archetypes if we have a match.
        $name = $code;
        foreach ($available as $a) {
            if (strcasecmp((string) ($a['id'] ?? ''), $code) === 0) {
                $name = (string) ($a['name'] ?? $code);
                break;
            }
        }
        return [
            'id'     => $code,
            'name'   => $name,
            'reason' => 'site-type default for active bundle',
            'kind'   => 'packaged',
        ];
    }

    /**
     * Summarise supported action URIs for the default compatibility document.
     * Reads the registry directly — does NOT invoke the snapshot composer so
     * the default-doc endpoint can be called independently of live state.
     *
     * @return array<int, string>
     */
    private function summariseSupportedActions(): array
    {
        $uris = [];
        // Reach into the action registry via the snapshot service's composer
        // dependency — but only read the static catalog, not compose.
        $reg = function_exists('myronActionRegistry') ? myronActionRegistry() : null;
        if ($reg === null) {
            return [];
        }
        foreach ($reg->all() as $action) {
            $m = $action->metadata();
            if (!$m->exposeToMcp) {
                continue;
            }
            $uris[] = $m->uri;
        }
        sort($uris);
        return $uris;
    }

    /**
     * @param array<string, mixed> $snapshot
     * @return array<string, mixed>
     */
    private function stripMetaKeys(array $snapshot): array
    {
        unset($snapshot['version'], $snapshot['generatedAt'], $snapshot['install'], $snapshot['manifest']);
        return $snapshot;
    }

    /**
     * APO-205-011 Fix C — resolve the active site-type code from the active
     * bundle id. The original implementation called a non-existent
     * `siteTypeForBundle()` method on the registry repo, then silently fell
     * through to an empty string — causing `activeSiteType` to report empty
     * even when a bundle was active. The repo's `findSiteTypeById()` is the
     * correct lookup; the row carries the `code` (AGY, PORT, etc.).
     */
    private function resolveActiveSiteTypeKey(string $primaryBundleId): string
    {
        if ($primaryBundleId === '') {
            return '';
        }
        try {
            $row = $this->siteTypes->findSiteTypeById($primaryBundleId);
            if (is_array($row)) {
                $code = trim((string) ($row['code'] ?? $row['bundle_type'] ?? ''));
                if ($code !== '') {
                    return strtoupper($code);
                }
            }
        } catch (Throwable) {
            // Defensive fallback — return empty so callers degrade gracefully.
        }
        return '';
    }
}
