using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using TMPro;
using UnityEngine;
using CG.Utils;
using CG.Speech;

namespace CG.Chat
{
    public class ChatOrchestrator : MonoBehaviour
    {
        [Header("Config")]
        public ChatBackendConfig config;

        [Header("UI")]
        public TMP_Text assistantText;

        [Header("Speech adapter (for resume)")]
        public SpeechInputAdapter speechAdapter;

        [Header("Traditional Chinese (OpenCC)")]
        public string openccExePath = "";
        public string openccConfig = "s2twp.json";

        [Header("Conversation")]
        public int maxTurnsToKeep = 8;

        [Header("Timeout")]
        public float requestTimeoutSeconds = 30f;

        private readonly List<ChatMessage> _messages = new();
        private IChatProvider _provider;
        private ITextNormalizer _normalizer;
        private CancellationTokenSource _cts;

        private void Awake()
        {
            if (config == null)
            {
                Debug.LogError("ChatBackendConfig is not assigned.");
                enabled = false;
                return;
            }

            _normalizer = string.IsNullOrWhiteSpace(openccExePath)
                ? new PassthroughNormalizer()
                : new OpenCCExternalProcessNormalizer(openccExePath, openccConfig);

            _provider = BuildProvider(config);
            ResetConversation();
        }

        private IChatProvider BuildProvider(ChatBackendConfig cfg)
        {
            return cfg.provider switch
            {
                ChatBackendConfig.ProviderType.Ollama => new OllamaChatProvider(cfg.ollamaBaseUrl),
                ChatBackendConfig.ProviderType.OpenAICompatible => new OpenAICompatibleChatProvider(cfg.openaiBaseUrl, cfg.openaiApiKey),
                ChatBackendConfig.ProviderType.AnythingLLM => new AnythingLLMChatProvider(),
                _ => new OllamaChatProvider(cfg.ollamaBaseUrl),
            };
        }

        public void ResetConversation()
        {
            _messages.Clear();
            if (!string.IsNullOrWhiteSpace(config.systemPrompt))
                _messages.Add(new ChatMessage("system", config.systemPrompt));
        }

        // Router will call this
        public void EnqueueUserChat(string userTextZhTW)
        {
            _ = SendAsync(userTextZhTW);
        }

        public void CancelCurrent()
        {
            _cts?.Cancel();
        }

        private async Task SendAsync(string userText)
        {
            userText ??= "";
            userText = userText.Trim();
            if (string.IsNullOrWhiteSpace(userText)) return;

            _cts?.Cancel();
            _cts = new CancellationTokenSource();

            // timeout wrapper
            using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(Mathf.Max(5f, requestTimeoutSeconds)));
            using var linked = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token, timeoutCts.Token);
            var ct = linked.Token;

            try
            {
                _messages.Add(new ChatMessage("user", userText));
                TrimHistory();

                if (assistantText != null) assistantText.text = "（思考中…）";

                var reply = await _provider.SendChatAsync(_messages, config.model, ct);
                reply ??= "";

                // Ensure Traditional (even if model outputs simplified)
                var replyZhTW = _normalizer.Normalize(reply);

                _messages.Add(new ChatMessage("assistant", replyZhTW));
                TrimHistory();

                if (assistantText != null) assistantText.text = replyZhTW;
            }
            catch (OperationCanceledException)
            {
                if (assistantText != null) assistantText.text = "（已取消）";
            }
            catch (Exception ex)
            {
                if (assistantText != null) assistantText.text = $"（錯誤）{ex.Message}";
            }
            finally
            {
                // Default strategy: resume listening after reply
                speechAdapter?.ResumeListening();
            }
        }

        private void TrimHistory()
        {
            // Keep system + last N turns (user+assistant pairs)
            // Roughly keep 1 system + (maxTurnsToKeep*2) msgs
            int keep = 1 + maxTurnsToKeep * 2;
            if (_messages.Count <= keep) return;

            // Preserve system if present at index 0
            int start = (_messages.Count - keep);
            if (_messages.Count > 0 && _messages[0].role == "system")
                start = Math.Max(1, start);

            _messages.RemoveRange(1, start - 1);
        }
    }
}
