using System;
using System.IO;
using UnityEngine;

namespace JebAI
{
    /// <summary>
    /// Singleton settings loader. Reads from GameData/JebAI/Settings.cfg.
    /// Hot-readable: edit the cfg in-flight and reload the scene to apply changes.
    /// </summary>
    public class JebAISettings
    {
        private static JebAISettings _instance;
        public static  JebAISettings Instance => _instance ?? (_instance = Load());

        // OpenAI-compatible local server connection (LM Studio, Ollama, etc.)
        public string ApiProvider = "lmstudio";
        public string ApiBaseUrl = null; // Optional; split host/port fields are safer in ConfigNode.
        public string ApiScheme = "http";
        public string ApiHost = "localhost";
        public int    ApiPort = 0;
        public string ChatCompletionsPath = "/v1/chat/completions";
        public string ModelsPath = "/v1/models";
        public string ModelName   = "local-model";
        public int    RequestTimeoutSeconds = 30;

        // LLM token budgets — this backend burns many tokens in hidden reasoning before visible output.
        // Keep these high, then clamp final visible text in OpenAICompatClient.ClampReplyForInteraction.
        public int   MaxTokensPoke  = 220;
        public int   MaxTokensSpeak = 320;
        public int   MaxTokensChat  = 420;
        public int   MaxTokensEvent = 260;
        public float Temperature    = 0.95f;
        public bool  DisableModelThinking = true;
        public string NoThinkDirective = "/no_think";
        public bool  RetryOnReasoningOnly = true;
        public int   ReasoningRetryExtraTokens = 256;

        // UI
        public float SpeechBubbleDuration = 10f;
        public float MultiKerbalDelay     = 2.2f; // Gap between speakers in chained reactions

        // Cooldowns (seconds)
        public float StagingCooldown = 10f;  // Min time between staging reactions
        public float EventCooldown   = 20f;  // Min time between other event reactions

        public static JebAISettings Load()
        {
            var    s    = new JebAISettings();
            string path = Path.Combine(
                KSPUtil.ApplicationRootPath, "GameData", "JebAI", "Settings.cfg");

            if (!File.Exists(path))
            {
                Debug.LogWarning("[JebAI] Settings.cfg not found — using defaults.");
                return s;
            }

            ConfigNode node = ConfigNode.Load(path)?.GetNode("JebAI_Settings");
            if (node == null)
            {
                Debug.LogWarning("[JebAI] Could not parse Settings.cfg.");
                return s;
            }

            bool hasApiBaseUrl = TryRead(node, "ApiBaseUrl", out string apiBaseUrl);
            bool hasApiScheme = TryRead(node, "ApiScheme", out string apiScheme);
            bool hasApiHost = TryRead(node, "ApiHost", out string apiHost);
            bool hasApiPort = TryReadInt(node, "ApiPort", out int apiPort);
            bool hasChatPath = TryRead(node, "ChatCompletionsPath", out string chatPath);
            bool hasModelsPath = TryRead(node, "ModelsPath", out string modelsPath);

            TryGet(node, "ApiProvider", ref s.ApiProvider);

            if (hasApiBaseUrl) s.ApiBaseUrl = apiBaseUrl;
            else if (TryRead(node, "LMStudioUrl", out string legacyUrl)) s.ApiBaseUrl = legacyUrl;

            if (hasApiScheme) s.ApiScheme = apiScheme;
            else if (TryRead(node, "LMStudioScheme", out string legacyScheme)) s.ApiScheme = legacyScheme;

            if (hasApiHost) s.ApiHost = apiHost;
            else if (TryRead(node, "LMStudioHost", out string legacyHost)) s.ApiHost = legacyHost;

            if (hasApiPort) s.ApiPort = apiPort;
            else if (TryReadInt(node, "LMStudioPort", out int legacyPort)) s.ApiPort = legacyPort;

            if (hasChatPath) s.ChatCompletionsPath = chatPath;
            else if (TryRead(node, "LMStudioApiPath", out string legacyChatPath)) s.ChatCompletionsPath = legacyChatPath;

            if (hasModelsPath) s.ModelsPath = modelsPath;

            TryGet(node, "ModelName", ref s.ModelName);
            TryGetInt(node, "RequestTimeoutSeconds", ref s.RequestTimeoutSeconds);
            TryGetFloat(node, "Temperature", ref s.Temperature);
            TryGetInt(node, "MaxTokensPoke", ref s.MaxTokensPoke);
            TryGetInt(node, "MaxTokensSpeak", ref s.MaxTokensSpeak);
            TryGetInt(node, "MaxTokensChat", ref s.MaxTokensChat);
            TryGetInt(node, "MaxTokensEvent", ref s.MaxTokensEvent);
            TryGetBool(node, "DisableModelThinking", ref s.DisableModelThinking);
            TryGet(node, "NoThinkDirective", ref s.NoThinkDirective);
            TryGetBool(node, "RetryOnReasoningOnly", ref s.RetryOnReasoningOnly);
            TryGetInt(node, "ReasoningRetryExtraTokens", ref s.ReasoningRetryExtraTokens);
            TryGetFloat(node, "SpeechBubbleDuration", ref s.SpeechBubbleDuration);
            TryGetFloat(node, "MultiKerbalDelay", ref s.MultiKerbalDelay);
            TryGetFloat(node, "StagingCooldown", ref s.StagingCooldown);
            TryGetFloat(node, "EventCooldown", ref s.EventCooldown);

            Debug.Log($"[JebAI] Settings loaded — provider: {s.GetServerDisplayName()}, model: {s.ModelName} @ {s.GetChatCompletionsUrl()}");
            return s;
        }

        public static void Reload() => _instance = Load();

        // ── Helpers — safe getters that don't throw on missing/malformed values

        public string GetChatCompletionsUrl()
        {
            if (TryGetConfiguredBaseUri(out Uri baseUri))
                return CombineUri(baseUri, ChatCompletionsPath);

            string scheme = string.IsNullOrWhiteSpace(ApiScheme) ? "http" : ApiScheme.Trim();
            string host = string.IsNullOrWhiteSpace(ApiHost) ? "localhost" : ApiHost.Trim();
            int port = GetEffectivePort();
            string path = NormalizePath(ChatCompletionsPath, "/v1/chat/completions");
            return $"{scheme}://{host}:{port}{path}";
        }

        public string GetModelsUrl()
        {
            if (TryGetConfiguredBaseUri(out Uri baseUri))
                return CombineUri(baseUri, ModelsPath);

            string scheme = string.IsNullOrWhiteSpace(ApiScheme) ? "http" : ApiScheme.Trim();
            string host = string.IsNullOrWhiteSpace(ApiHost) ? "localhost" : ApiHost.Trim();
            int port = GetEffectivePort();
            string path = NormalizePath(ModelsPath, "/v1/models");
            return $"{scheme}://{host}:{port}{path}";
        }

        public string GetServerDisplayName()
        {
            string provider = string.IsNullOrWhiteSpace(ApiProvider)
                ? "openai-compatible"
                : ApiProvider.Trim().ToLowerInvariant();

            switch (provider)
            {
                case "lmstudio":
                case "lm-studio":
                case "lm studio":
                    return "LM Studio";
                case "ollama":
                    return "Ollama";
                default:
                    return "OpenAI-compatible local server";
            }
        }

        private bool TryGetConfiguredBaseUri(out Uri uri)
        {
            uri = null;
            if (string.IsNullOrWhiteSpace(ApiBaseUrl)) return false;
            if (!Uri.TryCreate(ApiBaseUrl.Trim(), UriKind.Absolute, out Uri parsed)) return false;
            if (string.IsNullOrWhiteSpace(parsed.Host)) return false;
            uri = parsed;
            return true;
        }

        private int GetEffectivePort()
        {
            if (ApiPort > 0) return ApiPort;

            string provider = string.IsNullOrWhiteSpace(ApiProvider)
                ? string.Empty
                : ApiProvider.Trim().ToLowerInvariant();

            if (provider == "ollama")
                return 11434;

            return 1234;
        }

        private static string NormalizePath(string path, string fallback)
        {
            string value = string.IsNullOrWhiteSpace(path) ? fallback : path.Trim();
            return value.StartsWith("/") ? value : "/" + value;
        }

        private static string CombineUri(Uri baseUri, string path)
        {
            string cleanPath = NormalizePath(path, "/");
            string baseText = $"{baseUri.Scheme}://{baseUri.Host}";
            if (!baseUri.IsDefaultPort)
                baseText += $":{baseUri.Port}";
            return baseText + cleanPath;
        }

        private static void TryGet(ConfigNode n, string key, ref string field)
        {
            string v = n.GetValue(key);
            if (!string.IsNullOrEmpty(v)) field = v;
        }

        private static void TryGetFloat(ConfigNode n, string key, ref float field)
        {
            string v = n.GetValue(key);
            if (!string.IsNullOrEmpty(v) && float.TryParse(v, out float result))
                field = result;
        }

        private static void TryGetInt(ConfigNode n, string key, ref int field)
        {
            string v = n.GetValue(key);
            if (!string.IsNullOrEmpty(v) && int.TryParse(v, out int result))
                field = result;
        }

        private static void TryGetBool(ConfigNode n, string key, ref bool field)
        {
            string v = n.GetValue(key);
            if (!string.IsNullOrEmpty(v) && bool.TryParse(v, out bool result))
                field = result;
        }

        private static bool TryRead(ConfigNode n, string key, out string value)
        {
            value = n.GetValue(key);
            return !string.IsNullOrEmpty(value);
        }

        private static bool TryReadInt(ConfigNode n, string key, out int value)
        {
            value = 0;
            string raw = n.GetValue(key);
            return !string.IsNullOrEmpty(raw) && int.TryParse(raw, out value);
        }
    }
}
