<?php

namespace App\Services\Documents;

use App\Models\Contract;
use App\Models\DocumentTemplate;
use App\Models\User;

class TokenValueResolver
{
    /**
     * Resolve tokens to values.
     * Returns array with keys: values (resolved token=>value) and unresolved (list of tokens not resolved / not allowed)
     * Policy determines whether invalid tokens throw (fail) or are collected (blank|keep).
     *
     * @return array{values:array<string,string>,unresolved:array<int,string>,customTypes?:array<string,string>}
     */
    public function resolve(
        array $tokens,
        DocumentTemplate $template,
        Contract $contract,
        User $user,
        string $policy = 'fail',
        array $customOverrides = [],
        ?array $customDefaults = null,
        string $onMissingCustom = 'empty'
    ): array {
        $values = [];
        $unresolved = [];
        $customTypesOut = [];
        // Custom namespace: merge defaults from settings/template meta and overrides
        $settings = app(\App\Services\Documents\DocumentSettings::class)->get();
        $defaults = $customDefaults ?? ($template->meta['custom_defaults'] ?? null) ?? ($settings->custom_defaults ?? []);
        if (! is_array($defaults)) {
            $defaults = [];
        }
        if (! is_array($customOverrides)) {
            $customOverrides = [];
        }
        $custom = array_replace($defaults, $customOverrides);
        // Collect custom types from template meta (optional)
        $customTypes = [];
        if (isset($template->meta['custom_default_types']) && is_array($template->meta['custom_default_types'])) {
            foreach ($template->meta['custom_default_types'] as $k => $t) {
                $t = in_array($t, ['string', 'number', 'date', 'text'], true) ? $t : 'string';
                $customTypes[(string) $k] = $t;
            }
        }
        // Retrieve whitelist from DB settings (if present) and merge with config baseline (config acts as baseline; DB can add or override entity arrays)
        $settingsWhitelist = app(\App\Services\Documents\DocumentSettings::class)->get()->whitelist ?? [];
        $configWhitelist = config('documents.whitelist', []);
        // Merge preserving DB additions/overrides
        $globalWhitelist = array_replace($configWhitelist, $settingsWhitelist);
        // Always treat globally whitelisted entities as available, even if legacy template does not list them
        if ($template->entities && is_array($template->entities)) {
            $templateEntities = array_values(array_unique(array_merge($template->entities, array_keys($globalWhitelist))));
        } else {
            $templateEntities = array_keys($globalWhitelist);
        }
        // Normalize template tokens list (used as an allow-list if columns / global whitelist are not exhaustive)
        $templateTokens = [];
        $rawTemplateTokens = $template->tokens ?? null;
        if (is_array($rawTemplateTokens)) {
            $templateTokens = array_values(array_filter(array_map('strval', $rawTemplateTokens)));
        } elseif (is_string($rawTemplateTokens)) {
            $decoded = json_decode($rawTemplateTokens, true);
            if (is_array($decoded)) {
                $templateTokens = array_values(array_filter(array_map('strval', $decoded)));
            }
        }
        foreach ($tokens as $token) {
            [$entity,$attr] = explode('.', $token, 2);
            if ($entity === 'generation') {
                $values[$token] = $this->generationAttribute($attr, $user);

                continue;
            }
            if ($entity === 'custom') {
                // Track type info if present
                if (isset($customTypes[$attr])) {
                    $customTypesOut[$token] = $customTypes[$attr];
                }
                if (array_key_exists($attr, $custom)) {
                    $v = $custom[$attr];
                    if (is_scalar($v) || (is_object($v) && method_exists($v, '__toString'))) {
                        $values[$token] = (string) $v;
                    } else {
                        $values[$token] = '';
                    }
                } else {
                    // Missing custom – apply onMissingCustom policy locally (empty|leave|error)
                    if ($onMissingCustom === 'error') {
                        if ($policy === 'fail') {
                            throw new \RuntimeException("Manjkajoč custom token: {$token}");
                        }
                        $unresolved[] = $token;
                    } elseif ($onMissingCustom === 'leave') {
                        $unresolved[] = $token;
                    } else { // empty
                        $values[$token] = '';
                    }
                }

                continue;
            }
            if (! in_array($entity, $templateEntities, true)) {
                // If the token is explicitly listed on the template's tokens, allow it
                if (! $templateTokens || ! in_array($token, $templateTokens, true)) {
                    if ($policy === 'fail') {
                        throw new \RuntimeException("Nedovoljen entiteta token: $entity");
                    }
                    $unresolved[] = $token;

                    continue;
                }
            }
            // Allowed attributes: merge template-declared columns with global whitelist (config + DB settings)
            // Support nested dotted attributes (e.g. person.person_address.city). We allow if either the full
            // dotted path is listed or if the base prefix is listed (e.g. person.person_address) and the resolver
            // can handle it.
            // Safely read template-declared columns
            $columns = is_array($template->columns ?? null) ? $template->columns : [];
            $allowedFromTemplate = $columns[$entity] ?? [];
            $allowedFromGlobal = $globalWhitelist[$entity] ?? [];
            $allowed = array_values(array_unique(array_merge($allowedFromTemplate, $allowedFromGlobal)));
            $isAllowed = in_array($attr, $allowed, true);
            if (! $isAllowed && str_contains($attr, '.')) {
                // Check progressive prefixes: a.b.c -> a.b
                $parts = explode('.', $attr);
                while (count($parts) > 1 && ! $isAllowed) {
                    array_pop($parts);
                    $prefix = implode('.', $parts);
                    if (in_array($prefix, $allowed, true)) {
                        $isAllowed = true;
                        break;
                    }
                }
            }
            if (! $isAllowed) {
                if ($policy === 'fail') {
                    throw new \RuntimeException("Nedovoljen stolpec token: $token");
                }
                $unresolved[] = $token;

                continue;
            }
            $values[$token] = $this->entityAttribute($entity, $attr, $contract) ?? '';
        }

        return [
            'values' => $values,
            'unresolved' => array_values(array_unique($unresolved)),
            'customTypes' => $customTypesOut,
        ];
    }

    private function generationAttribute(string $attr, User $user): string
    {
        return match ($attr) {
            'timestamp' => (string) now()->timestamp,
            'date' => now()->toDateString(), // raw ISO; formatting applied later
            'user_name' => $user->name ?? 'Uporabnik',
            default => ''
        };
    }

    private function entityAttribute(string $entity, string $attr, Contract $contract): ?string
    {
        switch ($entity) {
            case 'contract':
                return (string) ($contract->{$attr} ?? '');
            case 'client_case':
                return (string) optional($contract->clientCase)->{$attr};
            case 'client':
                $client = optional($contract->clientCase)->client;
                if (! $client) {
                    return '';
                }
                if (str_contains($attr, '.')) {
                    return $this->resolveNestedFromModel($client, $attr);
                }

                return (string) ($client->{$attr} ?? '');
            case 'person':
                $person = optional($contract->clientCase)->person;
                if (! $person) {
                    return '';
                }
                if (str_contains($attr, '.')) {
                    return $this->resolveNestedFromModel($person, $attr);
                }

                return (string) ($person->{$attr} ?? '');
            case 'account':
                $account = optional($contract->account);

                return (string) $account->{$attr};
            default:
                return '';
        }
    }

    /**
     * Resolve nested dotted paths from a base model for supported relations/aliases.
     * Supports:
     * - Client: person.*
     * - Person: person_address.* (uses first active address)
     */
    private function resolveNestedFromModel(object $model, string $path): string
    {
        $segments = explode('.', $path);
        $current = $model;
        foreach ($segments as $seg) {
            if (! $current) {
                return '';
            }
            if ($current instanceof \App\Models\Client && $seg === 'person') {
                $current = $current->person;

                continue;
            }
            if ($current instanceof \App\Models\Person\Person && $seg === 'person_address') {
                $current = $current->addresses()->first();

                continue;
            }
            // Default attribute access
            try {
                $current = is_array($current) ? ($current[$seg] ?? null) : ($current->{$seg} ?? null);
            } catch (\Throwable $e) {
                return '';
            }
        }

        return $current !== null ? (string) $current : '';
    }
}
