<?php

declare(strict_types=1);

namespace Builder\Core\Foundation\Revisions;

use Builder\Core\Foundation\Contracts\Modules\BlockFieldHandler;
use Builder\Core\Foundation\Contracts\Translatable;
use Builder\Core\Foundation\Resources\FieldHandlerRegistry;
use Builder\Core\Foundation\Resources\ResourceDefinition;
use Builder\Core\Models\Revision;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

final class RevisionService
{
    public function __construct(
        private readonly FieldHandlerRegistry $fieldHandlerRegistry,
    ) {}

    public function capture(
        Model $record,
        ResourceDefinition $definition,
        int|string|null $userId,
        bool $isRestore = false,
        ?int $restoredFrom = null,
    ): Revision {
        $snapshot = $this->computeSnapshot($record, $definition);

        if ($isRestore && $restoredFrom !== null) {
            $snapshot[RevisionDiff::KEY_RESTORED_FROM] = $restoredFrom;
        }

        $revision = Revision::query()->create([
            'revisable_type' => get_class($record),
            'revisable_id' => $record->getKey(),
            'user_id' => $userId,
            'revision_data' => $snapshot,
        ]);

        $this->pruneRecord($record, $definition->getMaxRevisions());

        return $revision;
    }

    public function restore(Revision $revision, Model $record): void
    {
        DB::transaction(function () use ($revision, $record): void {
            $data = $revision->revision_data ?? [];
            $translations = Arr::pull($data, RevisionDiff::KEY_TRANSLATIONS, []);
            $topLevelBlocks = Arr::pull($data, RevisionDiff::KEY_BLOCKS, []);
            $mediaByCollection = Arr::pull($data, RevisionDiff::KEY_MEDIA, []);

            $baseAttributes = Arr::except($data, ['id', 'created_at', 'updated_at', RevisionDiff::KEY_RESTORED_FROM]);

            $record->fill($baseAttributes)->save();

            if ($record instanceof Translatable && $translations !== []) {
                foreach ($translations as $locale => $attrs) {
                    $localeBlocks = Arr::pull($attrs, RevisionDiff::KEY_BLOCKS, []);

                    $record->setTranslation($locale, $attrs);

                    if ($localeBlocks !== []) {
                        $this->syncBlocks($record, $localeBlocks, $locale);
                    }
                }
            }

            if ($topLevelBlocks !== []) {
                $this->syncBlocks($record, $topLevelBlocks);
            }

            if ($mediaByCollection !== [] && method_exists($record, 'syncMedia')) {
                foreach ($mediaByCollection as $collection => $ids) {
                    $record->syncMedia(array_values((array) $ids), (string) $collection);
                }
            }
        });
    }

    /** @return array<string, mixed> */
    public function computeSnapshot(Model $record, ?ResourceDefinition $definition = null): array
    {
        $fresh = $record->fresh() ?? $record;
        $snapshot = $fresh->getAttributes();

        if ($fresh instanceof Translatable) {
            $snapshot[RevisionDiff::KEY_TRANSLATIONS] = $fresh->getTranslations()
                ->mapWithKeys(fn (Model $t): array => [
                    (string) $t->getAttribute('locale') => Arr::except($t->getAttributes(), ['id', 'created_at', 'updated_at']),
                ])
                ->all();
        }

        if ($definition !== null) {
            $this->captureHandlerFieldsIntoSnapshot($fresh, $definition, $snapshot);
        }

        return $snapshot;
    }

    public function pruneRecord(Model $record, int $keep): int
    {
        if ($keep <= 0) {
            return 0;
        }

        $idsToKeep = Revision::query()
            ->where('revisable_type', get_class($record))
            ->where('revisable_id', $record->getKey())
            ->orderByDesc('created_at')
            ->orderByDesc('id')
            ->limit($keep)
            ->pluck('id');

        return Revision::query()
            ->where('revisable_type', get_class($record))
            ->where('revisable_id', $record->getKey())
            ->whereNotIn('id', $idsToKeep)
            ->delete();
    }

    public function pruneModelClass(string $modelClass, int $keep): int
    {
        if ($keep <= 0) {
            return 0;
        }

        $deleted = 0;

        Revision::query()
            ->where('revisable_type', $modelClass)
            ->distinct()
            ->pluck('revisable_id')
            ->chunk(200)
            ->each(function (Collection $chunk) use ($modelClass, $keep, &$deleted): void {
                foreach ($chunk as $recordId) {
                    $idsToKeep = Revision::query()
                        ->where('revisable_type', $modelClass)
                        ->where('revisable_id', $recordId)
                        ->orderByDesc('created_at')
                        ->orderByDesc('id')
                        ->limit($keep)
                        ->pluck('id');

                    $deleted += (int) Revision::query()
                        ->where('revisable_type', $modelClass)
                        ->where('revisable_id', $recordId)
                        ->whereNotIn('id', $idsToKeep)
                        ->delete();
                }
            });

        return $deleted;
    }

    /**
     * Dispatch each registered field handler to snapshot its field data.
     *
     * @param  array<string, mixed>  $snapshot
     */
    private function captureHandlerFieldsIntoSnapshot(Model $fresh, ResourceDefinition $definition, array &$snapshot): void
    {
        foreach ($this->fieldHandlerRegistry->all() as $componentName => $handlerInterface) {
            $handler = $this->fieldHandlerRegistry->resolve($componentName);

            if ($handler === null) {
                continue;
            }

            $fieldDefs = $definition->getFieldDefinitionsByComponent($componentName);

            if ($fieldDefs === []) {
                continue;
            }

            $fieldNames = array_keys($fieldDefs);
            $isBlockHandler = $handler instanceof BlockFieldHandler;
            $fieldsMap = array_fill_keys($fieldNames, []);

            if ($fresh instanceof Translatable) {
                foreach ($fresh->getTranslations() as $translation) {
                    $locale = (string) $translation->getAttribute('locale');
                    $handler->loadFields($fresh, $fieldsMap, $locale);

                    if ($isBlockHandler) {
                        $localeBlocks = [];

                        foreach ($fieldNames as $collection) {
                            $rawBlocks = (array) ($fresh->getAttribute($collection) ?? []);

                            if ($rawBlocks !== []) {
                                $localeBlocks[$collection] = array_map(
                                    fn (array $b, int $order): array => [
                                        'key' => (string) ($b['key'] ?? ''),
                                        'data' => (array) ($b['data'] ?? []),
                                        'media' => (array) ($b['media'] ?? []),
                                        'order' => $order,
                                    ],
                                    $rawBlocks,
                                    array_keys($rawBlocks),
                                );
                            }
                        }

                        if ($localeBlocks !== []) {
                            $snapshot[RevisionDiff::KEY_TRANSLATIONS][$locale][RevisionDiff::KEY_BLOCKS] = $localeBlocks;
                        }
                    } else {
                        foreach ($fieldNames as $fieldName) {
                            $ids = $this->extractMediaIds($fresh, $fieldName);
                            $collection = (string) ($fieldDefs[$fieldName]['meta']['collection'] ?? $fieldName);

                            if ($ids !== []) {
                                $snapshot[RevisionDiff::KEY_MEDIA]["{$collection}_{$locale}"] = $ids;
                            }
                        }
                    }
                }
            } else {
                $handler->loadFields($fresh, $fieldsMap);

                if ($isBlockHandler) {
                    $topBlocks = [];

                    foreach ($fieldNames as $collection) {
                        $rawBlocks = (array) ($fresh->getAttribute($collection) ?? []);

                        if ($rawBlocks !== []) {
                            $topBlocks[$collection] = array_map(
                                fn (array $b, int $order): array => [
                                    'key' => (string) ($b['key'] ?? ''),
                                    'data' => (array) ($b['data'] ?? []),
                                    'media' => (array) ($b['media'] ?? []),
                                    'order' => $order,
                                ],
                                $rawBlocks,
                                array_keys($rawBlocks),
                            );
                        }
                    }

                    if ($topBlocks !== []) {
                        $snapshot[RevisionDiff::KEY_BLOCKS] = $topBlocks;
                    }
                } else {
                    foreach ($fieldNames as $fieldName) {
                        $ids = $this->extractMediaIds($fresh, $fieldName);
                        $collection = (string) ($fieldDefs[$fieldName]['meta']['collection'] ?? $fieldName);

                        if ($ids !== []) {
                            $snapshot[RevisionDiff::KEY_MEDIA][$collection] = $ids;
                        }
                    }
                }
            }
        }
    }

    /**
     * Sync block collections back via the registered block handler.
     *
     * @param  array<string, list<array<string, mixed>>>  $blocksByCollection
     */
    private function syncBlocks(Model $record, array $blocksByCollection, ?string $locale = null): void
    {
        foreach ($this->fieldHandlerRegistry->all() as $componentName => $handlerInterface) {
            $handler = $this->fieldHandlerRegistry->resolve($componentName);

            if ($handler === null || ! $handler instanceof BlockFieldHandler) {
                continue;
            }

            $blockFields = array_fill_keys(array_keys($blocksByCollection), []);

            $requestData = array_map(
                fn (array $blocks): array => array_map(
                    fn (array $b): array => [
                        'key' => $b['key'],
                        'data' => $b['data'] ?? [],
                        'media' => $b['media'] ?? [],
                    ],
                    $blocks,
                ),
                $blocksByCollection,
            );

            $handler->syncFields($record, $blockFields, $requestData, $locale);
        }
    }

    /** @return list<int> */
    private function extractMediaIds(Model $record, string $fieldName): array
    {
        return array_values(array_map(
            'intval',
            array_filter(
                (array) ($record->getAttribute($fieldName) ?? []),
                fn (mixed $id): bool => $id !== null && $id !== '',
            ),
        ));
    }
}
