<?php

declare(strict_types=1);

namespace App\Support;

use App\Models\Accounting\Account;
use App\Models\SystemSetting;
use Hypervel\Support\Facades\Config;
use Hypervel\Support\Str;

/**
 * Accounting theme registry.
 *
 * Themes are defined in config/accounting/themes.php and identified by the
 * `accounting_theme` system setting. Theme identity travels with the data:
 * accounts seeded from a theme are stamped with theme_key + theme_account_code
 * so we can branch on the active theme and detect drift later, even after
 * users edit the chart through admin CRUD.
 */
class AccountingTheme
{
    public const DEFAULT_KEY = 'blank';

    public const FALLBACK_KEY = 'blank';

    /**
     * Active theme key for this deployment.
     */
    public static function current(): string
    {
        $key = SystemSetting::getValue('accounting_theme', self::DEFAULT_KEY);

        if (!\is_string($key) || $key === '' || !\array_key_exists($key, self::all())) {
            return self::FALLBACK_KEY;
        }

        return $key;
    }

    /**
     * Whether the active theme matches the given key.
     */
    public static function is(string $key): bool
    {
        return self::current() === $key;
    }

    /**
     * All registered theme definitions, keyed by theme key.
     *
     * @return array<string, array<string, mixed>>
     */
    public static function all(): array
    {
        $themes = Config::get('accounting.themes', []);
        return \is_array($themes) ? $themes : [];
    }

    /**
     * The definition for a single theme (or the active theme when no key given).
     *
     * @return array<string, mixed>|null
     */
    public static function definition(?string $key = null): ?array
    {
        $key = $key ?? self::current();
        $themes = self::all();
        return $themes[$key] ?? null;
    }

    /**
     * The starter Chart of Accounts hierarchy for a theme.
     *
     * @return array<int, array<string, mixed>>
     */
    public static function tree(?string $key = null): array
    {
        $definition = self::definition($key);
        $tree = $definition['tree'] ?? [];
        return \is_array($tree) ? $tree : [];
    }

    /**
     * Convert an account name to a stable code for the given theme.
     * Used so re-applying a theme can match existing seeded accounts even if
     * a user has renamed them.
     */
    public static function accountCode(string $name): string
    {
        $slug = strtolower(trim($name));
        $slug = preg_replace('/[^a-z0-9]+/', '_', $slug) ?? '';
        return trim($slug, '_');
    }

    /**
     * Flatten a theme tree into a list of definitions keyed by code.
     *
     * Each entry: ['code', 'name', 'type', 'default_flow', 'parent_code', 'description'].
     *
     * @return array<string, array<string, mixed>>
     */
    public static function flatten(?string $key = null): array
    {
        $flat = [];
        self::flattenWalk(self::tree($key), null, $flat);
        return $flat;
    }

    private static function flattenWalk(array $nodes, ?string $parentCode, array &$flat): void
    {
        foreach ($nodes as $node) {
            $name = (string) ($node['name'] ?? '');
            if ($name === '') {
                continue;
            }
            $code = self::accountCode($name);
            $type = strtoupper((string) ($node['type'] ?? ''));
            $flat[$code] = [
                'code' => $code,
                'name' => $name,
                'type' => $type,
                'default_flow' => $node['default_flow'] ?? (($type === 'REVENUE' || $type === 'LIABILITY') ? 'INCOME' : 'EXPENSE'),
                'parent_code' => $parentCode,
                'description' => $node['description'] ?? null,
            ];
            if (!empty($node['children']) && \is_array($node['children'])) {
                self::flattenWalk($node['children'], $code, $flat);
            }
        }
    }

    /**
     * Compare the active theme against the DB. Pure read — never mutates.
     *
     * Returns:
     *   - missing:    codes in the theme but absent from DB (need apply)
     *   - present:    codes in both, name matches
     *   - renamed:    codes in both but name differs in DB (user-edited)
     *   - user_added: account ids/names with theme_key NULL (custom additions)
     *
     * @return array<string, mixed>
     */
    public static function drift(?string $key = null): array
    {
        $key = $key ?? self::current();
        $themeAccounts = self::flatten($key);

        $rows = Account::where('theme_key', $key)
            ->get(['id', 'name', 'theme_account_code', 'is_active', 'parent_id'])
            ->keyBy('theme_account_code');

        $missing = [];
        $present = [];
        $renamed = [];
        foreach ($themeAccounts as $code => $def) {
            $row = $rows[$code] ?? null;
            if (!$row) {
                $missing[] = ['code' => $code, 'name' => $def['name']];
                continue;
            }
            if ($row->name !== $def['name']) {
                $renamed[] = ['code' => $code, 'expected' => $def['name'], 'current' => $row->name, 'id' => $row->id];
            } else {
                $present[] = ['code' => $code, 'name' => $def['name'], 'id' => $row->id];
            }
        }

        $userAdded = Account::whereNull('theme_key')
            ->orderBy('id')
            ->get(['id', 'name', 'parent_id'])
            ->map(fn($r) => ['id' => $r->id, 'name' => $r->name, 'parent_id' => $r->parent_id])
            ->all();

        return [
            'theme_key' => $key,
            'missing' => $missing,
            'present' => $present,
            'renamed' => $renamed,
            'user_added' => $userAdded,
            'totals' => [
                'expected' => \count($themeAccounts),
                'missing' => \count($missing),
                'present' => \count($present),
                'renamed' => \count($renamed),
                'user_added' => \count($userAdded),
            ],
        ];
    }

    /**
     * Idempotently apply the active (or specified) theme to the DB.
     *
     * Never deletes, never renames user edits. For each theme node:
     *   - if a row matches by (theme_key, theme_account_code), stamp/refresh metadata
     *   - else if a row matches by (parent_id, name), back-stamp it
     *   - else create a new row
     *
     * @return array{created:int, stamped:int, skipped:int, theme_key:string}
     */
    public static function apply(?string $key = null): array
    {
        $key = $key ?? self::current();
        $tree = self::tree($key);

        $stats = ['created' => 0, 'stamped' => 0, 'skipped' => 0, 'theme_key' => $key];
        foreach ($tree as $node) {
            self::applyNode($node, null, $key, $stats);
        }
        return $stats;
    }

    private static function applyNode(array $node, ?int $parentId, string $themeKey, array &$stats): void
    {
        $name = (string) ($node['name'] ?? '');
        if ($name === '') {
            return;
        }
        $code = self::accountCode($name);
        $type = strtoupper((string) ($node['type'] ?? ''));
        $defaultFlow = $node['default_flow']
            ?? (($type === 'REVENUE' || $type === 'LIABILITY') ? 'INCOME' : 'EXPENSE');

        $account = Account::where('theme_key', $themeKey)
            ->where('theme_account_code', $code)
            ->where(function ($q) use ($parentId) {
                if ($parentId === null) {
                    $q->whereNull('parent_id');
                } else {
                    $q->where('parent_id', $parentId);
                }
            })
            ->first();

        if (!$account) {
            $account = Account::where('parent_id', $parentId)
                ->where('name', $name)
                ->first();
        }

        if ($account) {
            $dirty = false;
            if ($account->theme_key !== $themeKey) { $account->theme_key = $themeKey; $dirty = true; }
            if ($account->theme_account_code !== $code) { $account->theme_account_code = $code; $dirty = true; }
            if (empty($account->default_flow)) { $account->default_flow = $defaultFlow; $dirty = true; }
            if (empty($account->description) && !empty($node['description'])) {
                $account->description = $node['description'];
                $dirty = true;
            }
            if ($dirty) {
                $account->save();
                $stats['stamped']++;
            } else {
                $stats['skipped']++;
            }
        } else {
            $account = Account::create([
                'hashkey' => Str::random(40),
                'parent_id' => $parentId,
                'type' => $type,
                'default_flow' => $defaultFlow,
                'name' => $name,
                'description' => $node['description'] ?? null,
                'theme_key' => $themeKey,
                'theme_account_code' => $code,
                'is_active' => true,
            ]);
            $stats['created']++;
        }

        if (!empty($node['children']) && \is_array($node['children'])) {
            foreach ($node['children'] as $child) {
                self::applyNode($child, $account->id, $themeKey, $stats);
            }
        }
    }

    /**
     * Lightweight summary of all themes for admin UI dropdowns.
     *
     * @return array<int, array<string, mixed>>
     */
    public static function options(): array
    {
        $options = [];
        foreach (self::all() as $key => $def) {
            $options[] = [
                'key' => $key,
                'label' => $def['label'] ?? $key,
                'description' => $def['description'] ?? null,
                'version' => $def['version'] ?? 1,
            ];
        }
        return $options;
    }
}
