<?php

declare(strict_types=1);

/**
 * MyronTemplateContentResolver — single source of truth for resolving the canonical
 * defaultContent of a template across rendering paths.
 *
 * Background: under STN-013, four rendering paths each implemented their own resolution
 * of "given a templateId, what defaultContent should I render?":
 *
 *   1. EditorSurfaceReadModel::templateSurface (Page Templates Designer editor)
 *   2. EditorSurfaceReadModel::templateStartedPageSurface (page-editor template_source flow)
 *   3. TemplatePreviewFactory::buildPreviewPage (Site Templates inspect view)
 *   4. page-editor.php editorCanvasPage content seeding
 *
 * Each had subtly different fallback rules, leading to the AGY-About-renders-dev-helper
 * leak that operator validation surfaced. This resolver centralizes the logic so the
 * next surface that needs to render template-with-content joins the existing pattern
 * instead of inventing its own.
 *
 * Resolution order (preferred → fallback):
 *   1. Runtime template's `page_template_versions.schema_json.defaultContent` (Designer-customized)
 *   2. Platform definition's `template_definitions.record_json.defaultContent` (loader-seeded)
 *   3. Empty array (renderer falls through to its own per-component placeholder logic)
 *
 * Eager instantiation (opt-in): when callers need the runtime template to exist (e.g.,
 * the page-editor template_source flow), pass `eagerInstantiate: true` to instantiate
 * the runtime if missing. This is the same pattern STN-012-017 applied to the
 * page-editor.php GET handler. Idempotent (instantiateTemplate preserves Designer edits).
 */
final class MyronTemplateContentResolver
{
    public function __construct(
        private readonly MyronTemplateDefinitionRepository $definitions,
        private readonly MyronContentRepository $content,
        private readonly MyronTemplateInstantiationService $instantiation,
    ) {
    }

    /**
     * Resolve the canonical defaultContent for a templateId.
     *
     * @param string $sourceTemplateId Platform-shipped template id (e.g., "template.home.agy.v1")
     * @param bool $eagerInstantiate If true and runtime template is missing, instantiate it
     *                                so it carries forward the loader-seeded defaultContent.
     * @return array<string,mixed> Canonical defaultContent shape: { _schemaVersion, sections }
     *                              May be empty if no seed exists at any layer.
     */
    public function resolveDefaultContent(string $sourceTemplateId, bool $eagerInstantiate = false): array
    {
        $definition = $this->definitions->findTemplateById($sourceTemplateId);
        if (!is_array($definition)) {
            return [];
        }

        $runtimeTemplate = $eagerInstantiate
            ? $this->instantiation->instantiateTemplate($definition)
            : $this->content->findRuntimeTemplateBySourceTemplateId($sourceTemplateId);

        // Layer 1: runtime template defaultContent (Designer-customized seed)
        $runtimeSchema = is_array($runtimeTemplate['schema'] ?? null) ? $runtimeTemplate['schema'] : null;
        $runtimeDefault = is_array($runtimeSchema['defaultContent'] ?? null) ? $runtimeSchema['defaultContent'] : null;
        if ($runtimeDefault !== null && self::hasContent($runtimeDefault)) {
            return $runtimeDefault;
        }

        // Layer 2: platform definition defaultContent (loader-seeded, preserved across re-ingestion)
        $definitionDefault = is_array($definition['defaultContent'] ?? null) ? $definition['defaultContent'] : null;
        if ($definitionDefault !== null && self::hasContent($definitionDefault)) {
            return $definitionDefault;
        }

        // Layer 3: empty — let the renderer's per-component placeholders handle it.
        return [];
    }

    /**
     * Resolve a flat content payload (eyebrow/headline/intro/body/ctaLabel/ctaHref) for a
     * template's first/primary section. Useful for filling page-level chrome content
     * where renderers expect a flat content shape rather than the section-keyed schema.
     *
     * Promotes the first non-empty section's flat fields to the top level. Falls back to
     * deriving headline from the template's pageTitle/name when no section content exists.
     *
     * @return array<string,string>
     */
    public function resolvePageLevelContent(string $sourceTemplateId, bool $eagerInstantiate = false): array
    {
        $definition = $this->definitions->findTemplateById($sourceTemplateId);
        $defaultContent = $this->resolveDefaultContent($sourceTemplateId, $eagerInstantiate);

        $sections = is_array($defaultContent['sections'] ?? null) ? $defaultContent['sections'] : [];
        $primary = [];
        foreach ($sections as $sectionContent) {
            if (is_array($sectionContent)) {
                $primary = $sectionContent;
                break;
            }
        }

        $title = is_array($definition)
            ? trim((string) (($definition['pageTitle'] ?? '') ?: ($definition['name'] ?? $sourceTemplateId)))
            : $sourceTemplateId;

        return [
            'eyebrow' => trim((string) ($primary['eyebrow'] ?? '')),
            'headline' => trim((string) ($primary['headline'] ?? '')) ?: $title,
            'intro' => trim((string) ($primary['intro'] ?? '')),
            'body' => trim((string) ($primary['body'] ?? '')),
            'ctaLabel' => trim((string) ($primary['ctaLabel'] ?? '')),
            'ctaHref' => trim((string) ($primary['ctaHref'] ?? '')),
        ];
    }

    /**
     * Whether a defaultContent payload carries at least one non-empty section field.
     * Empty `sections` array, or sections present but all-empty, both return false so
     * the resolver falls through to the next layer.
     *
     * @param array<string,mixed> $defaultContent
     */
    private static function hasContent(array $defaultContent): bool
    {
        $sections = is_array($defaultContent['sections'] ?? null) ? $defaultContent['sections'] : [];
        if ($sections === []) {
            return false;
        }
        foreach ($sections as $section) {
            if (!is_array($section)) {
                continue;
            }
            foreach ($section as $value) {
                if (is_string($value) && trim($value) !== '') {
                    return true;
                }
                if (is_array($value) && $value !== []) {
                    // Repeatable group with entries counts as content.
                    return true;
                }
            }
        }
        return false;
    }
}
