<?php

namespace Modules\AiCore\Entities;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\AiCore\Entities\Verticals\VerticalCatalog;
use Modules\AiCore\Entities\Verticals\VerticalProfile;

/**
 * Per-business AI provider configuration (optionally one of several named
 * profiles). This is AiCore's generalised replacement for VetAI's VaiSetting —
 * core fields only, plus the business_vertical that drives the smart persona.
 */
class AiProvider extends Model
{
    protected $table = 'ai_providers';
    protected $guarded = ['id'];

    protected $hidden = ['api_key'];

    protected $casts = [
        'api_key'                    => 'encrypted',
        't_api_key'                  => 'encrypted',
        'enabled'                    => 'boolean',
        'temperature'                => 'float',
        'max_tokens'                 => 'integer',
        'meta'                       => 'array',
        'use_separate_transcription' => 'boolean',
        'auto_structure_soap'        => 'boolean',
        'consent_warning'            => 'boolean',
        't_clean_non_medical'        => 'boolean',
    ];

    /**
     * Get (or lazily create) a provider profile for a business.
     *
     * On first creation it imports any existing VetAI (vai_settings) config so
     * businesses already using the VetAI assistant get AI immediately, with no
     * re-entry of the API key — VetAI itself is left untouched and working.
     */
    public static function forBusiness(int $businessId, string $name = 'default'): self
    {
        $existing = static::where('business_id', $businessId)->where('name', $name)->first();
        if ($existing) {
            return $existing;
        }

        $seed = [
            'business_id'       => $businessId,
            'name'              => $name,
            'business_vertical' => VerticalCatalog::detectDefault(),
            'provider'          => config('aicore.default_provider', 'gemini'),
            'enabled'           => false,
        ];

        if ($name === 'default') {
            $seed = array_merge($seed, static::importFromVetAi($businessId));
        }

        return static::create($seed);
    }

    /**
     * One-time import of the core fields from a VetAI vai_settings row, so an
     * existing clinic AI config seeds the AiCore default profile. Returns an
     * empty array when there is nothing to import.
     */
    private static function importFromVetAi(int $businessId): array
    {
        try {
            if (!Schema::hasTable('vai_settings')) {
                return [];
            }
            $row = DB::table('vai_settings')->where('business_id', $businessId)->first();
            if (!$row) {
                return [];
            }

            $import = [
                'provider'             => $row->provider ?: config('aicore.default_provider', 'gemini'),
                'base_url'             => $row->base_url ?? null,
                'model'                => $row->model ?? null,
                'enabled'              => (bool) ($row->enabled ?? false),
                'temperature'          => (float) ($row->temperature ?? 0.4),
                'max_tokens'           => (int) ($row->max_tokens ?? 1024),
                'monthly_token_budget' => $row->monthly_token_budget ?? null,
                'business_vertical'    => 'vet_clinic', // VetAI implies a veterinary clinic
            ];

            // Carry the voice-transcription block too, so a clinic's existing
            // Whisper/WebSpeech setup keeps working under AiCore unchanged.
            foreach ([
                'use_separate_transcription', 't_provider', 't_base_url', 't_model',
                't_language_hint', 't_silence_limit_sec', 't_clean_non_medical',
                'auto_structure_soap', 'consent_warning',
            ] as $col) {
                if (property_exists($row, $col) || isset($row->{$col})) {
                    $import[$col] = $row->{$col} ?? null;
                }
            }

            // Both api_key and t_api_key use Laravel's 'encrypted' cast, which
            // encrypts WITHOUT serialisation. Decrypt the same way (serialize =
            // false) before our own 'encrypted' cast re-encrypts them.
            foreach (['api_key', 't_api_key'] as $keyCol) {
                if (!empty($row->{$keyCol})) {
                    try {
                        $import[$keyCol] = decrypt($row->{$keyCol}, false);
                    } catch (\Throwable $e) {
                        // Leave blank rather than store unusable ciphertext;
                        // the admin can re-enter it in AiCore Settings.
                    }
                }
            }

            return $import;
        } catch (\Throwable $e) {
            return [];
        }
    }

    /** Ready to make AI calls? */
    public function isConfigured(): bool
    {
        if (!$this->enabled || empty($this->model)) {
            return false;
        }
        // openai_compatible needs an endpoint; the API key is optional (local Ollama).
        if ($this->provider === 'openai_compatible') {
            return !empty($this->base_url);
        }
        return !empty($this->api_key);
    }

    /** The resolved business-type profile for this provider row. */
    public function vertical(): VerticalProfile
    {
        return VerticalCatalog::get($this->business_vertical);
    }

    /**
     * Returns the (provider, base_url, api_key, model, language) tuple for VOICE
     * transcription. When use_separate_transcription is false it falls back to
     * the main assistant credentials (so a single OpenAI-compatible setup powers
     * both the text AI and Whisper). Ported verbatim from VetAI's VaiSetting.
     */
    public function transcriptionConfig(): array
    {
        if ($this->use_separate_transcription) {
            return [
                'provider' => $this->t_provider      ?: 'webspeech',
                'base_url' => $this->t_base_url      ?: null,
                'api_key'  => $this->t_api_key       ?: null,
                'model'    => $this->t_model         ?: 'whisper-large-v3-turbo',
                'language' => $this->t_language_hint ?: 'auto',
            ];
        }
        // Reuse the main assistant credentials. Only OpenAI-compatible providers
        // can transcribe; everything else degrades to the browser Web Speech API.
        $reusableProviders = ['openai', 'openai_compatible'];
        if (!in_array($this->provider, $reusableProviders, true)) {
            return [
                'provider' => 'webspeech',
                'base_url' => null, 'api_key' => null,
                'model'    => null, 'language' => 'auto',
            ];
        }
        return [
            'provider' => $this->provider === 'openai' ? 'whisper_openai' : 'whisper_openai_compat',
            'base_url' => $this->base_url ?: 'https://api.openai.com/v1',
            'api_key'  => $this->api_key,
            'model'    => 'whisper-1',
            'language' => 'auto',
        ];
    }

    /** Is voice transcription configured and ready to use? */
    public function isTranscriptionConfigured(): bool
    {
        $cfg = $this->transcriptionConfig();
        if ($cfg['provider'] === 'webspeech') return true;
        if (empty($cfg['model'])) return false;
        if ($cfg['provider'] === 'whisper_openai_compat') {
            return !empty($cfg['base_url']);
        }
        return !empty($cfg['api_key']);
    }

    public static function transcriptionProviders(): array
    {
        return [
            'webspeech'             => 'Browser Web Speech (free, runs on-device)',
            'whisper_openai'        => 'OpenAI Whisper (whisper-1)',
            'whisper_groq'          => 'Groq Whisper (whisper-large-v3-turbo — cheap)',
            'whisper_openai_compat' => 'Self-hosted / OpenAI-compatible endpoint',
        ];
    }

    public static function providers(): array
    {
        return ['anthropic', 'openai', 'gemini', 'openai_compatible'];
    }

    /**
     * One-click service presets for the failover-chain UI (label + provider +
     * base_url + suggested model). Sourced from config so brands can extend the
     * list without a code change. See config('aicore.presets').
     */
    public static function presets(): array
    {
        return (array) config('aicore.presets', []);
    }

    /**
     * All configurable profile names, in failover order: the primary 'default'
     * followed by the fallback profile names. Drives both the settings UI order
     * and the test() validation whitelist.
     */
    public static function profileNames(): array
    {
        return array_merge(['default'], (array) config('aicore.fallback_names', []));
    }

    /**
     * The ai_providers `name` used to store a reusable "AI service" — a set of
     * connection credentials configured & connection-tested once in the AI
     * services library, then referenced by the failover chain slots. Prefixing
     * keeps service rows out of the failover-name space (config only retries
     * config('aicore.fallback_names') rows), so a bare service row is inert.
     */
    public static function serviceName(string $presetKey): string
    {
        return 'svc_' . $presetKey;
    }

    /** All AI-service preset keys (groq, claude, …, custom). */
    public static function serviceKeys(): array
    {
        return array_keys(static::presets());
    }

    /**
     * Existing AI-service rows for a business, keyed by preset key. Only rows
     * that were actually saved are returned; an unsaved service maps to null
     * (no lazy creation here — the library UI must not create empty rows).
     *
     * @return array<string,self|null>
     */
    public static function servicesFor(int $businessId): array
    {
        $out = [];
        foreach (static::serviceKeys() as $key) {
            $out[$key] = static::where('business_id', $businessId)
                ->where('name', static::serviceName($key))
                ->first();
        }
        return $out;
    }

    /**
     * Best-match library preset key for a manually-configured connection —
     * used by the one-time auto-import of legacy main-form credentials into
     * the AI services library. Direct providers map 1:1 (gemini→gemini,
     * anthropic→claude, openai→openai); openai_compatible matches the endpoint
     * host against the known preset base_urls (groq / deepseek / zai), falling
     * back to 'custom'. Returns null when no usable preset exists.
     */
    public static function presetKeyForConnection(?string $provider, ?string $baseUrl): ?string
    {
        $presets = static::presets();

        $direct = ['gemini' => 'gemini', 'anthropic' => 'claude', 'openai' => 'openai'];
        if (isset($direct[$provider])) {
            return array_key_exists($direct[$provider], $presets) ? $direct[$provider] : null;
        }
        if ($provider !== 'openai_compatible') {
            return null;
        }

        $host = $baseUrl ? parse_url($baseUrl, PHP_URL_HOST) : null;
        if ($host) {
            foreach ($presets as $key => $preset) {
                if (($preset['provider'] ?? '') !== 'openai_compatible' || empty($preset['base_url'])) {
                    continue;
                }
                if (parse_url($preset['base_url'], PHP_URL_HOST) === $host) {
                    return $key;
                }
            }
        }
        return array_key_exists('custom', $presets) ? 'custom' : null;
    }

    /** Default model suggestion per provider. */
    public static function defaultModel(string $provider): string
    {
        return [
            'anthropic' => 'claude-haiku-4-5-20251001',
            'openai'    => 'gpt-4o-mini',
            'gemini'    => 'gemini-2.5-flash',
        ][$provider] ?? '';
    }

    /** Month-to-date tokens used by this business across all AiCore calls. */
    public function monthToDateTokens(): int
    {
        return AiUsageLog::monthToDateTokens((int) $this->business_id);
    }

    /**
     * Has this business hit its monthly token budget? Always false when no
     * budget is set or budget enforcement is disabled.
     */
    public function budgetExceeded(): bool
    {
        if (!config('aicore.budget_enforce', true) || empty($this->monthly_token_budget)) {
            return false;
        }
        return $this->monthToDateTokens() >= (int) $this->monthly_token_budget;
    }

    /**
     * Fraction (0..1+) of the monthly token budget used month-to-date, or null
     * when no budget is configured. May exceed 1.0 once over budget.
     */
    public function budgetUsedFraction(): ?float
    {
        if (empty($this->monthly_token_budget)) {
            return null;
        }
        return $this->monthToDateTokens() / (int) $this->monthly_token_budget;
    }

    /**
     * True when a budget is set and month-to-date usage has reached $threshold of
     * it (default 80%) — the "approaching the cap" signal for the usage dashboard,
     * so a clinic gets a heads-up before calls start being refused.
     */
    public function budgetNearLimit(float $threshold = 0.8): bool
    {
        $f = $this->budgetUsedFraction();
        return $f !== null && $f >= $threshold;
    }

    /**
     * Ordered fallback provider rows for a business, used by AiClient for
     * transparent failover. Returns the enabled + configured AiProvider rows
     * whose `name` is listed in config('aicore.fallback_names'), in that order,
     * skipping $excludeName (the profile that was just tried).
     *
     * @return self[]
     */
    public static function fallbacksFor(int $businessId, ?string $excludeName = null): array
    {
        // Fetch everything we might fail over to in one query: the explicit
        // chain slots (fallback/fallback2/fallback3) PLUS every AI service in the
        // library (svc_*), plus the primary (to skip retrying it).
        $chainNames   = array_values(array_filter((array) config('aicore.fallback_names', ['fallback'])));
        $serviceNames = array_map([static::class, 'serviceName'], static::serviceKeys());
        $names        = array_unique(array_merge($chainNames, $serviceNames, $excludeName ? [$excludeName] : []));

        $rows = static::where('business_id', $businessId)
            ->whereIn('name', $names)
            ->get()
            ->keyBy('name');

        $out  = [];
        $seen = [];

        // Never retry the exact provider/endpoint the primary already used
        // (e.g. the quota-exhausted default) — seed its signature as "seen".
        if ($excludeName && ($primary = $rows->get($excludeName))) {
            $seen[static::connectionSig($primary)] = true;
        }

        $add = function (?self $row) use (&$out, &$seen) {
            if (! $row || ! $row->enabled || ! $row->isConfigured()) {
                return;
            }
            $sig = static::connectionSig($row);
            if ($sig === '' || isset($seen[$sig])) {
                return; // unconfigured or a duplicate endpoint — skip
            }
            $seen[$sig] = true;
            $out[] = $row;
        };

        // 1. Explicit failover-chain slots first (admin-defined order).
        foreach ($chainNames as $name) {
            if ($name !== $excludeName) {
                $add($rows->get($name));
            }
        }

        // 2. Then every configured AI service, so ADDING a service automatically
        //    makes it a failover target even if the chain slots weren't wired.
        foreach ($serviceNames as $name) {
            if ($name !== $excludeName) {
                $add($rows->get($name));
            }
        }

        return $out;
    }

    /** Stable signature of a provider connection, for de-duping failover targets. */
    protected static function connectionSig(self $row): string
    {
        return strtolower(trim(($row->provider ?? '') . '|' . ($row->base_url ?? '') . '|' . ($row->model ?? '')));
    }

    /**
     * Resolve the effective SYSTEM prompt for a clinical task transaction: the
     * per-business override stored in meta['prompts'][$key] if non-empty, else
     * the built-in default from {@see AiPrompts}. {placeholders} in $replace are
     * substituted (e.g. ['specialty' => 'cardiology']).
     */
    public function prompt(string $key, array $replace = []): string
    {
        $text = '';
        $meta = $this->meta;
        if (is_array($meta) && !empty($meta['prompts'][$key]) && is_string($meta['prompts'][$key])) {
            $text = trim($meta['prompts'][$key]);
        }
        if ($text === '') {
            $text = AiPrompts::default($key, $this->vertical());
        }
        foreach ($replace as $k => $v) {
            $text = str_replace('{' . $k . '}', (string) $v, $text);
        }
        return $text;
    }
}
