<?php

// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedNamespaceFound -- APD is the established namespace shared with the Pro add-on and retained for backward compatibility.
namespace APD\Runtime;

defined('ABSPATH') || exit;

class RuntimeBootPayloadResolver
{
    public function resolve(mixed $document, mixed $state) : array
    {
        $document = is_array($document) ? $this->normalizeDocumentPayloadForRuntime($document) : [];
        $state = is_array($state) ? $this->normalizeStatePayloadForRuntime($state) : [];

        if ($document !== [] && $this->payloadHasViews($document)) {
            if ($state === [] || ! $this->payloadHasViews($state)) {
                $state = $document;
            } else {
                $state = $this->mergeCanonicalDocumentState($state, $document);
            }
        }

        if ($state !== [] && $this->payloadHasViews($state)) {
            return [
                'initialDocument' => null,
                'initialState' => $state,
            ];
        }

        return [
            'initialDocument' => null,
            'initialState' => null,
        ];
    }



    private function shouldPreferStateBoot(mixed $state, int $stateObjectCount, int $documentObjectCount) : bool
    {
        if (! is_array($state) || $state === []) {
            return false;
        }

        // Element instances are backed by the Elements catalog/variations. The
        // document-only export can contain the Fabric object, but not the catalog
        // entry needed to restore/edit it reliably. In that case the modular/full
        // state is the canonical boot payload.
        if ($this->payloadHasElementCatalog($state)) {
            return true;
        }

        // Complete sessions are canonical when they cover at least the same
        // document objects as the document-only fallback.
        if (
            in_array((string) ($state['format'] ?? ''), ['asuka-state', 'asuka-modular-state'], true)
            && $stateObjectCount >= $documentObjectCount
        ) {
            return true;
        }

        return $documentObjectCount <= 0 && $stateObjectCount > 0;
    }

    private function payloadHasElementCatalog(mixed $payload) : bool
    {
        if (! is_array($payload) || $payload === []) {
            return false;
        }

        if (isset($payload['catalog']) && is_array($payload['catalog'])) {
            $elements = $payload['catalog']['elements'] ?? null;
            if (is_array($elements) && $elements !== []) {
                return true;
            }
        }

        if (isset($payload['elements']) && is_array($payload['elements']) && $payload['elements'] !== []) {
            return true;
        }

        foreach (['state', 'initialState', 'document', 'documentState', 'initialDocument', 'legacy'] as $key) {
            if (isset($payload[$key]) && $this->payloadHasElementCatalog($payload[$key])) {
                return true;
            }
        }

        return false;
    }

    private function payloadHasViews(mixed $payload) : bool
    {
        if (! is_array($payload) || $payload === []) {
            return false;
        }

        $views = $payload['views'] ?? null;
        if (is_array($views)) {
            if (array_is_list($views) && $views !== []) {
                return true;
            }
            if (isset($views['views']) && is_array($views['views']) && $views['views'] !== []) {
                return true;
            }
        }

        foreach (['document', 'initialDocument', 'state', 'legacy'] as $key) {
            if (isset($payload[$key]) && $this->payloadHasViews($payload[$key])) {
                return true;
            }
        }

        return false;
    }

    private function normalizeStatePayloadForRuntime(array $payload) : array
    {
        if ($payload === []) {
            return [];
        }

        if (($payload['format'] ?? '') === 'asuka-modular-state' && (int) ($payload['schemaVersion'] ?? 0) === 1) {
            $payload['format'] = 'asuka-state';
            $payload['schemaVersion'] = 2;
            if (isset($payload['meta']) && is_array($payload['meta'])) {
                $payload['meta']['app'] = 'AsukaDesigner';
                $payload['meta']['schemaVersion'] = 2;
                if (isset($payload['exportedAt']) && is_scalar($payload['exportedAt'])) {
                    $payload['meta']['exportedAt'] = (string) $payload['exportedAt'];
                }
            }
        }

        if (($payload['format'] ?? '') !== 'asuka-state' || (int) ($payload['schemaVersion'] ?? 0) !== 2) {
            if (! $this->payloadHasViews($payload)) {
                return [];
            }
            if (! isset($payload['document']) || ! is_array($payload['document'])) {
                return $this->normalizeDocumentPayloadForRuntime($payload);
            }
            $payload['format'] = 'asuka-state';
            $payload['schemaVersion'] = 2;
        }

        if (isset($payload['document']) && is_array($payload['document']) && $this->isCanonicalStateEnvelope($payload['document'])) {
            $nested = $payload['document'];
            $payload['document'] = isset($nested['document']) && is_array($nested['document']) ? $nested['document'] : [];
            if (isset($nested['catalog']) && is_array($nested['catalog'])) {
                $payload['catalog'] = array_replace_recursive(
                    isset($payload['catalog']) && is_array($payload['catalog']) ? $payload['catalog'] : [],
                    $nested['catalog']
                );
            }
        }

        $payload['scope'] = isset($payload['scope']) && is_array($payload['scope']) ? $payload['scope'] : [];
        $payload['scope']['document'] = array_replace(
            isset($payload['scope']['document']) && is_array($payload['scope']['document']) ? $payload['scope']['document'] : [],
            $this->canonicalDocumentScope()
        );

        return $this->removeCanonicalRootDocumentAliases($payload);
    }

    private function normalizeDocumentPayloadForRuntime(array $document) : array
    {
        if ($document === []) {
            return [];
        }

        if ($this->isCanonicalStateEnvelope($document)) {
            if (isset($document['document']) && is_array($document['document']) && $this->isCanonicalStateEnvelope($document['document'])) {
                $nested = $document['document'];
                $document['document'] = isset($nested['document']) && is_array($nested['document']) ? $nested['document'] : [];
                if (isset($nested['catalog']) && is_array($nested['catalog'])) {
                    $document['catalog'] = array_replace_recursive(
                        isset($document['catalog']) && is_array($document['catalog']) ? $document['catalog'] : [],
                        $nested['catalog']
                    );
                }
            }
            $document['scope'] = isset($document['scope']) && is_array($document['scope']) ? $document['scope'] : [];
            $document['scope']['document'] = array_replace(
                isset($document['scope']['document']) && is_array($document['scope']['document']) ? $document['scope']['document'] : [],
                $this->canonicalDocumentScope()
            );
            return $this->removeCanonicalRootDocumentAliases($document);
        }

        if (! $this->payloadHasViews($document) && isset($document['document']) && is_array($document['document'])) {
            $document = $document['document'];
        } elseif (! $this->payloadHasViews($document) && isset($document['initialDocument']) && is_array($document['initialDocument'])) {
            $document = $document['initialDocument'];
        }

        $views = $document['views'] ?? [];
        if (is_array($views) && isset($views['views']) && is_array($views['views'])) {
            $viewList = array_values($views['views']);
            $activeViewId = (string) ($views['activeViewId'] ?? ($document['activeViewId'] ?? ''));
        } elseif (is_array($views) && array_is_list($views)) {
            $viewList = array_values($views);
            $activeViewId = (string) ($document['activeViewId'] ?? '');
        } else {
            $viewList = [];
            $activeViewId = (string) ($document['activeViewId'] ?? '');
        }

        if ($activeViewId === '' && isset($viewList[0]) && is_array($viewList[0])) {
            $activeViewId = (string) ($viewList[0]['id'] ?? '');
        }

        $snapshotsByView = $this->payloadSnapshotsByView($document);
        $objectsByView = $this->deriveObjectsByViewFromSnapshots($snapshotsByView, $this->payloadObjectsByView($document));
        $rootObjects = method_exists($this, 'payloadRootObjects') ? $this->payloadRootObjects($document) : [];
        if ($rootObjects !== []) {
            $viewId = $activeViewId !== '' ? $activeViewId : $this->payloadActiveViewId($document);
            if (! isset($objectsByView[$viewId]) || ! is_array($objectsByView[$viewId]) || $objectsByView[$viewId] === []) {
                $objectsByView[$viewId] = $rootObjects;
            }
        }

        $canonical = [
            'views' => [
                'views' => $viewList,
                'activeViewId' => $activeViewId,
            ],
            'activeViewId' => $activeViewId,
        ];

        if ($objectsByView !== []) {
            $canonical['objects'] = ['byView' => $objectsByView];
        }
        if (isset($document['viewportByView']) && is_array($document['viewportByView'])) {
            $canonical['viewportByView'] = $document['viewportByView'];
        } elseif (isset($document['documentViewportByView']) && is_array($document['documentViewportByView'])) {
            $canonical['viewportByView'] = $document['documentViewportByView'];
        }
        if (isset($document['selection']) && is_array($document['selection'])) {
            $canonical['selection'] = $document['selection'];
        }
        if (isset($document['drawingAreas']) && is_array($document['drawingAreas'])) {
            $canonical['drawingAreas'] = $document['drawingAreas'];
            if (isset($document['drawingAreas']['byView']) && is_array($document['drawingAreas']['byView'])) {
                $canonical['drawingAreasByView'] = $document['drawingAreas']['byView'];
                $canonical['drawingZones'] = $this->flattenDrawingAreasByView($document['drawingAreas']['byView']);
            } elseif (isset($document['drawingAreas']['zones']) && is_array($document['drawingAreas']['zones'])) {
                $canonical['drawingZones'] = $this->normalizeDrawingZoneList(array_values(array_filter($document['drawingAreas']['zones'], 'is_array')), $activeViewId);
                $canonical['drawingAreasByView'] = $this->groupDrawingZonesByView($canonical['drawingZones'], $activeViewId);
                $canonical['drawingAreas']['byView'] = $canonical['drawingAreasByView'];
            }
        } elseif (isset($document['drawingAreasByView']) && is_array($document['drawingAreasByView'])) {
            $canonical['drawingAreasByView'] = $document['drawingAreasByView'];
            $canonical['drawingZones'] = $this->flattenDrawingAreasByView($document['drawingAreasByView']);
            $canonical['drawingAreas'] = [
                'byView' => $document['drawingAreasByView'],
                'zones' => isset($document['drawingAreasByView'][$activeViewId]) && is_array($document['drawingAreasByView'][$activeViewId]) ? $document['drawingAreasByView'][$activeViewId] : $canonical['drawingZones'],
                'activeDrawingAreaId' => $document['activeDrawingAreaId'] ?? null,
            ];
        } elseif (isset($document['drawingZones']) && is_array($document['drawingZones'])) {
            $canonical['drawingZones'] = $this->normalizeDrawingZoneList(array_values(array_filter($document['drawingZones'], 'is_array')), $activeViewId);
            $canonical['drawingAreasByView'] = $this->groupDrawingZonesByView($canonical['drawingZones'], $activeViewId);
            $canonical['drawingAreas'] = [
                'byView' => $canonical['drawingAreasByView'],
                'zones' => isset($canonical['drawingAreasByView'][$activeViewId]) ? $canonical['drawingAreasByView'][$activeViewId] : $canonical['drawingZones'],
                'activeDrawingAreaId' => $document['activeDrawingAreaId'] ?? null,
            ];
        }
        if (isset($document['elements']) && is_array($document['elements']) && $document['elements'] !== []) {
            $canonical['elements'] = array_values($document['elements']);
        }
        if (isset($document['fixedSlots']) && is_array($document['fixedSlots']) && $document['fixedSlots'] !== []) {
            $canonical['fixedSlots'] = array_values($document['fixedSlots']);
            $canonical['fixedSlotActiveId'] = $document['fixedSlotActiveId'] ?? null;
        }
        $scenes3d = $document['scenes3d'] ?? ($document['document3dScenesByView'] ?? null);
        if (is_array($scenes3d) && $scenes3d !== []) {
            $canonical['scenes3d'] = $scenes3d;
        }
        if (isset($document['asukaDocument']) && is_array($document['asukaDocument']) && $document['asukaDocument'] !== []) {
            $canonical['asukaDocument'] = $document['asukaDocument'];
        }
        if (isset($document['meta']) && is_array($document['meta'])) {
            $canonical['meta'] = $document['meta'];
        }
        if (isset($document['__apdSubmitProof']) && is_array($document['__apdSubmitProof'])) {
            $canonical['__apdSubmitProof'] = $document['__apdSubmitProof'];
        }

        $envelope = [
            'format' => 'asuka-state',
            'schemaVersion' => 2,
            'exportedAt' => gmdate('c'),
            'scope' => [
                'document' => $this->canonicalDocumentScope(),
                'catalog' => ['elements' => true],
            ],
            'document' => $canonical,
        ];
        if (isset($canonical['elements']) && is_array($canonical['elements']) && $canonical['elements'] !== []) {
            $envelope['catalog'] = ['elements' => array_values($canonical['elements'])];
        }

        return $envelope;
    }

    private function isCanonicalStateEnvelope(array $payload) : bool
    {
        return ($payload['format'] ?? '') === 'asuka-state'
            && (int) ($payload['schemaVersion'] ?? 0) === 2;
    }

    /**
     * @return array<string, bool>
     */
    private function canonicalDocumentScope() : array
    {
        return [
            'views' => true,
            'drawingAreas' => true,
            'snapshots' => true,
            'objects' => true,
            'selection' => true,
            'viewportByView' => true,
            'scenes3d' => true,
            'asukaDocument' => true,
        ];
    }

    private function mergeCanonicalDocumentState(array $state, array $documentState) : array
    {
        $document = isset($documentState['document']) && is_array($documentState['document'])
            ? $documentState['document']
            : [];
        if ($this->isCanonicalStateEnvelope($document)) {
            $document = isset($document['document']) && is_array($document['document']) ? $document['document'] : [];
        }
        $state['document'] = $document;
        $state['scope'] = isset($state['scope']) && is_array($state['scope']) ? $state['scope'] : [];
        $state['scope']['document'] = array_replace(
            isset($state['scope']['document']) && is_array($state['scope']['document']) ? $state['scope']['document'] : [],
            $this->canonicalDocumentScope()
        );

        if (isset($documentState['catalog']) && is_array($documentState['catalog'])) {
            $state['catalog'] = array_replace_recursive(
                isset($state['catalog']) && is_array($state['catalog']) ? $state['catalog'] : [],
                $documentState['catalog']
            );
            $state['scope']['catalog'] = isset($state['scope']['catalog']) && is_array($state['scope']['catalog'])
                ? $state['scope']['catalog']
                : [];
            $state['scope']['catalog']['elements'] = true;
        }

        return $this->removeCanonicalRootDocumentAliases($state);
    }

    private function removeCanonicalRootDocumentAliases(array $payload) : array
    {
        if (! $this->isCanonicalStateEnvelope($payload)) {
            return $payload;
        }

        unset(
            $payload['views'],
            $payload['activeViewId'],
            $payload['objects'],
            $payload['objectsByView'],
            $payload['documentObjectsByView'],
            $payload['byView'],
            $payload['drawingAreas'],
            $payload['drawingAreasByView'],
            $payload['drawingZones'],
            $payload['snapshots'],
            $payload['snapshotsByView'],
            $payload['viewSnapshots'],
            $payload['nativeSnapshots'],
            $payload['selection'],
            $payload['viewportByView'],
            $payload['documentViewportByView'],
            $payload['scenes3d'],
            $payload['asukaDocument'],
            $payload['documentState'],
            $payload['initialDocument']
        );

        return $payload;
    }


    private function flattenDrawingAreasByView(array $byView) : array
    {
        $zones = [];
        foreach ($byView as $viewId => $items) {
            if (! is_array($items)) {
                continue;
            }
            foreach ($items as $item) {
                if (! is_array($item)) {
                    continue;
                }
                if (! isset($item['viewId']) && ! isset($item['view'])) {
                    $item['viewId'] = (string) $viewId;
                }
                $zones[] = $item;
            }
        }

        return $zones;
    }

    private function normalizeDrawingZoneList(array $zones, string $fallbackViewId = '') : array
    {
        $normalized = [];
        foreach ($zones as $zone) {
            if (! is_array($zone)) {
                continue;
            }
            $item = $zone;
            $viewId = $item['viewId'] ?? $item['view'] ?? $item['view_id'] ?? $fallbackViewId;
            $viewId = is_scalar($viewId) ? trim((string) $viewId) : '';
            if ($viewId !== '') {
                $item['viewId'] = $viewId;
            }
            $id = $item['id'] ?? $item['uid'] ?? '';
            if (! is_scalar($id) || trim((string) $id) === '') {
                $item['id'] = 'drawing-area-' . ($viewId !== '' ? $viewId . '-' : '') . substr(md5(wp_json_encode($item) ?: uniqid('', true)), 0, 12);
            }
            if (! isset($item['left']) && isset($item['x'])) {
                $item['left'] = $item['x'];
            }
            if (! isset($item['top']) && isset($item['y'])) {
                $item['top'] = $item['y'];
            }
            if (! isset($item['x']) && isset($item['left'])) {
                $item['x'] = $item['left'];
            }
            if (! isset($item['y']) && isset($item['top'])) {
                $item['y'] = $item['top'];
            }
            foreach (['left', 'top', 'x', 'y', 'width', 'height'] as $key) {
                if (isset($item[$key]) && is_numeric($item[$key])) {
                    $item[$key] = $item[$key] + 0;
                }
            }
            $normalized[] = $item;
        }
        return $normalized;
    }

    private function groupDrawingZonesByView(array $zones, string $fallbackViewId = '') : array
    {
        $byView = [];
        foreach ($this->normalizeDrawingZoneList($zones, $fallbackViewId) as $zone) {
            $viewId = $zone['viewId'] ?? $fallbackViewId;
            $viewId = is_scalar($viewId) && trim((string) $viewId) !== '' ? trim((string) $viewId) : '__default__';
            $zone['viewId'] = $viewId;
            if (! isset($byView[$viewId])) {
                $byView[$viewId] = [];
            }
            $byView[$viewId][] = $zone;
        }
        return $byView;
    }


    private function payloadActiveViewId(array $payload) : string
    {
        $activeViewId = (string) ($payload['activeViewId'] ?? '');
        $views = $payload['views'] ?? null;

        if ($activeViewId === '' && is_array($views) && isset($views['activeViewId'])) {
            $activeViewId = (string) $views['activeViewId'];
        }

        if ($activeViewId === '') {
            $viewList = [];
            if (is_array($views) && array_is_list($views)) {
                $viewList = $views;
            } elseif (is_array($views) && isset($views['views']) && is_array($views['views'])) {
                $viewList = $views['views'];
            }

            if (isset($viewList[0]) && is_array($viewList[0])) {
                $activeViewId = (string) ($viewList[0]['id'] ?? '');
            }
        }

        return $activeViewId !== '' ? $activeViewId : 'view-1';
    }

    private function payloadRootObjects(array $payload) : array
    {
        $canvas = isset($payload['canvas']) && is_array($payload['canvas']) ? $payload['canvas'] : [];

        if (isset($payload['objects']) && is_array($payload['objects']) && array_is_list($payload['objects'])) {
            return $payload['objects'];
        }

        if (isset($payload['objects']['objects']) && is_array($payload['objects']['objects']) && array_is_list($payload['objects']['objects'])) {
            return $payload['objects']['objects'];
        }

        if (isset($canvas['objects']) && is_array($canvas['objects']) && array_is_list($canvas['objects'])) {
            return $canvas['objects'];
        }

        if (isset($canvas['objects']['objects']) && is_array($canvas['objects']['objects']) && array_is_list($canvas['objects']['objects'])) {
            return $canvas['objects']['objects'];
        }

        return [];
    }

    private function payloadObjectsByView(array $payload) : array
    {
        $canvas = isset($payload['canvas']) && is_array($payload['canvas']) ? $payload['canvas'] : [];
        $candidates = [
            (isset($payload['objects']) && is_array($payload['objects']) && ! array_is_list($payload['objects'])) ? ($payload['objects']['byView'] ?? null) : null,
            $payload['documentObjectsByView'] ?? null,
            $payload['objectsByView'] ?? null,
            $payload['byView'] ?? null,
            (isset($canvas['objects']) && is_array($canvas['objects']) && ! array_is_list($canvas['objects'])) ? ($canvas['objects']['byView'] ?? null) : null,
            $canvas['documentObjectsByView'] ?? null,
            $canvas['objectsByView'] ?? null,
            $canvas['byView'] ?? null,
        ];

        $objectsByView = [];
        foreach ($candidates as $candidate) {
            if (is_array($candidate) && $candidate !== []) {
                $objectsByView = $candidate;
                break;
            }
        }

        $rootObjects = $this->payloadRootObjects($payload);
        if ($rootObjects !== []) {
            $viewId = $this->payloadActiveViewId($payload);
            if (! isset($objectsByView[$viewId]) || ! is_array($objectsByView[$viewId]) || $objectsByView[$viewId] === []) {
                $objectsByView[$viewId] = $rootObjects;
            }
        }

        return $objectsByView;
    }

    private function payloadSnapshotsByView(array $payload) : array
    {
        $canvas = isset($payload['canvas']) && is_array($payload['canvas']) ? $payload['canvas'] : [];
        $candidates = [
            $payload['snapshotsByView'] ?? null,
            $payload['snapshots']['snapshotsByView'] ?? null,
            $payload['viewSnapshots']['snapshotsByView'] ?? null,
            $payload['viewSnapshots'] ?? null,
            $canvas['snapshotsByView'] ?? null,
            $canvas['snapshots']['snapshotsByView'] ?? null,
            $canvas['viewSnapshots']['snapshotsByView'] ?? null,
            $canvas['viewSnapshots'] ?? null,
        ];

        foreach ($candidates as $candidate) {
            if (is_array($candidate) && $candidate !== []) {
                return $candidate;
            }
        }

        return [];
    }

    private function deriveObjectsByViewFromSnapshots(array $snapshotsByView, array $objectsByView = []) : array
    {
        foreach ($snapshotsByView as $viewId => $snapshot) {
            $key = (string) $viewId;
            if (isset($objectsByView[$key]) && is_array($objectsByView[$key]) && $objectsByView[$key] !== []) {
                continue;
            }
            if (is_array($snapshot) && isset($snapshot['objects']) && is_array($snapshot['objects'])) {
                $objectsByView[$key] = $snapshot['objects'];
            }
        }

        return $objectsByView;
    }

    private function payloadObjectCount(mixed $payload) : int
    {
        if (! is_array($payload) || $payload === []) {
            return 0;
        }

        $count = 0;

        $objectsByView = $this->payloadObjectsByView($payload);
        foreach ($objectsByView as $objects) {
            if (is_array($objects)) {
                $count += count($objects);
            }
        }

        $snapshotsByView = $this->payloadSnapshotsByView($payload);
        foreach ($snapshotsByView as $viewId => $snapshot) {
            $key = (string) $viewId;
            if (isset($objectsByView[$key]) && is_array($objectsByView[$key]) && $objectsByView[$key] !== []) {
                continue;
            }
            if (is_array($snapshot) && isset($snapshot['objects']) && is_array($snapshot['objects'])) {
                $count += count($snapshot['objects']);
            }
        }

        foreach (['document', 'initialDocument', 'documentState', 'state', 'legacy'] as $key) {
            if (isset($payload[$key]) && is_array($payload[$key])) {
                $count += $this->payloadObjectCount($payload[$key]);
            }
        }

        return $count;
    }


}
