<?php

declare(strict_types=1);

final class MyronPageContractService
{
    public function __construct(
        private readonly MyronContentRepository $repository,
        private readonly MyronEditorSurfaceReadModel $surfaceReadModel,
        private readonly MyronFileRepository $files,
    ) {
    }

    /**
     * @return array<string, mixed>
     */
    public function contractForPage(string $pageId): array
    {
        $surface = $this->surfaceReadModel->pageSurface($pageId);
        if (!is_array($surface)) {
            throw new MyronActionPreconditionError('Page not found: ' . $pageId);
        }

        $rawPage = is_array($surface['raw']['page'] ?? null) ? $surface['raw']['page'] : [];
        $templateVersionId = trim((string) (($rawPage['template_version_id'] ?? '') ?: ($rawPage['active_template_version_id'] ?? '')));
        $pageVersionId = trim((string) (($rawPage['active_version_id'] ?? '') ?: ($rawPage['current_draft_version_id'] ?? '')));

        $sections = [];
        foreach (is_array($surface['sections'] ?? null) ? $surface['sections'] : [] as $node) {
            if (!is_array($node)) {
                continue;
            }
            $sections[] = $this->contractSection($node);
        }

        return [
            'kind' => 'page-contract',
            'contractVersion' => $templateVersionId !== '' ? 'page-template-version:' . $templateVersionId : 'unknown',
            'page' => [
                'id' => (string) ($surface['identity']['id'] ?? ''),
                'title' => (string) ($surface['identity']['title'] ?? ''),
                'path' => (string) ($surface['identity']['path'] ?? ''),
                'pageVersionId' => $pageVersionId,
                'templateId' => (string) ($surface['identity']['templateId'] ?? ''),
                'templateKey' => (string) ($surface['identity']['templateKey'] ?? ''),
                'templateName' => (string) ($surface['identity']['templateName'] ?? ''),
                'templateVersionId' => $templateVersionId,
                'templateCurrentVersionId' => (string) ($rawPage['template_current_version_id'] ?? ''),
            ],
            'sourceAuthority' => (string) ($surface['sourceAuthority'] ?? ''),
            'ownershipState' => (string) ($surface['ownershipState'] ?? ''),
            'availableActions' => array_values(array_filter(
                is_array($surface['availableActions'] ?? null) ? $surface['availableActions'] : [],
                'is_string'
            )),
            'sections' => $sections,
        ];
    }

    /**
     * @param array<string, mixed> $fields
     * @return array<string, mixed>
     */
    public function updateSectionContent(
        string $pageId,
        string $sectionInstanceKey,
        array $fields,
        bool $publishNow = false,
        ?string $publishedAt = null
    ): array {
        $pageId = trim($pageId);
        $sectionInstanceKey = trim($sectionInstanceKey);
        if ($pageId === '') {
            throw new InvalidArgumentException('pageId is required.');
        }
        if ($sectionInstanceKey === '') {
            throw new InvalidArgumentException('sectionInstanceKey is required.');
        }
        if ($fields === []) {
            throw new InvalidArgumentException('fields must include at least one value.');
        }
        if (array_key_exists('bodyMarkdown', $fields)) {
            if (!array_key_exists('body', $fields)) {
                $fields['body'] = myronMarkdownToHtmlConverter()->convert((string) $fields['bodyMarkdown']);
            }
            unset($fields['bodyMarkdown']);
        }

        $page = $this->repository->getEditablePage($pageId);
        if (!is_array($page)) {
            throw new MyronActionPreconditionError('Page not found: ' . $pageId);
        }

        $contract = $this->contractForPage($pageId);
        $section = $this->findContractSection($contract, $sectionInstanceKey);
        if (!is_array($section)) {
            throw new InvalidArgumentException('Section is not present in the Page Contract: ' . $sectionInstanceKey . '.');
        }
        if ((string) ($section['editability'] ?? '') !== 'editable') {
            throw new InvalidArgumentException('Section is inspect-only and cannot be edited: ' . $sectionInstanceKey . '.');
        }

        $normalized = $this->validateFieldPayload($section, $fields);
        $content = is_array($page['content'] ?? null) ? $page['content'] : [];
        $sections = is_array($content['sections'] ?? null) ? $content['sections'] : [];
        $currentSection = is_array($sections[$sectionInstanceKey] ?? null) ? $sections[$sectionInstanceKey] : [];
        $sections[$sectionInstanceKey] = array_merge($currentSection, $normalized);
        $content['sections'] = $sections;
        $content['_schemaVersion'] = 2;

        foreach (['eyebrow', 'headline', 'intro', 'body', 'ctaLabel', 'ctaHref'] as $flatKey) {
            if (array_key_exists($flatKey, $normalized) && !is_array($normalized[$flatKey])) {
                $content[$flatKey] = (string) $normalized[$flatKey];
            }
        }

        $this->repository->savePage(
            $pageId,
            (string) ($page['template_id'] ?? ''),
            (string) ($page['title'] ?? ''),
            (string) ($page['slug'] ?? ''),
            isset($page['parent_page_id']) ? (string) $page['parent_page_id'] : null,
            (string) ($page['seo_title'] ?? ''),
            (string) ($page['meta_description'] ?? ''),
            $content,
            $publishNow,
            $publishedAt
        );

        return [
            'pageId' => $pageId,
            'sectionInstanceKey' => $sectionInstanceKey,
            'fieldsUpdated' => array_keys($normalized),
            'publishedAt' => $publishNow ? ($publishedAt ?? myronNow()) : null,
        ];
    }

    /**
     * @return array<string, mixed>
     */
    public function updateBodyMarkdown(
        string $pageId,
        ?string $sectionInstanceKey,
        string $markdown,
        bool $publishNow = false,
        ?string $publishedAt = null
    ): array {
        $pageId = trim($pageId);
        if ($pageId === '') {
            throw new InvalidArgumentException('pageId is required.');
        }
        if (trim($markdown) === '') {
            throw new InvalidArgumentException('markdown is required.');
        }

        $targetSectionKey = trim((string) $sectionInstanceKey);
        if ($targetSectionKey === '') {
            $contract = $this->contractForPage($pageId);
            foreach (is_array($contract['sections'] ?? null) ? $contract['sections'] : [] as $section) {
                if (!is_array($section) || (string) ($section['editability'] ?? '') !== 'editable') {
                    continue;
                }
                $hasBody = false;
                foreach (is_array($section['fields'] ?? null) ? $section['fields'] : [] as $field) {
                    if (is_array($field) && (string) ($field['key'] ?? '') === 'body') {
                        $hasBody = true;
                        break;
                    }
                }
                if ($hasBody && (string) ($section['componentId'] ?? '') === 'cms-content-body') {
                    $targetSectionKey = (string) ($section['instanceKey'] ?? '');
                    break;
                }
                if ($hasBody && $targetSectionKey === '') {
                    $targetSectionKey = (string) ($section['instanceKey'] ?? '');
                }
            }
        }
        if ($targetSectionKey === '') {
            throw new InvalidArgumentException('No editable body section was found for this page.');
        }

        return $this->updateSectionContent($pageId, $targetSectionKey, [
            'body' => myronMarkdownToHtmlConverter()->convert($markdown),
        ], $publishNow, $publishedAt);
    }

    /**
     * @param array<string, mixed> $node
     * @return array<string, mixed>
     */
    private function contractSection(array $node): array
    {
        return [
            'id' => (string) ($node['id'] ?? ''),
            'instanceKey' => (string) ($node['instanceKey'] ?? ''),
            'sectionUuid' => (string) ($node['sectionUuid'] ?? ''),
            'position' => (int) ($node['position'] ?? 0),
            'label' => (string) ($node['label'] ?? ''),
            'sectionKey' => (string) ($node['sectionKey'] ?? ''),
            'componentId' => (string) ($node['componentId'] ?? ''),
            'variant' => (string) ($node['variant'] ?? ''),
            'mediaLayout' => (string) ($node['mediaLayout'] ?? ''),
            'editability' => (string) ($node['editability'] ?? 'inspect-only'),
            'ownershipState' => (string) ($node['ownershipState'] ?? ''),
            'availableActions' => array_values(array_filter(
                is_array($node['availableActions'] ?? null) ? $node['availableActions'] : [],
                'is_string'
            )),
            'fields' => $this->contractFields(is_array($node['editableFields'] ?? null) ? $node['editableFields'] : []),
            'repeaters' => $this->contractRepeaters(is_array($node['repeatableGroups'] ?? null) ? $node['repeatableGroups'] : []),
        ];
    }

    /**
     * @param list<array<string, mixed>> $fields
     * @return list<array<string, mixed>>
     */
    private function contractFields(array $fields): array
    {
        $contract = [];
        foreach ($fields as $field) {
            if (!is_array($field)) {
                continue;
            }
            $key = trim((string) ($field['key'] ?? ''));
            if ($key === '') {
                continue;
            }
            $type = $this->normalizeType((string) ($field['type'] ?? 'text'));
            $contract[] = [
                'key' => $key,
                'label' => (string) ($field['label'] ?? $key),
                'type' => $type,
                'required' => (bool) ($field['required'] ?? false),
                'assetKind' => (string) ($field['assetKind'] ?? ''),
                'presentation' => (string) (($field['presentation'] ?? '') ?: ($type === 'icon' ? 'icon' : '')),
                'binding' => (string) ($field['binding'] ?? ''),
                'value' => $field['value'] ?? '',
                'default' => $field['default'] ?? null,
                'constraints' => is_array($field['constraints'] ?? null) ? $field['constraints'] : [],
            ];
        }

        return $contract;
    }

    /**
     * @param list<array<string, mixed>> $repeaters
     * @return list<array<string, mixed>>
     */
    private function contractRepeaters(array $repeaters): array
    {
        $contract = [];
        foreach ($repeaters as $repeater) {
            if (!is_array($repeater)) {
                continue;
            }
            $key = trim((string) ($repeater['key'] ?? ''));
            if ($key === '') {
                continue;
            }
            $contract[] = [
                'key' => $key,
                'label' => (string) ($repeater['label'] ?? $key),
                'itemLabel' => (string) ($repeater['itemLabel'] ?? 'Item'),
                'type' => 'repeater',
                'minItems' => max(0, (int) ($repeater['minItems'] ?? 0)),
                'maxItems' => max(0, (int) ($repeater['maxItems'] ?? 50)),
                'binding' => (string) ($repeater['binding'] ?? ''),
                'fields' => $this->contractFields(is_array($repeater['fields'] ?? null) ? $repeater['fields'] : []),
                'items' => array_values(array_filter(is_array($repeater['items'] ?? null) ? $repeater['items'] : [], 'is_array')),
            ];
        }

        return $contract;
    }

    /**
     * @param array<string, mixed> $contract
     * @return array<string, mixed>|null
     */
    private function findContractSection(array $contract, string $sectionInstanceKey): ?array
    {
        foreach (is_array($contract['sections'] ?? null) ? $contract['sections'] : [] as $section) {
            if (!is_array($section)) {
                continue;
            }
            if ((string) ($section['instanceKey'] ?? '') === $sectionInstanceKey || (string) ($section['sectionUuid'] ?? '') === $sectionInstanceKey) {
                return $section;
            }
        }

        return null;
    }

    /**
     * @param array<string, mixed> $section
     * @param array<string, mixed> $payload
     * @return array<string, mixed>
     */
    private function validateFieldPayload(array $section, array $payload): array
    {
        $fieldMap = [];
        foreach (is_array($section['fields'] ?? null) ? $section['fields'] : [] as $field) {
            if (is_array($field) && trim((string) ($field['key'] ?? '')) !== '') {
                $fieldMap[(string) $field['key']] = $field;
            }
        }
        $repeaterMap = [];
        foreach (is_array($section['repeaters'] ?? null) ? $section['repeaters'] : [] as $repeater) {
            if (is_array($repeater) && trim((string) ($repeater['key'] ?? '')) !== '') {
                $repeaterMap[(string) $repeater['key']] = $repeater;
            }
        }

        $normalized = [];
        foreach ($payload as $key => $value) {
            $fieldKey = trim((string) $key);
            if ($fieldKey === '') {
                continue;
            }
            if (isset($fieldMap[$fieldKey])) {
                $normalized[$fieldKey] = $this->normalizeValue($fieldMap[$fieldKey], $value, $fieldKey);
                continue;
            }
            if (isset($repeaterMap[$fieldKey])) {
                $normalized[$fieldKey] = $this->normalizeRepeaterValue($repeaterMap[$fieldKey], $value, $fieldKey);
                continue;
            }

            throw new InvalidArgumentException('Field is not editable by this Page Contract: ' . $fieldKey . '.');
        }

        return $normalized;
    }

    /**
     * @param array<string, mixed> $field
     */
    private function normalizeValue(array $field, mixed $value, string $fieldKey): mixed
    {
        $type = $this->normalizeType((string) ($field['type'] ?? 'text'));
        if ($value === null) {
            $value = '';
        }

        if ($type === 'boolean') {
            return filter_var($value, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? false;
        }

        if ($type === 'number') {
            if ($value === '' || !is_numeric($value)) {
                throw new InvalidArgumentException('Field must be numeric: ' . $fieldKey . '.');
            }
            return (float) $value;
        }

        if (is_array($value)) {
            throw new InvalidArgumentException('Field does not accept an object or list value: ' . $fieldKey . '.');
        }

        $stringValue = trim((string) $value);
        if ((bool) ($field['required'] ?? false) && $stringValue === '') {
            throw new InvalidArgumentException('Required field cannot be empty: ' . $fieldKey . '.');
        }
        if ($type === 'url' && $stringValue !== '' && !str_starts_with($stringValue, '/') && filter_var($stringValue, FILTER_VALIDATE_URL) === false) {
            throw new InvalidArgumentException('Field must be a valid URL or root-relative path: ' . $fieldKey . '.');
        }
        $assetKind = $this->assetKindForField($field, $type);
        if ($assetKind !== '' && $stringValue !== '') {
            $this->assertAssetKind($stringValue, $assetKind, $fieldKey);
        }
        $acceptedValues = $this->acceptedValuesForField($field);
        if ($stringValue !== '' && $acceptedValues !== [] && !in_array($stringValue, $acceptedValues, true)) {
            throw new InvalidArgumentException(
                'Field must be one of the accepted values for ' . $fieldKey . ': ' . implode(', ', $acceptedValues) . '.'
            );
        }

        return $stringValue;
    }

    /**
     * @param array<string, mixed> $field
     * @return list<string>
     */
    private function acceptedValuesForField(array $field): array
    {
        $constraints = is_array($field['constraints'] ?? null) ? $field['constraints'] : [];
        $values = is_array($constraints['acceptedValues'] ?? null)
            ? $constraints['acceptedValues']
            : (is_array($constraints['enum'] ?? null) ? $constraints['enum'] : []);
        $accepted = [];
        foreach ($values as $value) {
            if (!is_scalar($value)) {
                continue;
            }
            $normalized = trim((string) $value);
            if ($normalized !== '') {
                $accepted[$normalized] = true;
            }
        }

        return array_keys($accepted);
    }

    /**
     * @param array<string, mixed> $repeater
     * @return list<array<string, mixed>>
     */
    private function normalizeRepeaterValue(array $repeater, mixed $value, string $fieldKey): array
    {
        if (!is_array($value)) {
            throw new InvalidArgumentException('Repeater field must be a list: ' . $fieldKey . '.');
        }
        $items = array_values(array_filter($value, 'is_array'));
        $min = max(0, (int) ($repeater['minItems'] ?? 0));
        $max = max(0, (int) ($repeater['maxItems'] ?? 50));
        if (count($items) < $min) {
            throw new InvalidArgumentException('Repeater field has too few items: ' . $fieldKey . '.');
        }
        if ($max > 0 && count($items) > $max) {
            throw new InvalidArgumentException('Repeater field has too many items: ' . $fieldKey . '.');
        }

        $fieldMap = [];
        foreach (is_array($repeater['fields'] ?? null) ? $repeater['fields'] : [] as $field) {
            if (is_array($field) && trim((string) ($field['key'] ?? '')) !== '') {
                $fieldMap[(string) $field['key']] = $field;
            }
        }
        $normalizedItems = [];
        foreach ($items as $item) {
            $normalizedItem = [];
            foreach ($item as $itemKey => $itemValue) {
                $normalizedKey = trim((string) $itemKey);
                if (!isset($fieldMap[$normalizedKey])) {
                    throw new InvalidArgumentException('Repeater field is not editable by this Page Contract: ' . $fieldKey . '.' . $normalizedKey . '.');
                }
                $normalizedItem[$normalizedKey] = $this->normalizeValue($fieldMap[$normalizedKey], $itemValue, $fieldKey . '.' . $normalizedKey);
            }
            $normalizedItems[] = $normalizedItem;
        }

        return $normalizedItems;
    }

    /**
     * @param array<string, mixed> $field
     */
    private function assetKindForField(array $field, string $type): string
    {
        $assetKind = strtolower(trim((string) ($field['assetKind'] ?? '')));
        if ($assetKind === '' && in_array($type, ['icon', 'image', 'video', 'audio', 'document'], true)) {
            $assetKind = $type === 'icon' ? 'image' : $type;
        }
        $assetKind = rtrim($assetKind, 's');

        return in_array($assetKind, ['image', 'video', 'audio', 'document'], true) ? $assetKind : '';
    }

    private function assertAssetKind(string $fileId, string $expectedKind, string $fieldKey): void
    {
        $file = $this->files->getFileById($fileId);
        if (!is_array($file)) {
            throw new InvalidArgumentException('Referenced file does not exist for field: ' . $fieldKey . '.');
        }
        $actualKind = (string) ($file['media_class'] ?? '');
        if ($actualKind !== $expectedKind) {
            throw new InvalidArgumentException('Field expects a ' . $expectedKind . ' asset but received ' . $actualKind . ': ' . $fieldKey . '.');
        }
    }

    private function normalizeType(string $type): string
    {
        $normalized = strtolower(trim($type));
        return match ($normalized) {
            'asset:icon', 'file:icon', 'media:icon' => 'icon',
            'asset:image', 'file:image', 'media:image' => 'image',
            'asset:video', 'file:video', 'media:video' => 'video',
            'asset:audio', 'file:audio', 'media:audio' => 'audio',
            'asset:document', 'file:document', 'media:document' => 'document',
            'text', 'textarea', 'url', 'icon', 'image', 'video', 'audio', 'document', 'number', 'boolean' => $normalized,
            default => 'text',
        };
    }
}
