using System;
using System.Text;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
using UnityEngine.Networking;

namespace VibeCopilot.Editor
{
    // O "directie" clickabila (stil mint.gg): titlu scurt + promptul bogat trimis la click.
    [Serializable]
    public class DirectionOption
    {
        public string title;   // eticheta scurta ("Sleek European luxury sedan")
        public string prompt;  // descrierea bogata trimisa in chat cand userul da click
    }
    [Serializable] internal class DirectionsWrap { public DirectionOption[] directions; }

    // Raspunsul venit (prin serverul tau) de la AI.
    public class ChatResponse
    {
        public string Reply;       // text prietenos afisat in chat (fara cod)
        public string RawReply;    // raspunsul complet (cu blocul) - pt memoria conversatiei
        public string Code;
        public string FileName;
        public string AttachTarget; // pe ce obiect existent se pune scriptul ("new" = obiect nou)
        public string ActionsJson;
        public DirectionOption[] Directions;  // optiuni clickabile (mint-style); null daca nu-s
        public int TokensUsed;
        public float TokensLeft;
        public int MaxTokens;
        public bool Blocked;
        public bool HasCredits;   // true daca serverul a trimis solduri reale (sa nu suprascriem cu 0 la erori)
    }

    // Pluginul vorbeste cu SERVERUL tau (Vercel), nu direct cu Gemini.
    // Cheia AI sta pe server, NU aici => clientii nu o pot fura.
    public static class VibeBackendClient
    {
        private const string ServerUrl = "https://server-five-ebon-79.vercel.app/api/chat";

        // Tokenul userului (creditele lui). Introdus de user in plugin DUPA ce cumpara
        // (il primeste pe pagina de succes dupa plata Stripe). Salvat in EditorPrefs.
        private const string UserTokenKey = "VibeCopilot.UserToken";
        public static string UserToken
        {
            get => EditorPrefs.GetString(UserTokenKey, "").Trim();
            set => EditorPrefs.SetString(UserTokenKey, (value ?? "").Trim());
        }
        public static bool HasToken => !string.IsNullOrEmpty(UserToken);

        private const int DefaultTotal = 100000;
        private const string TokensKey = "VibeCopilot.TokensLeft";
        private const string TotalKey = "VibeCopilot.TokensTotal";
        private const string BrainKey = "VibeCopilot.Brain";

        // Soldul afisat — actualizat din raspunsul serverului (serverul e sursa de adevar).
        public static float TokensLeft => EditorPrefs.GetFloat(TokensKey, DefaultTotal);
        public static int Max => EditorPrefs.GetInt(TotalKey, DefaultTotal);
        private static void SetTokensLeft(float v) => EditorPrefs.SetFloat(TokensKey, Mathf.Max(0f, v));
        private static void SetTotal(int v) => EditorPrefs.SetInt(TotalKey, v);
        public static void ResetTokens() => SetTokensLeft(Max);

        // ---- Contor SEPARAT pt generari 3D (modele) — numarate pe bucata ----
        private const int DefaultModels = 12;
        private const string ModelsKey = "VibeCopilot.ModelsLeft";
        private const string ModelsTotalKey = "VibeCopilot.ModelsTotal";
        public static int ModelsLeft => EditorPrefs.GetInt(ModelsKey, DefaultModels);
        public static int ModelsMax => EditorPrefs.GetInt(ModelsTotalKey, DefaultModels);
        // Actualizat din raspunsurile serverului (chat sau gen3d). total<=0 => ignoram (eroare/raspuns vechi).
        public static void UpdateModels(int left, int total)
        {
            if (total > 0)
            {
                EditorPrefs.SetInt(ModelsTotalKey, total);
                EditorPrefs.SetInt(ModelsKey, Mathf.Clamp(left, 0, total));
            }
        }

        // ---- REZERVORUL de tokeni 3D (preview + mesh + piese), numarul REAL de pe server ----
        // Astea sunt cifrele adevarate din cont; `ModelsLeft` de mai sus e doar o traducere
        // aproximativa in "cate modele mai ies", pastrata pt compatibilitate.
        private const string GenTokKey = "VibeCopilot.GenTokensLeft";
        private const string GenTokTotalKey = "VibeCopilot.GenTokensTotal";
        public static int GenTokensLeft => EditorPrefs.GetInt(GenTokKey, 0);
        public static int GenTokensMax => EditorPrefs.GetInt(GenTokTotalKey, 0);
        public static void UpdateGenTokens(int left, int total)
        {
            if (total > 0)
            {
                EditorPrefs.SetInt(GenTokTotalKey, total);
                EditorPrefs.SetInt(GenTokKey, Mathf.Clamp(left, 0, total));
            }
        }

        // Creier ales — nume de BRAND (modelul real e ascuns clientului). Maparea reala e pe server:
        //  Copilot Flash = Gemini Flash (ieftin/rapid), Copilot Pro = Gemini Pro,
        //  Copilot Ultra = momentan Gemini Pro, la final Claude.
        public static readonly string[] BrainOptions = { "Copilot Flash", "Copilot Pro", "Copilot Ultra" };
        public static string Brain
        {
            get
            {
                string b = EditorPrefs.GetString(BrainKey, "Copilot Pro");
                return Array.IndexOf(BrainOptions, b) >= 0 ? b : "Copilot Pro";
            }
            set => EditorPrefs.SetString(BrainKey, value);
        }

        public static void SendChat(Turn[] history, string sceneContext, Action<ChatResponse> onComplete)
        {
            if (!HasToken)
            {
                onComplete(new ChatResponse { Reply = "Add your access key first — open settings (gear icon) and paste the key from your purchase." });
                return;
            }
            string json = JsonUtility.ToJson(new ServerRequest
            {
                token = UserToken, messages = history, smart = true, scene = sceneContext, brain = Brain, models3d = VibeGen3D.CachedSlugs()
            });
            SendRequest(json, onComplete);
        }

        // "Fix Error": trimite eroarea + codul la AI (mod "fix") si primeste fisierul corectat.
        public static void SendFix(string userPrompt, Action<ChatResponse> onComplete)
        {
            if (!HasToken)
            {
                onComplete(new ChatResponse { Reply = "Add your access key first — open settings (gear icon) and paste the key from your purchase." });
                return;
            }
            Turn[] turns = new Turn[] { new Turn { role = "user", text = userPrompt } };
            string json = JsonUtility.ToJson(new ServerRequest
            {
                token = UserToken, messages = turns, smart = true, mode = "fix", brain = Brain
            });
            SendRequest(json, onComplete);
        }

        // Trimite un request gata-format la server si intoarce raspunsul parsat.
        private static void SendRequest(string json, Action<ChatResponse> onComplete)
        {
            SendRequest(json, onComplete, 2); // pana la 2 reincercari la eroare de conexiune
        }

        private static void SendRequest(string json, Action<ChatResponse> onComplete, int retriesLeft)
        {
            byte[] payload = Encoding.UTF8.GetBytes(json);

            UnityWebRequest req = new UnityWebRequest(ServerUrl, "POST");
            req.uploadHandler = new UploadHandlerRaw(payload);
            req.downloadHandler = new DownloadHandlerBuffer();
            req.SetRequestHeader("Content-Type", "application/json");
            // Plasa de siguranta: serverul are oricum max 60s, deci dupa 75s ceva e cert blocat.
            // Fara asta, animatia "thinking" se putea invarti la infinit daca conexiunea atarna.
            req.timeout = 75;
            UnityWebRequestAsyncOperation op = req.SendWebRequest();

            EditorApplication.CallbackFunction tick = null;
            tick = () =>
            {
                if (!op.isDone) return;
                EditorApplication.update -= tick;

                // Eroare de CONEXIUNE (cod 0) — de obicei o conexiune keep-alive ramasa
                // "moarta" intre cereri (Vercel o inchide cat scrie userul). O cerere noua
                // deschide o conexiune noua, deci reincercam automat. Asta scotea "server error (0)"
                // din 2 in 2 mesaje.
                if (req.result == UnityWebRequest.Result.ConnectionError && retriesLeft > 0)
                {
                    req.Dispose();
                    SendRequest(json, onComplete, retriesLeft - 1);
                    return;
                }

                ChatResponse resp = ParseServer(req);

                // Serverul e sursa de adevar pt credite: cache-uim ce ne-a trimis.
                if (resp.HasCredits)
                {
                    SetTokensLeft(resp.TokensLeft);
                    if (resp.MaxTokens > 0) SetTotal(resp.MaxTokens);
                }

                req.Dispose();
                onComplete(resp);
            };
            EditorApplication.update += tick;
        }

        private static ChatResponse ParseServer(UnityWebRequest req)
        {
            if (req.result != UnityWebRequest.Result.Success)
            {
                if (req.responseCode == 401)
                    return new ChatResponse { Reply = "Your access token is invalid or missing." };
                if (req.responseCode == 429)
                    return new ChatResponse { Reply = "The AI is rate-limited right now. Please wait ~30s and try again." };
                if (req.responseCode == 503 || req.responseCode == 502 || req.responseCode == 500)
                    return new ChatResponse { Reply = "The AI is very busy right now (high demand). Please try again in a few seconds." };
                return new ChatResponse
                {
                    Reply = "Server error (" + req.responseCode + "):\n" + req.downloadHandler.text
                };
            }

            ServerResponse data = null;
            try { data = JsonUtility.FromJson<ServerResponse>(req.downloadHandler.text); }
            catch { }

            if (data == null)
                return new ChatResponse { Reply = "(empty response)" };

            // Contor generari 3D (daca serverul l-a trimis) — reimprospatam cache-ul local.
            if (data.models_total > 0) UpdateModels(data.models_left, data.models_total);
            // Rezervorul de tokeni 3D vine la FIECARE raspuns de chat -> contorul din sertar
            // ramane aproape de realitate fara sa mai facem o cerere separata.
            if (data.gen_tokens_total > 0) UpdateGenTokens(data.gen_tokens_left, data.gen_tokens_total);

            bool hasCredits = data.tokens_total > 0;

            // Userul a ramas fara credite.
            if (data.blocked)
                return new ChatResponse
                {
                    Blocked = true, HasCredits = hasCredits,
                    Reply = string.IsNullOrEmpty(data.reply) ? "You're out of credits." : data.reply,
                    TokensLeft = 0f, MaxTokens = data.tokens_total
                };

            if (string.IsNullOrEmpty(data.reply))
                return new ChatResponse { Reply = "(empty response)" };

            // RawReply = textul complet (cu bloc) pt memoria conversatiei.
            // Reply = doar propozitia prietenoasa afisata in chat (fara cod/JSON).
            ChatResponse r = new ChatResponse
            {
                Reply = data.reply, RawReply = data.reply, TokensUsed = data.tokens,
                HasCredits = hasCredits, TokensLeft = data.tokens_left, MaxTokens = data.tokens_total
            };

            string lang;
            string block = ExtractBlock(data.reply, out lang);
            if (!string.IsNullOrEmpty(block))
            {
                // Scoatem blocul din ce vede userul; ramane doar mesajul prietenos.
                string friendly = StripBlock(data.reply);
                r.Reply = string.IsNullOrEmpty(friendly) ? "Done!" : friendly;

                // Bloc de DIRECTII (mint-style) — optiuni clickabile, nu actiuni de scena.
                bool isDirections = block.Contains("\"directions\"");
                bool isJson = (lang != null && lang.ToLowerInvariant().Contains("json"))
                              || (block.TrimStart().StartsWith("{") && block.Contains("\"actions\""));
                if (isDirections)
                {
                    try
                    {
                        DirectionsWrap wrap = JsonUtility.FromJson<DirectionsWrap>(block);
                        if (wrap != null && wrap.directions != null && wrap.directions.Length > 0)
                            r.Directions = wrap.directions;
                    }
                    catch { /* daca parsarea pica, ramane doar textul prietenos */ }
                }
                else if (isJson)
                    r.ActionsJson = block;
                else
                {
                    // Scoatem directiva "// attach: X" si retinem tinta (pe ce obiect se pune scriptul).
                    string target;
                    r.Code = ExtractAttachDirective(block, out target);
                    r.AttachTarget = target;
                    r.FileName = FileNameFromCode(r.Code);
                }
            }
            return r;
        }

        // Scoate primul bloc ```...``` din text + eticheta de limbaj (csharp/json).
        private static string ExtractBlock(string text, out string lang)
        {
            lang = "";
            if (string.IsNullOrEmpty(text)) return null;

            int fence = text.IndexOf("```", StringComparison.Ordinal);
            if (fence < 0)
            {
                // Fara garduri ```: uneori AI-ul scoate JSON-ul de actiuni "gol".
                // Il detectam direct ca sa-l putem executa (si ascunde) oricum.
                string raw = FindRawActionsJson(text);
                if (raw != null) { lang = "json"; return raw; }
                // La fel pentru COD C# fara garduri (altfel scriptul apare brut in chat
                // si nu se aplica deloc — vezi cazul "make the police car drivable").
                string code = FindRawCSharp(text);
                if (code != null) { lang = "csharp"; return code; }
                return null;
            }
            int lineEnd = text.IndexOf('\n', fence);
            if (lineEnd < 0) return null;
            lang = text.Substring(fence + 3, lineEnd - (fence + 3)).Trim();
            int start = lineEnd + 1;
            int end = text.IndexOf("```", start, StringComparison.Ordinal);
            if (end < 0) return null;

            string content = text.Substring(start, end - start).Trim();
            return content.Length == 0 ? null : content;
        }

        // Returneaza textul FARA blocul ```...``` (propozitia prietenoasa dinainte + ce e dupa).
        private static string StripBlock(string text)
        {
            if (string.IsNullOrEmpty(text)) return null;
            int fence = text.IndexOf("```", StringComparison.Ordinal);
            if (fence < 0)
            {
                // Fara garduri: scoatem JSON-ul brut de actiuni din mesajul afisat, daca exista.
                string raw = FindRawActionsJson(text);
                if (raw != null) return text.Replace(raw, "").Trim();
                // La fel pentru codul C# brut — in chat ramane doar propozitia prietenoasa.
                string code = FindRawCSharp(text);
                if (code != null) return text.Replace(code, "").Trim();
                return text.Trim();
            }
            string before = text.Substring(0, fence);
            string after = "";
            int close = text.IndexOf("```", fence + 3, StringComparison.Ordinal);
            if (close >= 0 && close + 3 < text.Length)
                after = text.Substring(close + 3);
            return (before + " " + after).Trim();
        }

        // Gaseste un obiect JSON de actiuni {"actions":...} scos FARA garduri ```.
        // Numara acoladele (ignorand cele din siruri) ca sa prinda corect inchiderea.
        // Intoarce substring-ul JSON sau null daca nu e niciun obiect de actiuni.
        private static string FindRawActionsJson(string text)
        {
            if (string.IsNullOrEmpty(text)) return null;
            if (text.IndexOf("\"actions\"", StringComparison.Ordinal) < 0
                && text.IndexOf("\"directions\"", StringComparison.Ordinal) < 0) return null;
            int open = text.IndexOf('{');
            if (open < 0) return null;

            int depth = 0;
            bool inStr = false;
            char prev = '\0';
            for (int i = open; i < text.Length; i++)
            {
                char c = text[i];
                if (inStr)
                {
                    if (c == '"' && prev != '\\') inStr = false;
                }
                else if (c == '"') inStr = true;
                else if (c == '{') depth++;
                else if (c == '}')
                {
                    depth--;
                    if (depth == 0)
                    {
                        string json = text.Substring(open, i - open + 1);
                        return (json.Contains("\"actions\"") || json.Contains("\"directions\"")) ? json : null;
                    }
                }
                prev = c;
            }
            return null;
        }

        // Gaseste cod C# scos FARA garduri ``` (Flash le omite uneori). Codul incepe la
        // directiva "// attach:" sau la "using ..."/"public class ..." si tine pana la
        // finalul mesajului (dupa bloc AI-ul nu mai scrie nimic, conform promptului).
        private static string FindRawCSharp(string text)
        {
            if (string.IsNullOrEmpty(text)) return null;
            // Trebuie sa arate a script Unity, nu doar sa contina cuvantul "class".
            if (text.IndexOf("MonoBehaviour", StringComparison.Ordinal) < 0) return null;
            int start = text.IndexOf("// attach:", StringComparison.Ordinal);
            if (start < 0)
            {
                int u = text.IndexOf("using ", StringComparison.Ordinal);
                int c = text.IndexOf("public class ", StringComparison.Ordinal);
                start = (u >= 0 && (c < 0 || u < c)) ? u : c;
            }
            if (start < 0) return null;
            string code = text.Substring(start).Trim();
            // Plasa de siguranta: sa fie chiar un script (are corp cu acolade).
            return (code.Contains("{") && code.Contains("}")) ? code : null;
        }

        private static string FileNameFromCode(string code)
        {
            Match m = Regex.Match(code, @"class\s+([A-Za-z_][A-Za-z0-9_]*)");
            return m.Success ? m.Groups[1].Value + ".cs" : "GeneratedScript.cs";
        }

        // Citeste directiva "// attach: NumeObiect" (sau "new") din cod, o scoate, si intoarce codul curat.
        // target = numele obiectului existent pe care se pune scriptul, sau "new", sau null daca lipseste.
        private static string ExtractAttachDirective(string code, out string target)
        {
            target = null;
            if (string.IsNullOrEmpty(code)) return code;

            Match m = Regex.Match(code, @"^\s*//\s*attach:\s*(.+?)\s*$", RegexOptions.Multiline | RegexOptions.IgnoreCase);
            if (!m.Success) return code;

            target = m.Groups[1].Value.Trim();
            // Scoatem linia directivei din cod ca sa ramana un script C# curat.
            return code.Remove(m.Index, m.Length).TrimStart('\r', '\n', ' ', '\t');
        }

        [Serializable] public class Turn { public string role; public string text; }
        [Serializable] private class ServerRequest { public string token; public Turn[] messages; public bool smart; public string scene; public string mode; public string brain; public string models3d; }
        [Serializable] private class ServerResponse { public string reply; public int tokens; public string error; public int tokens_left; public int tokens_total; public bool blocked; public int models_left; public int models_total; public int gen_tokens_left; public int gen_tokens_total; }
    }
}
