<?php

declare(strict_types=1);

namespace App\Support\AuditLogDisplay;

use App\Models\DberpAdminPermission;
use App\Models\DberpAdminRole;
use App\Models\DberpAuditLog;
use App\Models\DberpCustomer;
use App\Models\DberpCustomerCategory;
use App\Models\DberpEmployee;
use App\Models\DberpProduct;
use App\Models\DberpProductBrand;
use App\Models\DberpProductCategory;
use App\Models\DberpPurchaseOrder;
use App\Models\DberpPurchaseReceiptOrder;
use App\Models\DberpPurchaseReturnOrder;
use App\Models\DberpRegion;
use App\Models\DberpSalesOrder;
use App\Models\DberpSupplier;
use App\Models\DberpSupplierCategory;
use App\Models\DberpUnit;
use App\Models\DberpWarehouse;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Throwable;

/**
 * 将常见外键 ID 批量解析为业务可读名称；缺失记录回退为「已删除记录 #ID」，查询失败不抛异常。
 */
final class DberpAuditLogRelationLabelResolver
{
    /**
     * @var array<string, array{class-string<Model>, list<string>}>
     */
    private const FOREIGN_KEY_TARGETS = [
        'customer_category_id' => [DberpCustomerCategory::class, ['name', 'code']],
        'supplier_category_id' => [DberpSupplierCategory::class, ['name', 'code']],
        'warehouse_id' => [DberpWarehouse::class, ['name', 'code']],
        'product_id' => [DberpProduct::class, ['name', 'code']],
        'unit_id' => [DberpUnit::class, ['name', 'code']],
        'brand_id' => [DberpProductBrand::class, ['name', 'brand_code', 'short_name']],
        'category_id' => [DberpProductCategory::class, ['name', 'code']],
        'customer_id' => [DberpCustomer::class, ['name', 'code']],
        'supplier_id' => [DberpSupplier::class, ['name', 'code']],
        'purchase_order_id' => [DberpPurchaseOrder::class, ['code']],
        'sales_order_id' => [DberpSalesOrder::class, ['code']],
        'purchase_receipt_order_id' => [DberpPurchaseReceiptOrder::class, ['code']],
        'purchase_return_order_id' => [DberpPurchaseReturnOrder::class, ['code']],
        'handler_employee_id' => [DberpEmployee::class, ['name', 'code']],
        'manager_employee_id' => [DberpEmployee::class, ['name', 'code']],
        'region_id' => [DberpRegion::class, ['name', 'code']],
        'user_id' => [User::class, ['name', 'login_account']],
        'role_id' => [DberpAdminRole::class, ['display_name', 'name']],
        'permission_id' => [DberpAdminPermission::class, ['display_name', 'name']],
    ];

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

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

    /**
     * @param  iterable<int, DberpAuditLog>  $logs
     */
    public function preloadFromLogs(iterable $logs): void
    {
        $groupedIds = [];

        foreach ($logs as $log) {
            foreach ([$log->old_values, $log->new_values] as $values) {
                if (! is_array($values)) {
                    continue;
                }

                foreach ($values as $key => $raw) {
                    $id = $this->normalizeId($raw);
                    if ($id === null) {
                        continue;
                    }

                    if (! isset(self::FOREIGN_KEY_TARGETS[$key])) {
                        continue;
                    }

                    $modelClass = self::FOREIGN_KEY_TARGETS[$key][0];
                    $groupedIds[$modelClass] ??= [];
                    $groupedIds[$modelClass][$id] = true;
                }
            }
        }

        foreach ($groupedIds as $modelClass => $idMap) {
            $ids = array_keys($idMap);
            $this->loadIdsForModel($modelClass, $ids);
        }
    }

    /**
     * @param  class-string<Model>  $modelClass
     */
    public function preloadAuditable(string $modelClass, int $id): void
    {
        if ($id <= 0) {
            return;
        }

        $this->loadIdsForModel($modelClass, [$id]);
    }

    public function canResolveForeignKey(string $column): bool
    {
        return isset(self::FOREIGN_KEY_TARGETS[$column]);
    }

    public function labelForForeignKey(string $column, mixed $rawId): string
    {
        if (! isset(self::FOREIGN_KEY_TARGETS[$column])) {
            return $this->scalarToString($rawId);
        }

        $id = $this->normalizeId($rawId);
        if ($id === null) {
            return (string) __('dberp.audit_display.value_empty');
        }

        $modelClass = self::FOREIGN_KEY_TARGETS[$column][0];
        $this->loadIdsForModel($modelClass, [$id]);

        return $this->cache[$modelClass][$id]
            ?? (string) __('dberp.audit_display.deleted_relation_record', ['id' => $id]);
    }

    /**
     * @param  class-string<Model>  $modelClass
     * @param  list<int>  $ids
     */
    private function loadIdsForModel(string $modelClass, array $ids): void
    {
        $ids = array_values(array_unique(array_filter($ids, static fn (int $id): bool => $id > 0)));
        if ($ids === []) {
            return;
        }

        $batchKey = $modelClass.'|'.implode(',', $ids);
        if (isset($this->batchLoaded[$batchKey])) {
            return;
        }

        $this->batchLoaded[$batchKey] = true;

        $pending = [];
        foreach ($ids as $id) {
            if (! isset($this->cache[$modelClass][$id])) {
                $pending[] = $id;
            }
        }

        if ($pending === []) {
            return;
        }

        $columns = ['name', 'code'];
        foreach (self::FOREIGN_KEY_TARGETS as $cfg) {
            if ($cfg[0] === $modelClass) {
                $columns = $cfg[1];

                break;
            }
        }

        try {
            /** @var Collection<int, Model> $rows */
            $rows = $modelClass::query()
                ->whereKey($pending)
                ->get(array_values(array_unique(array_merge(['id'], $columns))));
        } catch (Throwable) {
            foreach ($pending as $id) {
                $this->cache[$modelClass][$id] = (string) __('dberp.audit_display.deleted_relation_record', ['id' => $id]);
            }

            return;
        }

        foreach ($pending as $id) {
            $this->cache[$modelClass][$id] = (string) __('dberp.audit_display.deleted_relation_record', ['id' => $id]);
        }

        foreach ($rows as $row) {
            $this->cache[$modelClass][(int) $row->getKey()] = $this->formatModelLabel($row, $columns);
        }
    }

    /**
     * @param  list<string>  $columns
     */
    private function formatModelLabel(Model $model, array $columns): string
    {
        foreach ($columns as $column) {
            $value = $model->getAttribute($column);
            if (is_string($value) && $value !== '') {
                return $value;
            }
        }

        return (string) __('dberp.audit_display.deleted_relation_record', ['id' => $model->getKey()]);
    }

    private function scalarToString(mixed $rawId): string
    {
        if ($rawId === null || $rawId === '') {
            return (string) __('dberp.audit_display.value_empty');
        }

        return is_scalar($rawId) ? (string) $rawId : (string) json_encode($rawId, JSON_UNESCAPED_UNICODE);
    }

    private function normalizeId(mixed $value): ?int
    {
        if ($value === null || $value === '') {
            return null;
        }

        if (is_bool($value)) {
            return null;
        }

        if (is_int($value)) {
            return $value > 0 ? $value : null;
        }

        if (is_float($value)) {
            $int = (int) $value;

            return $int > 0 ? $int : null;
        }

        if (is_string($value) && ctype_digit($value)) {
            $int = (int) $value;

            return $int > 0 ? $int : null;
        }

        return null;
    }
}
