using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace JebAI
{
    /// <summary>
    /// Subscribes to KSP game events and drives multi-Kerbal crew reactions.
    ///
    /// Design rules:
    ///   - ZERO polling. Every reaction fires from a KSP callback, never Update().
    ///   - Cooldowns prevent spam when events fire rapidly (e.g. rapid staging).
    ///   - Sequential dialogue: each Kerbal sees what the previous said and reacts.
    ///   - Speaker selection is personality-weighted (brave leads during staging, etc.)
    ///   - Every event applies a mood change to participating Kerbals before prompting.
    /// </summary>
    [KSPAddon(KSPAddon.Startup.Flight, false)]
    public class EventListener : MonoBehaviour
    {
        public static EventListener Instance { get; private set; }

        private float _lastStagingTime = -999f;
        private float _lastEventTime   = -999f;
        private bool  _reactionRunning = false;

        // ── Unity lifecycle ────────────────────────────────────────────────────

        void Awake()
        {
            if (Instance != null && Instance != this) { Destroy(gameObject); return; }
            Instance = this;
        }

        void Start()
        {
            GameEvents.onStageActivate.Add(OnStageActivate);
            GameEvents.onVesselSituationChange.Add(OnSituationChange);
            GameEvents.onCrash.Add(OnCrash);
            GameEvents.onCrashSplashdown.Add(OnSplashCrash);
            GameEvents.onCrewOnEva.Add(OnEVA);
            GameEvents.onVesselSOIChanged.Add(OnSOIChanged);
        }

        void OnDestroy()
        {
            GameEvents.onStageActivate.Remove(OnStageActivate);
            GameEvents.onVesselSituationChange.Remove(OnSituationChange);
            GameEvents.onCrash.Remove(OnCrash);
            GameEvents.onCrashSplashdown.Remove(OnSplashCrash);
            GameEvents.onCrewOnEva.Remove(OnEVA);
            GameEvents.onVesselSOIChanged.Remove(OnSOIChanged);
        }

        // ── KSP event hooks ────────────────────────────────────────────────────

        private void OnStageActivate(int stage)
        {
            float cooldown = JebAISettings.Instance.StagingCooldown;
            if (Time.time - _lastStagingTime < cooldown) return;
            _lastStagingTime = Time.time;
            TriggerCrewReaction(GameEvent.Staging);
        }

        private void OnSituationChange(
            GameEvents.HostedFromToAction<Vessel, Vessel.Situations> data)
        {
            if (data.host != FlightGlobals.ActiveVessel) return;

            GameEvent? evt = data.to switch
            {
                Vessel.Situations.ORBITING    => GameEvent.OrbitAchieved,
                Vessel.Situations.SUB_ORBITAL => GameEvent.SubOrbital,
                Vessel.Situations.LANDED      => GameEvent.Landing,
                Vessel.Situations.SPLASHED    => GameEvent.Splashdown,
                Vessel.Situations.FLYING
                    when data.from == Vessel.Situations.SUB_ORBITAL
                                      => GameEvent.AtmosphericEntry,
                _ => (GameEvent?)null
            };

            if (evt.HasValue) TriggerEventWithCooldown(evt.Value, data.host);
        }

        private void OnCrash(EventReport report)
        {
            if (report.origin?.vessel != FlightGlobals.ActiveVessel) return;
            TriggerEventWithCooldown(GameEvent.Crash, report.origin?.vessel);
        }

        private void OnSplashCrash(EventReport report)
        {
            if (report.origin?.vessel != FlightGlobals.ActiveVessel) return;
            TriggerEventWithCooldown(GameEvent.Splashdown, report.origin?.vessel);
        }

        private void OnEVA(GameEvents.FromToAction<Part, Part> data)
        {
            Vessel sourceVessel = data.to?.vessel ?? data.from?.vessel ?? FlightGlobals.ActiveVessel;
            TriggerEventWithCooldown(GameEvent.EVA, sourceVessel);
        }

        private void OnSOIChanged(GameEvents.HostedFromToAction<Vessel, CelestialBody> data)
        {
            if (data.host != FlightGlobals.ActiveVessel) return;
            TriggerEventWithCooldown(GameEvent.SphereOfInfluenceChange, data.host);
        }

        // ── Reaction orchestration ─────────────────────────────────────────────

        private void TriggerEventWithCooldown(GameEvent evt, Vessel contextVessel = null)
        {
            float cooldown = JebAISettings.Instance.EventCooldown;
            if (Time.time - _lastEventTime < cooldown) return;
            _lastEventTime = Time.time;
            TriggerCrewReaction(evt, contextVessel);
        }

        /// <summary>
        /// Public entry point - also called by JebAI for low-fuel warnings.
        /// Applies mood changes first so prompts reflect the crew's actual emotional state.
        /// </summary>
        public void TriggerCrewReaction(GameEvent evt, Vessel contextVessel = null)
        {
            if (_reactionRunning) return;

            Vessel vessel = contextVessel ?? FlightGlobals.ActiveVessel;
            if (vessel == null) return;

            var crew = vessel.GetVesselCrew();
            if (crew == null || crew.Count == 0) return;

            // Apply mood to all crew before generating any dialogue
            // — this way the prompt reflects genuine emotional state
            foreach (var c in crew)
                KerbalMoodTracker.ApplyEvent(c.name, evt, c.courage, c.stupidity);

            int maxSpeakers = Mathf.Min(crew.Count, evt == GameEvent.Staging ? 3 : 2);
            var speakers    = SelectSpeakers(crew, evt, maxSpeakers);
            if (speakers.Count == 0) return;

            string ctx = ContextBuilder.BuildEventContext(evt, vessel);
            StartCoroutine(RunConversation(speakers, ctx, evt));

            RecordMemory(evt, vessel);
        }

        // ── Sequential multi-Kerbal dialogue ──────────────────────────────────

        /// <summary>
        /// Kerbals speak in turn. Each one sees what the previous said,
        /// creating a natural chain reaction where personality dynamics emerge.
        /// </summary>
        private IEnumerator RunConversation(
            List<ProtoCrewMember> speakers,
            string                context,
            GameEvent             evt)
        {
            _reactionRunning = true;
            float delay = JebAISettings.Instance.MultiKerbalDelay;

            // Staging gets its own prompt type — it has a distinct, punchy instruction
            // in KerbalPersona vs. the more general EventReaction instruction.
            InteractionType interactionType = evt == GameEvent.Staging
                ? InteractionType.StagingReaction
                : InteractionType.EventReaction;

            string prevSpeaker = null;
            string prevLine    = null;

            for (int i = 0; i < speakers.Count; i++)
            {
                ProtoCrewMember kerbal = speakers[i];

                string system = KerbalPersona.BuildSystemPrompt(
                    kerbal, interactionType,
                    previousSpeaker: prevSpeaker,
                    previousLine:    prevLine);

                string prompt = $"React to this situation. Use it as fuel, not as a checklist to restate:\n{context}";

                bool done = false;
                ModelCallResult result = null;

                if (JebAIMcpLayer.Instance == null)
                {
                    done = true;
                    result = new ModelCallResult
                    {
                        Success = false,
                        Error = "MCP unavailable.",
                    };
                }
                else
                {
                    JebAIMcpLayer.Instance.RequestCompletion(
                        system,
                        null,
                        prompt,
                        JebAISettings.Instance.MaxTokensEvent,
                        $"{evt}:{kerbal.name}",
                        r =>
                        {
                            result = r;
                            done = true;
                        });
                }

                float timeout = JebAISettings.Instance.RequestTimeoutSeconds + 2f;
                while (!done && timeout > 0f)
                {
                    timeout -= Time.deltaTime;
                    yield return null;
                }

                if (!done)
                {
                    Debug.LogWarning($"[JebAI] MCP timeout waiting for {kerbal.name} during {evt}.");
                }
                else if (result != null && result.Success && result.Reply != null)
                {
                    string line = OpenAICompatClient.ClampReplyForInteraction(
                        result.Reply,
                        interactionType);

                    if (!string.IsNullOrWhiteSpace(line))
                    {
                        SpeechBubbleManager.Instance?.Show(kerbal, line);
                        KerbalMemory.AddRecentOutput(kerbal.name, interactionType, line);
                        prevSpeaker = kerbal.name;
                        prevLine    = line;
                        Debug.Log($"[JebAI] [{evt}] {kerbal.name}: {line}");
                    }
                    else
                    {
                        Debug.LogWarning($"[JebAI] Blank visible reply for {kerbal.name} during {evt}.");
                    }
                }
                else
                {
                    string error = result?.Error;
                    if (ShouldUseSyntheticFallback(error))
                    {
                        string synthetic = BuildSyntheticEventFallback(evt);
                        SpeechBubbleManager.Instance?.Show(kerbal, synthetic);
                        KerbalMemory.AddRecentOutput(kerbal.name, interactionType, synthetic);
                        prevSpeaker = kerbal.name;
                        prevLine = synthetic;
                        Debug.LogWarning($"[JebAI] Fallback line used for {kerbal.name} during {evt}: {error}");
                    }
                    else
                    {
                        Debug.LogWarning($"[JebAI] No response for {kerbal.name} during {evt}: {error}");
                    }
                }

                if (i < speakers.Count - 1)
                    yield return new WaitForSeconds(delay);
            }

            _reactionRunning = false;
        }

        // ── Speaker selection ──────────────────────────────────────────────────

        /// <summary>
        /// Personality-weighted speaker ordering.
        /// Brave Kerbals lead in exciting events; anxious ones dominate during crises.
        /// </summary>
        private static List<ProtoCrewMember> SelectSpeakers(
            List<ProtoCrewMember> crew,
            GameEvent             evt,
            int                   maxCount)
        {
            var pool = new List<ProtoCrewMember>(crew);

            switch (evt)
            {
                // Brave, slightly reckless Kerbals speak first when the engines fire
                case GameEvent.Staging:
                case GameEvent.OrbitAchieved:
                case GameEvent.SphereOfInfluenceChange:
                    pool = pool.OrderByDescending(c => c.courage + (1f - c.stupidity) * 0.2f).ToList();
                    break;

                // Cowardly Kerbals are loudest in a crisis — their panic is the most vivid
                case GameEvent.Crash:
                case GameEvent.AtmosphericEntry:
                case GameEvent.LowFuel:
                    pool = pool.OrderBy(c => c.courage).ToList();
                    break;

                // Scientists lead on milestone events — they've been waiting for this data
                case GameEvent.Landing:
                case GameEvent.Splashdown:
                    pool = pool.OrderByDescending(c => c.trait == "Scientist" ? 1 : 0).ToList();
                    break;

                // Pilots lead on manoeuvre events
                case GameEvent.Docking:
                case GameEvent.SubOrbital:
                case GameEvent.EVA:
                    pool = pool.OrderByDescending(c => c.trait == "Pilot" ? 1 : 0).ToList();
                    break;

                default:
                    Shuffle(pool);
                    break;
            }

            return pool.Take(maxCount).ToList();
        }

        // ── Memory recording ───────────────────────────────────────────────────

        private static void RecordMemory(GameEvent evt, Vessel v)
        {
            string memory = evt switch
            {
                GameEvent.OrbitAchieved          => $"Achieved orbit around {v.mainBody.displayName}.",
                GameEvent.Landing                 => $"Landed on {v.mainBody.displayName}.",
                GameEvent.Splashdown              => $"Splashed down in the ocean of {v.mainBody.displayName}.",
                GameEvent.Crash                   => $"Survived a crash near {v.mainBody.displayName}.",
                GameEvent.EVA                     => $"Performed an EVA at {v.altitude:F0}m above {v.mainBody.displayName}.",
                GameEvent.SphereOfInfluenceChange => $"Entered the sphere of influence of {v.mainBody.displayName}.",
                GameEvent.AtmosphericEntry        => $"Survived re-entry at {v.mainBody.displayName}.",
                _ => null
            };

            if (memory == null) return;
            foreach (var c in v.GetVesselCrew())
                KerbalMemory.AddMemory(c.name, memory);
        }

        // ── Utilities ──────────────────────────────────────────────────────────

        private static readonly System.Random Rng = new System.Random();

        private static bool ShouldUseSyntheticFallback(string error)
        {
            if (string.IsNullOrWhiteSpace(error)) return false;
            string lower = error.ToLowerInvariant();
            return lower.Contains("reasoning content")
                || lower.Contains("empty choices")
                || lower.Contains("blank message content")
                || lower.Contains("parse failed");
        }

        private static string BuildSyntheticEventFallback(GameEvent evt)
        {
            switch (evt)
            {
                case GameEvent.Staging:
                    return "Stage lit, hold steady!";
                case GameEvent.OrbitAchieved:
                    return "Orbit confirmed, breathing easier now.";
                case GameEvent.SubOrbital:
                    return "Suborbital arc locked in, eyes up.";
                case GameEvent.AtmosphericEntry:
                    return "Re-entry is biting, stay sharp.";
                case GameEvent.Landing:
                    return "Touchdown. Finally solid ground.";
                case GameEvent.Splashdown:
                    return "Splashdown confirmed, systems holding.";
                case GameEvent.Crash:
                    return "Impact! Check integrity now.";
                case GameEvent.EVA:
                    return "EVA complete, tether and breathe.";
                case GameEvent.Docking:
                    return "Closing in, keep it precise.";
                case GameEvent.LowFuel:
                    return "Fuel's thin. No wasted burns.";
                case GameEvent.SphereOfInfluenceChange:
                    return "New sphere of influence. New rules.";
                default:
                    return "Comms are rough, but we're still in it.";
            }
        }

        private static void Shuffle<T>(List<T> list)
        {
            for (int i = list.Count - 1; i > 0; i--)
            {
                int j = Rng.Next(i + 1);
                (list[i], list[j]) = (list[j], list[i]);
            }
        }
    }
}
