<?php

declare(strict_types=1);

namespace Builder\Core\Foundation\Concerns;

use Builder\Core\Models\Language;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;

/**
 * @property list<string> $translatable
 *
 * @phpstan-require-extends Model
 */
trait HasTranslations
{
    /** @var array<string, Model|null> */
    private array $translationCache = [];

    public function getTranslationTable(): string
    {
        return Str::singular($this->getTable()).'_translations';
    }

    public function getTranslationForeignKey(): string
    {
        return Str::singular($this->getTable()).'_id';
    }

    public function translationModelClass(): string
    {
        return static::class.'Translation';
    }

    /** @return HasMany<Model, Model> */
    public function translations(): HasMany
    {
        /** @var class-string<Model> $class */
        $class = $this->translationModelClass();

        /** @var HasMany<Model, Model> $relation */
        $relation = $this->hasMany($class, $this->getTranslationForeignKey(), $this->getKeyName());

        return $relation;
    }

    /** @return list<string> */
    public function getTranslatableAttributes(): array
    {
        return $this->translatable ?? [];
    }

    public function getTranslation(string $locale): ?Model
    {
        if (! array_key_exists($locale, $this->translationCache)) {
            $result = $this->translations()->where('locale', $locale)->first();
            $this->translationCache[$locale] = $result ?: null;
        }

        return $this->translationCache[$locale];
    }

    /** @param array<string, mixed> $attributes */
    public function setTranslation(string $locale, array $attributes): void
    {
        $this->translations()->updateOrCreate(
            ['locale' => $locale],
            $attributes,
        );

        unset($this->translationCache[$locale]);
    }

    /** @return Collection<string, Model> */
    public function getTranslations(): Collection
    {
        /** @var Collection<string, Model> $translations */
        $translations = $this->translations()->get()->keyBy('locale');

        foreach ($translations as $locale => $model) {
            $this->translationCache[(string) $locale] = $model;
        }

        return $translations;
    }

    public function hasTranslation(string $locale): bool
    {
        if (array_key_exists($locale, $this->translationCache)) {
            return $this->translationCache[$locale] !== null;
        }

        return $this->translations()->where('locale', $locale)->exists();
    }

    /**
     * Resolve the slug for hreflang/canonical output.
     * Override this method in your model to customise the resolved value,
     * e.g. to build a full path for hierarchical resources.
     */
    protected function resolveHreflangSlug(Model $translation, string $slugField, string $locale): ?string
    {
        return $translation->{$slugField} ?? null;
    }

    /**
     * Return hreflang data for the active locales, with x-default pointing at the default language.
     *
     * @return array<string, string|null>
     */
    public function hreflangData(string $slugField = 'slug'): array
    {
        $languages = rescue(
            fn (): Collection => Language::activeLocales(),
            collect(),
        );

        $result = [];

        foreach ($languages as $language) {
            $translation = $this->getTranslation($language->code);

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

            $result[$language->code] = $this->resolveHreflangSlug($translation, $slugField, $language->code);
        }

        $default = rescue(fn (): Language => Language::defaultLocale(), null);

        if ($default !== null && array_key_exists($default->code, $result)) {
            $result['x-default'] = $result[$default->code];
        }

        return $result;
    }

    /**
     * Return the canonical URL for the given locale (defaults to the current app locale).
     * Uses config('builder.public_url') as the base, falling back to config('app.url').
     */
    public function canonicalUrl(?string $locale = null, string $slugField = 'slug'): ?string
    {
        $resolveLocale = $locale ?? app()->getLocale();
        $translation = $this->getTranslation($resolveLocale);

        if ($translation === null) {
            return null;
        }

        $slug = $this->resolveHreflangSlug($translation, $slugField, $resolveLocale);

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

        $base = rtrim((string) config('builder.public_url') ?: config('app.url', ''), '/');

        return $base.'/'.ltrim($slug, '/');
    }

    /**
     * Prevent translatable attributes from being stored directly on the parent model.
     * Translatable values must be persisted via setTranslation().
     */
    public function setAttribute($key, $value): mixed
    {
        if (in_array($key, $this->getTranslatableAttributes(), true)) {
            return $this;
        }

        return parent::setAttribute($key, $value);
    }

    /**
     * Return the translated value for the given key using the current app locale.
     * Falls through to Eloquent's default resolution when:
     *   – the key is not in $translatable, or
     *   – the attribute is already loaded (e.g. via a LEFT JOIN in the index query).
     */
    public function getAttribute($key): mixed
    {
        if (
            in_array($key, $this->getTranslatableAttributes(), true)
            && ! array_key_exists($key, $this->getAttributes())
        ) {
            $translation = $this->getTranslation(app()->getLocale());

            return $translation === null ? null : $translation->{$key};
        }

        return parent::getAttribute($key);
    }

    /**
     * Include translatable attributes for the current app locale in the array output.
     * Attributes already present (e.g. eager-loaded via JOIN) are not overwritten.
     *
     * @return array<string, mixed>
     */
    public function toArray(): array
    {
        $array = parent::toArray();
        $locale = app()->getLocale();
        $missing = array_diff($this->getTranslatableAttributes(), array_keys($array));

        if ($missing !== []) {
            $translation = $this->getTranslation($locale);

            foreach ($missing as $attr) {
                $array[$attr] = $translation === null ? null : $translation->{$attr};
            }
        }

        return $array;
    }
}
