<?php

declare(strict_types=1);

namespace Builder\Core\Foundation\Revisions;

use Illuminate\Support\Arr;

/**
 * Computes structured diffs between two snapshots of revision data.
 *
 * Each method returns only the changed parts so the caller can decide
 * which diff types are relevant for their use case.
 */
final class RevisionDiff
{
    public const string KEY_TRANSLATIONS = '_translations';

    public const string KEY_BLOCKS = '_blocks';

    public const string KEY_MEDIA = '_media';

    public const string KEY_RESTORED_FROM = '_restored_from';

    private const array FIELD_SKIP_KEYS = ['id', 'created_at', 'updated_at', self::KEY_RESTORED_FROM];

    private const array TRANSLATION_SKIP_KEYS = ['id', 'created_at', 'updated_at', self::KEY_BLOCKS];

    /**
     * @param  array<string, mixed>  $current
     * @param  array<string, mixed>  $previous
     */
    public function __construct(
        private readonly array $current,
        private readonly array $previous,
    ) {}

    /**
     * Diff for scalar fields on the root record.
     * Returns only changed fields with 'old' and 'new' values.
     *
     * @return array<string, array{old: mixed, new: mixed}>
     */
    public function fields(): array
    {
        $skip = array_merge(self::FIELD_SKIP_KEYS, [self::KEY_TRANSLATIONS, self::KEY_BLOCKS, self::KEY_MEDIA]);

        $current = Arr::except($this->current, $skip);
        $previous = Arr::except($this->previous, $skip);

        $diff = [];

        foreach ($current as $key => $value) {
            $oldValue = $previous[$key] ?? null;

            if ($value !== $oldValue) {
                $diff[$key] = ['old' => $oldValue, 'new' => $value];
            }
        }

        foreach (array_keys($previous) as $key) {
            if (! array_key_exists($key, $current)) {
                $diff[$key] = ['old' => $previous[$key], 'new' => null];
            }
        }

        return $diff;
    }

    /**
     * Per-locale diff for translatable fields.
     *
     * @return array<string, array<string, array{old: mixed, new: mixed}>>
     */
    public function translations(): array
    {
        $currentTranslations = $this->current[self::KEY_TRANSLATIONS] ?? [];
        $previousTranslations = $this->previous[self::KEY_TRANSLATIONS] ?? [];

        $diff = [];

        foreach ($currentTranslations as $locale => $fields) {
            $prevFields = $previousTranslations[$locale] ?? [];
            $localeDiff = [];

            foreach ($fields as $key => $value) {
                if (in_array($key, self::TRANSLATION_SKIP_KEYS, true)) {
                    continue;
                }

                $oldValue = $prevFields[$key] ?? null;

                if ($value !== $oldValue) {
                    $localeDiff[$key] = ['old' => $oldValue, 'new' => $value];
                }
            }

            if ($localeDiff !== []) {
                $diff[$locale] = $localeDiff;
            }
        }

        return $diff;
    }

    /**
     * Diff for media attachment collections.
     *
     * @return array<string, array{old: list<int>, new: list<int>}>
     */
    public function media(): array
    {
        /** @var array<string, list<int>> $currentMedia */
        $currentMedia = $this->current[self::KEY_MEDIA] ?? [];
        /** @var array<string, list<int>> $previousMedia */
        $previousMedia = $this->previous[self::KEY_MEDIA] ?? [];

        $diff = [];
        $allCollections = array_unique(array_merge(array_keys($currentMedia), array_keys($previousMedia)));

        foreach ($allCollections as $collection) {
            $current = $currentMedia[$collection] ?? [];
            $previous = $previousMedia[$collection] ?? [];

            if ($current === $previous) {
                continue;
            }

            $diff[$collection] = ['old' => $previous, 'new' => $current];
        }

        return $diff;
    }

    /**
     * Coarse block diff: whole-collection old/new arrays for changed collections.
     *
     * @return array<string, array{old: list<array{key: string, data: array<string, mixed>}>, new: list<array{key: string, data: array<string, mixed>}>}>
     */
    public function blocks(): array
    {
        return $this->compareBlockCollections(
            $this->current[self::KEY_BLOCKS] ?? [],
            $this->previous[self::KEY_BLOCKS] ?? [],
        );
    }

    /**
     * Coarse per-locale block diff.
     *
     * @return array<string, array<string, array{old: list<array{key: string, data: array<string, mixed>}>, new: list<array{key: string, data: array<string, mixed>}>}>>
     */
    public function translationBlocks(): array
    {
        $diff = [];
        $currentTranslations = $this->current[self::KEY_TRANSLATIONS] ?? [];
        $previousTranslations = $this->previous[self::KEY_TRANSLATIONS] ?? [];

        foreach ($currentTranslations as $locale => $fields) {
            $localeDiff = $this->compareBlockCollections(
                $fields[self::KEY_BLOCKS] ?? [],
                $previousTranslations[$locale][self::KEY_BLOCKS] ?? [],
            );

            if ($localeDiff !== []) {
                $diff[$locale] = $localeDiff;
            }
        }

        return $diff;
    }

    /**
     * Granular position-based block diff for non-translatable records.
     *
     * @return array<string, list<array{action: 'added'|'removed'|'modified', position: int, key: string, data_diff?: array<string, array{old: mixed, new: mixed}>, media_diff?: array<string, array{old: list<int>, new: list<int>}>, data?: array<string, mixed>, media?: array<string, mixed>}>>
     */
    public function blockGranular(): array
    {
        return $this->computeGranularBlockCollections(
            $this->current[self::KEY_BLOCKS] ?? [],
            $this->previous[self::KEY_BLOCKS] ?? [],
        );
    }

    /**
     * Granular position-based block diff per locale.
     *
     * @return array<string, array<string, list<array{action: 'added'|'removed'|'modified', position: int, key: string, data_diff?: array<string, array{old: mixed, new: mixed}>, media_diff?: array<string, array{old: list<int>, new: list<int>}>, data?: array<string, mixed>, media?: array<string, mixed>}>>>
     */
    public function translationBlockGranular(): array
    {
        $diff = [];
        $currentTranslations = $this->current[self::KEY_TRANSLATIONS] ?? [];
        $previousTranslations = $this->previous[self::KEY_TRANSLATIONS] ?? [];

        foreach ($currentTranslations as $locale => $fields) {
            $localeDiff = $this->computeGranularBlockCollections(
                $fields[self::KEY_BLOCKS] ?? [],
                $previousTranslations[$locale][self::KEY_BLOCKS] ?? [],
            );

            if ($localeDiff !== []) {
                $diff[$locale] = $localeDiff;
            }
        }

        return $diff;
    }

    /**
     * @param  array<string, list<array<string, mixed>>>  $current
     * @param  array<string, list<array<string, mixed>>>  $previous
     * @return array<string, array{old: list<array{key: string, data: array<string, mixed>}>, new: list<array{key: string, data: array<string, mixed>}>}>
     */
    private function compareBlockCollections(array $current, array $previous): array
    {
        $diff = [];
        $allCollections = array_unique(array_merge(array_keys($current), array_keys($previous)));

        foreach ($allCollections as $collection) {
            $normalizedCurrent = $this->normalizeBlocks($current[$collection] ?? []);
            $normalizedPrevious = $this->normalizeBlocks($previous[$collection] ?? []);

            if ($normalizedCurrent === $normalizedPrevious) {
                continue;
            }

            $diff[$collection] = [
                'old' => $normalizedPrevious,
                'new' => $normalizedCurrent,
            ];
        }

        return $diff;
    }

    /**
     * Strip the `order` key so comparisons are order-agnostic.
     *
     * @param  list<array<string, mixed>>  $blocks
     * @return list<array{key: string, data: array<string, mixed>, media: array<string, list<int>>}>
     */
    private function normalizeBlocks(array $blocks): array
    {
        return array_map(
            fn (array $b): array => [
                'key' => (string) ($b['key'] ?? ''),
                'data' => (array) ($b['data'] ?? []),
                'media' => (array) ($b['media'] ?? []),
            ],
            $blocks,
        );
    }

    /**
     * @param  array<string, list<array<string, mixed>>>  $current
     * @param  array<string, list<array<string, mixed>>>  $previous
     * @return array<string, list<array{action: 'added'|'removed'|'modified', position: int, key: string, data_diff?: array<string, array{old: mixed, new: mixed}>, media_diff?: array<string, array{old: list<int>, new: list<int>}>, data?: array<string, mixed>, media?: array<string, mixed>}>>
     */
    private function computeGranularBlockCollections(array $current, array $previous): array
    {
        $diff = [];
        $allCollections = array_unique(array_merge(array_keys($current), array_keys($previous)));

        foreach ($allCollections as $collection) {
            $newBlocks = $current[$collection] ?? [];
            $oldBlocks = $previous[$collection] ?? [];
            $maxLen = max(count($newBlocks), count($oldBlocks));
            $entries = [];

            for ($i = 0; $i < $maxLen; $i++) {
                $hasNew = isset($newBlocks[$i]);
                $hasOld = isset($oldBlocks[$i]);

                if ($hasNew && ! $hasOld) {
                    $normNew = $this->normalizeBlocks([$newBlocks[$i]])[0];
                    $entries[] = [
                        'action' => 'added',
                        'position' => $i,
                        'key' => $normNew['key'],
                        'data' => $normNew['data'],
                        'media' => $normNew['media'],
                    ];

                    continue;
                }

                if ($hasOld && ! $hasNew) {
                    $normOld = $this->normalizeBlocks([$oldBlocks[$i]])[0];
                    $entries[] = [
                        'action' => 'removed',
                        'position' => $i,
                        'key' => $normOld['key'],
                        'data' => $normOld['data'],
                        'media' => $normOld['media'],
                    ];

                    continue;
                }

                $normNew = $this->normalizeBlocks([$newBlocks[$i]])[0];
                $normOld = $this->normalizeBlocks([$oldBlocks[$i]])[0];

                if ($normNew === $normOld) {
                    continue;
                }

                $dataDiff = [];
                $allDataKeys = array_unique(array_merge(array_keys($normNew['data']), array_keys($normOld['data'])));

                foreach ($allDataKeys as $dataKey) {
                    $newVal = $normNew['data'][$dataKey] ?? null;
                    $oldVal = $normOld['data'][$dataKey] ?? null;

                    if ($newVal !== $oldVal) {
                        $dataDiff[$dataKey] = ['old' => $oldVal, 'new' => $newVal];
                    }
                }

                $mediaDiff = [];
                $allMediaKeys = array_unique(array_merge(array_keys($normNew['media']), array_keys($normOld['media'])));

                foreach ($allMediaKeys as $mediaKey) {
                    $newIds = (array) ($normNew['media'][$mediaKey] ?? []);
                    $oldIds = (array) ($normOld['media'][$mediaKey] ?? []);

                    if ($newIds !== $oldIds) {
                        $mediaDiff[$mediaKey] = ['old' => $oldIds, 'new' => $newIds];
                    }
                }

                $entries[] = [
                    'action' => 'modified',
                    'position' => $i,
                    'key' => $normNew['key'],
                    'data_diff' => $dataDiff,
                    'media_diff' => $mediaDiff,
                ];
            }

            if ($entries !== []) {
                $diff[$collection] = $entries;
            }
        }

        return $diff;
    }
}
