<?php

declare(strict_types=1);

namespace Builder\Core\Foundation\Resources;

use Builder\Core\Exceptions\CircularHierarchyException;
use Builder\Core\Foundation\Concerns\HasHierarchy;
use Builder\Core\Foundation\Contracts\BuilderAccessible;
use Builder\Core\Foundation\Contracts\Translatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

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

    /**
     * @param  array<string, mixed>  $validatedData
     */
    public function build(
        ResourceDefinition $definition,
        Model $record,
        array $validatedData,
        ?string $locale,
        string $action,
        BuilderAccessible $user,
    ): ResourceWriteContext {
        $translatableNames = $definition->getTranslatableFieldNames();
        $translatableData = Arr::only($validatedData, $translatableNames);

        $handlerFieldNames = [];

        foreach (array_keys($this->fieldHandlerRegistry->all()) as $componentName) {
            foreach ($definition->getFieldsByComponent($componentName) as $field) {
                $handlerFieldNames[] = $field->getName();
            }
        }

        $baseData = Arr::except($validatedData, array_merge(
            $translatableNames,
            $definition->getVirtualFieldNames(),
            $handlerFieldNames,
        ));

        $existingRecord = $record->exists ? $record : null;

        $baseData = $this->applyGenerateFrom($definition, $baseData, false, $existingRecord, null);

        if ($record->exists && array_key_exists('parent_id', $baseData) && $baseData['parent_id'] !== null) {
            $this->guardCircularHierarchy($record, $baseData['parent_id']);
        }

        return new ResourceWriteContext(
            definition: $definition,
            record: $record,
            baseData: $baseData,
            translatableData: $translatableData,
            requestData: $validatedData,
            locale: $locale,
            action: $action,
            user: $user,
        );
    }

    /**
     * For fields with generateFrom that are empty/null, auto-generate their value
     * from the source field and ensure uniqueness by appending a numeric suffix.
     *
     * @param  array<string, mixed>  $data
     * @return array<string, mixed>
     */
    public function applyGenerateFrom(
        ResourceDefinition $definition,
        array $data,
        bool $translatableFields,
        ?Model $existingRecord,
        ?string $locale,
    ): array {
        $modelInstance = new ($definition->getModel());

        foreach ($definition->getAllFields() as $field) {
            if ($field->isTranslatable() !== $translatableFields) {
                continue;
            }

            $generateFrom = $field->getGenerateFrom();

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

            $fieldName = $field->getName();

            if (! empty($data[$fieldName])) {
                continue;
            }

            $sourceValue = (string) ($data[$generateFrom] ?? '');

            if ($sourceValue === '') {
                continue;
            }

            $base = Str::slug($sourceValue);

            if ($definition->isHierarchical() && $translatableFields && $locale !== null) {
                $parentField = $definition->getParentField();
                $parentId = (int) ($data[$parentField]
                    ?? $existingRecord?->getAttribute($parentField)
                    ?? 0);

                if ($parentId > 0) {
                    $base = $this->prependParentSlug($definition, $parentId, $base, $locale);
                }
            }

            $data[$fieldName] = $this->makeUniqueSlug(
                $modelInstance,
                $fieldName,
                $base,
                $translatableFields,
                $existingRecord,
                $locale,
            );
        }

        return $data;
    }

    private function guardCircularHierarchy(Model $record, mixed $parentId): void
    {
        $uses = class_uses_recursive($record);

        if (! in_array(HasHierarchy::class, $uses, true)) {
            return;
        }

        if ((int) $parentId === (int) $record->getKey()) {
            throw CircularHierarchyException::forParent($parentId);
        }

        /** @phpstan-ignore method.notFound */
        if ($record->descendants()->where($record->getKeyName(), $parentId)->exists()) {
            throw CircularHierarchyException::forParent($parentId);
        }
    }

    private function makeUniqueSlug(
        Model $modelInstance,
        string $fieldName,
        string $base,
        bool $inTranslationsTable,
        ?Model $existingRecord,
        ?string $locale,
    ): string {
        $slug = $base;
        $counter = 1;

        while ($this->slugExists($modelInstance, $fieldName, $slug, $inTranslationsTable, $existingRecord, $locale)) {
            $slug = "{$base}-{$counter}";
            $counter++;
        }

        return $slug;
    }

    private function slugExists(
        Model $modelInstance,
        string $fieldName,
        string $slug,
        bool $inTranslationsTable,
        ?Model $existingRecord,
        ?string $locale,
    ): bool {
        if ($inTranslationsTable && $modelInstance instanceof Translatable) {
            $query = DB::table($modelInstance->getTranslationTable())
                ->where($fieldName, $slug)
                ->where('locale', $locale);

            if ($existingRecord !== null) {
                $query->where($modelInstance->getTranslationForeignKey(), '!=', $existingRecord->getKey());
            }

            return $query->exists();
        }

        $query = DB::table($modelInstance->getTable())
            ->where($fieldName, $slug);

        if ($existingRecord !== null) {
            $query->where('id', '!=', $existingRecord->getKey());
        }

        return $query->exists();
    }

    private function prependParentSlug(
        ResourceDefinition $definition,
        int $parentId,
        string $slug,
        string $locale,
    ): string {
        $model = $definition->getModel();
        $parent = $model::find($parentId);

        if ($parent === null) {
            return $slug;
        }

        $parentSlug = $parent instanceof Translatable
            ? $parent->getTranslation($locale)?->getAttribute('slug')
            : $parent->getAttribute('slug');

        if ($parentSlug === null || $parentSlug === '') {
            return $slug;
        }

        return "{$parentSlug}/{$slug}";
    }
}
