<?php

namespace App\Domain\Identity\Services;

use App\Domain\Identity\Models\Branch;
use App\Domain\Identity\Models\BranchSetting;
use App\Domain\Identity\Support\Terminology;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;

/**
 * Resolves — and stores — the words a branch uses for member / non-member.
 *
 * Receives an explicit ?int $branchId like every other domain service: the
 * session lives in BranchContext, and the helpers in app/Helpers/terminology.php
 * are the adapter between the two. That is what keeps this callable from a
 * queued notification or an artisan report, both of which have a branch id and
 * no session at all.
 *
 * Reads go through the query builder rather than the model on purpose. This is
 * consulted by the sidebar, so it runs on literally every page; BranchSetting
 * carries the branch global scope, which would filter a settings screen editing
 * branch B while the user sits in branch A down to nothing. branch_id is unique
 * per key, so filtering on it needs no other scope to be unambiguous.
 *
 * Bound scoped, so the per-branch memo lives exactly one request — and a
 * Livewire round-trip is its own request.
 */
class TerminologyService
{
    public const GROUP = 'terminology';

    private const PREFIX = 'terminology.';

    private const PRESET_KEY = 'terminology.preset';

    /** @var array<string, array<string, string>> */
    private array $stored = [];

    /** @var array<string, Terminology> */
    private array $resolved = [];

    /**
     * The vocabulary for a branch, in the given locale.
     *
     * A null branch — an unconfigured install, the public website, a user in
     * all-branches mode — gets the default preset rather than blank labels.
     * Empty strings anywhere in the app would be a worse failure than the
     * wrong word.
     */
    public function for(?int $branchId, ?string $locale = null): Terminology
    {
        $locale ??= app()->getLocale();
        $cacheKey = ($branchId ?? 0) . '|' . $locale;

        return $this->resolved[$cacheKey] ??= $this->build($branchId, $locale);
    }

    private function build(?int $branchId, string $locale): Terminology
    {
        $stored = $branchId ? $this->storedFor($branchId) : [];

        $preset = $stored[self::PRESET_KEY] ?? null;
        if (! is_string($preset) || ! isset($this->presets()[$preset])) {
            $preset = $this->defaultPreset();
        }

        $terms = $this->presets()[$preset]['terms'] ?? [];

        // A branch that picked a preset and then edited two words stores only
        // those two. Everything it did not touch keeps following the preset,
        // which is how a preset fix ships to every branch that uses it.
        foreach ([...Terminology::BASE_KEYS, ...Terminology::ENGLISH_KEYS] as $key) {
            $value = $stored[self::PREFIX . $key] ?? null;

            if (is_string($value) && trim($value) !== '') {
                $terms[$key] = trim($value);
            }
        }

        return new Terminology($terms, $locale, $preset);
    }

    /**
     * The raw terminology rows for a branch, keyed by setting key.
     *
     * @return array<string, string>
     */
    private function storedFor(int $branchId): array
    {
        return $this->stored[$branchId] ??= $this->readSettings($branchId);
    }

    /**
     * @return array<string, string>
     */
    private function readSettings(int $branchId): array
    {
        try {
            return DB::table('branch_settings')
                ->where('branch_id', $branchId)
                ->where('group', self::GROUP)
                ->pluck('value', 'key')
                ->all();
        } catch (QueryException) {
            // Before the table exists — a container mid-migrate, a test booting
            // against an empty schema — the defaults are the right answer, not
            // a 500 on every page that renders a sidebar.
            return [];
        }
    }

    // --------------------------------------------------------------- writes

    /**
     * Persist a branch's vocabulary.
     *
     * Only genuine overrides are stored. A word the caller hands back exactly
     * as the preset spells it is deleted rather than written, so the settings
     * screen — which always posts all thirteen fields — does not silently pin
     * a branch to today's preset text and cut it off from a later correction.
     * Blank fields are deleted for the same reason: a cleared word falls back
     * to the preset instead of rendering nothing.
     *
     * @param  array<string, string|null>  $terms
     */
    public function save(int $branchId, string $preset, array $terms, ?int $academyId = null): void
    {
        $academyId ??= (int) Branch::withoutGlobalScopes()
            ->whereKey($branchId)
            ->firstOrFail(['id', 'academy_id'])
            ->academy_id;

        if (! isset($this->presets()[$preset])) {
            $preset = $this->defaultPreset();
        }

        $presetTerms = $this->presets()[$preset]['terms'] ?? [];

        DB::transaction(function () use ($branchId, $preset, $terms, $academyId, $presetTerms) {
            $this->put($branchId, $academyId, self::PRESET_KEY, $preset);

            foreach ([...Terminology::BASE_KEYS, ...Terminology::ENGLISH_KEYS] as $key) {
                $value = $terms[$key] ?? null;
                $value = is_string($value) ? trim($value) : null;

                if ($value === '' || $value === ($presetTerms[$key] ?? null)) {
                    $value = null;
                }

                $this->put($branchId, $academyId, self::PREFIX . $key, $value);
            }
        });

        unset($this->stored[$branchId]);
        $this->resolved = [];
    }

    private function put(int $branchId, int $academyId, string $key, ?string $value): void
    {
        if ($value === null) {
            BranchSetting::withoutGlobalScopes()
                ->where('branch_id', $branchId)
                ->where('key', $key)
                ->delete();

            return;
        }

        BranchSetting::withoutGlobalScopes()->updateOrCreate(
            ['branch_id' => $branchId, 'key' => $key],
            ['academy_id' => $academyId, 'value' => $value, 'group' => self::GROUP],
        );
    }

    /**
     * Copy one branch's vocabulary onto every other branch in its academy.
     * The nine-branch client configures once instead of nine times.
     *
     * What is copied is the source's preset plus the words it actually
     * overrode — not its fully expanded vocabulary. Copying the expansion
     * would leave every target pinned to today's preset text while the source
     * kept following the preset, so a later correction to a preset would reach
     * one branch out of nine.
     *
     * @return int  branches written, not counting the source
     */
    public function copyToAcademy(int $sourceBranchId): int
    {
        $source = Branch::withoutGlobalScopes()
            ->whereKey($sourceBranchId)
            ->firstOrFail(['id', 'academy_id']);

        $preset = $this->presetFor($sourceBranchId);
        $overrides = $this->overridesFor($sourceBranchId);

        $targets = Branch::withoutGlobalScopes()
            ->where('academy_id', $source->academy_id)
            ->where('id', '!=', $sourceBranchId)
            ->pluck('id');

        foreach ($targets as $targetId) {
            $this->save((int) $targetId, $preset, $overrides, (int) $source->academy_id);
        }

        return $targets->count();
    }

    /**
     * Only the words this branch stored for itself, keyed without the prefix.
     *
     * @return array<string, string>
     */
    private function overridesFor(int $branchId): array
    {
        $stored = $this->storedFor($branchId);
        $overrides = [];

        foreach ([...Terminology::BASE_KEYS, ...Terminology::ENGLISH_KEYS] as $key) {
            $value = $stored[self::PREFIX . $key] ?? null;

            if (is_string($value) && trim($value) !== '') {
                $overrides[$key] = trim($value);
            }
        }

        return $overrides;
    }

    // --------------------------------------------------------------- presets

    /**
     * @return array<string, array{label: string, terms: array<string, string>}>
     */
    public function presets(): array
    {
        return config('terminology.presets', []);
    }

    public function defaultPreset(): string
    {
        $default = config('terminology.default', 'club');

        return isset($this->presets()[$default])
            ? $default
            : (string) array_key_first($this->presets());
    }

    /**
     * Which preset a branch is on, before any per-word edits.
     */
    public function presetFor(?int $branchId): string
    {
        return $this->for($branchId, 'ar')->preset;
    }

    /**
     * Drop the memo. Only the settings screen needs this — it edits the very
     * rows the memo was built from and then re-renders in the same request.
     */
    public function flush(): void
    {
        $this->stored = [];
        $this->resolved = [];
    }
}
