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

namespace VibeCopilot.Editor
{
    // Genereaza modele 3D REALE prin serverul nostru (care cheama Tripo) si le pune in scena.
    // Flux async: porneste task -> intreaba statusul la 5s -> descarca .glb -> import (glTFast) -> instantiaza.
    // CACHE: daca acelasi prompt a mai fost generat, refoloseste fisierul (gratis, instant).
    public static class VibeGen3D
    {
        private const string GenUrl = "https://server-five-ebon-79.vercel.app/api/gen3d";
        private const string GenDir = "Assets/VibeCopilot/Generated3D";
        // Cache local de reutilizare OPRIT: fiecare generare = model NOU, unic (nu refolosim un
        // .glb vechi pt acelasi cuvant). Pune true daca vrei sa revii la refolosire locala.
        private const bool CacheReuse = false;

        // Textul scris de USER (setat de fereastra). NU mai e folosit pt cheia de cache
        // (formularea userului varia prea mult: "add a red car", "put a car here" -> chei
        // diferite -> regenera). Il pastram doar pt context/diagnostic.
        public static string CurrentUserText = "";

        // Se declanseaza cand serverul a facut POZA de concept si asteapta aprobarea userului.
        // Fereastra de chat se aboneaza, arata poza + butoanele [Generate 3D] / [Make changes].
        // Meshul (care costa) se face abia la apasarea "Generate 3D" -> VibeGen3D.Commit(pending).
        public static event Action<PendingGen> PreviewReady;

        // Se declanseaza cand fluxul se rezolva FARA poza de concept (model luat din biblioteca,
        // blocat, sau esec). Fereastra opreste spinnerul de "thinking"; daca `note` e nenul, il
        // arata ca mesaj scurt. Asa spinnerul nu ramane agatat cand nu apare nicio poza.
        public static event Action<string> PreviewSkipped;

        // Progres REAL al generarii meshului — ACELASI numar care apare in Console
        // ("Generating 3D... N%"). Fereastra il foloseste ca sa umple bara de pe cardul de loading,
        // deci animatia e sincronizata cu procesul adevarat, nu estimata.
        // genId = identificatorul cardului care afiseaza generarea (leaga evenimentul de card).
        public static event Action<string, int> GenProgress;    // (genId, 0..100)
        public static event Action<string, bool> GenFinished;   // (genId, a reusit?)

        // (RIGID BODY) Taierea in piese e un pas SEPARAT, care ruleaza dupa ce meshul e deja
        // in scena. Are eveniment propriu, ca sa poata cadea singur fara sa strice generarea:
        // daca segmentarea pica, userul ramane cu modelul intreg, care a reusit.
        public static event Action<string, bool> RigidFinished; // (genId, au iesit piesele?)

        // Generarile pornite si neterminate. E STATIC, deci se goleste la orice recompilare de
        // scripturi (domain reload) — exact cand moare si pollingul. Fereastra o intreaba la
        // pornire ca sa nu lase un card agatat, cu orbita invartindu-se la infinit.
        private static readonly System.Collections.Generic.HashSet<string> ActiveGens =
            new System.Collections.Generic.HashSet<string>();

        public static bool IsGenActive(string genId)
        {
            return !string.IsNullOrEmpty(genId) && ActiveGens.Contains(genId);
        }

        // Acelasi mecanism, pentru pasul de taiere in piese: si el moare la recompilare,
        // altfel cardul ar ramane agatat pe "Splitting into parts…" la infinit.
        private static readonly System.Collections.Generic.HashSet<string> ActiveRigids =
            new System.Collections.Generic.HashSet<string>();

        public static bool IsRigidActive(string genId)
        {
            return !string.IsNullOrEmpty(genId) && ActiveRigids.Contains(genId);
        }

        private static void Prog(string id, int pct)
        {
            if (string.IsNullOrEmpty(id)) return;
            try { if (GenProgress != null) GenProgress(id, Mathf.Clamp(pct, 0, 100)); } catch { }
        }
        private static void Fin(string id, bool ok)
        {
            if (string.IsNullOrEmpty(id)) return;
            ActiveGens.Remove(id);
            // PLASA: daca meshul a picat (oriunde), nu mai are ce sa fie taiat in piese.
            // Inchidem pasul de rigid body aici, o data pentru toate caile de eroare.
            if (!ok && ActiveRigids.Contains(id)) RigidFin(id, false);
            try { if (GenFinished != null) GenFinished(id, ok); } catch { }
        }
        private static void RigidFin(string id, bool ok)
        {
            if (string.IsNullOrEmpty(id)) return;
            ActiveRigids.Remove(id);
            try { if (RigidFinished != null) RigidFinished(id, ok); } catch { }
        }

        // Tot ce trebuie ca sa facem meshul dupa ce userul aproba poza. Serializabil ->
        // fereastra il tine in mesajul de chat, deci butoanele merg si dupa un rebuild/reload.
        [Serializable]
        public class PendingGen
        {
            public string imageUrl;   // poza de concept (o pasam lui Tripo la Commit)
            public string ext;        // jpg / png
            public string prompt;      // descrierea (fallback text->3D)
            public string slug;        // cheia de cache / nume fisier
            public string name;        // numele obiectului in scena
            public float px, py, pz;   // pozitia in scena
            public bool custom;        // varianta unica (fara biblioteca/cache)
            public string blackUrl;    // aceeasi poza pe fundal NEGRU -> difference matting (decupaj exact)
            public bool preCut;        // poza vine DEJA transparenta (Photoroom) -> o afisam direct, fara matting
        }

        // Apelata din VibeSceneActions pt actiunea {"action":"generate3d", "prompt":..., "name":..., "position":...}.
        public static void Generate(string prompt, string name, Vector3 position, bool custom = false)
        {
            if (string.IsNullOrEmpty(prompt)) { Debug.LogWarning("[VibeCopilot] generate3d called without a prompt."); return; }
            Directory.CreateDirectory(GenDir);

            // Normalizam SINONIMELE (human/guy/dude/... -> person) ca sa refolosim acelasi model
            // 3D din cache in loc sa regeneram degeaba (economie credite + Tripo). E DETERMINIST
            // -> merge pe TOATE tier-urile (Flash/Pro/Ultra), nu depinde ca AI-ul sa "inteleaga".
            string originalPrompt = prompt;
            prompt = Canonical(prompt);

            // Cheia de cache = promptul CANONIC al AI-ului. Serverul il forteaza sa fie doar
            // obiectul ("car", nu "a red sports car"), deci e acelasi indiferent cum formuleaza
            // userul: "make a car" / "add a car" / "fa o masina" -> AI da "car" -> acelasi
            // fisier din cache, FARA regenerare. Un adjectiv cerut explicit ("red car") da alt
            // slug -> alt model, ceea ce e corect (e alt obiect).
            // CUSTOM ("custom tree"): userul vrea o varianta UNICA -> slug local unic (nu
            // atinge fisierul din cache/biblioteca al obiectului de baza) si fara cache-hit.
            string slug = Slug(prompt);
            if (custom) slug = slug + "_c" + DateTime.UtcNow.ToString("yyMMddHHmmss");
            string assetPath = GenDir + "/" + slug + ".glb";

            // DIAGNOSTIC: ce folosim pt cheie/cale si daca exista deja
            Debug.Log("[VibeCopilot] generate3d: user=\"" + CurrentUserText + "\" aiPrompt=\"" + originalPrompt + "\" canon=\"" + prompt + "\" slug=\"" + slug +
                      (custom ? "\" CUSTOM" : "\"") + " exists=" + File.Exists(ToAbs(assetPath)));

            // CACHE local OPRIT (CacheReuse=false): generam mereu unic, nu refolosim .glb-ul vechi.
            if (CacheReuse && !custom && File.Exists(ToAbs(assetPath)))
            {
                Debug.Log("[VibeCopilot] Model from cache: \"" + prompt + "\" (reused, no cost).");
                PlaceModel(assetPath, name, position);
                Skip("Added it to your scene.");   // opreste spinnerul din fereastra
                return;
            }

            // Modelul nu e in cache -> il vom GENERA (costa o generare 3D). Avem nevoie de token.
            if (!VibeBackendClient.HasToken)
            {
                Debug.LogWarning("[VibeCopilot] Add your access key first (settings) to generate new 3D models.");
                Skip("Add your access key in settings to generate 3D models.");
                return;
            }

            // FLUX ca mint.gg: NU generam meshul direct. Cerem intai o POZA de concept (gratis),
            // o aratam in chat cu butoanele [Generate 3D] / [Make changes]. Meshul (care costa) se
            // face abia cand userul apasa "Generate 3D" -> Commit(). Asa userul aproba intai ce vede.
            Debug.Log("[VibeCopilot] Previewing 3D concept: \"" + prompt + "\"" + (custom ? " (custom)" : "") + "...");
            RequestPreview(prompt, slug, custom, name, position);
        }

        // ---- pas 1: cerem o POZA de concept (op:preview). Fara mesh, fara cost. ----
        // Daca modelul e deja in biblioteca -> serverul da URL-ul direct si il punem pe loc (gratis).
        // Altfel primim image_url -> declansam PreviewReady ca fereastra sa arate poza + butoane.
        private static void RequestPreview(string prompt, string slug, bool custom, string name, Vector3 position)
        {
            string json = JsonUtility.ToJson(new PreviewReq { op = "preview", prompt = prompt, token = VibeBackendClient.UserToken, slug = slug, lib = 1, custom = custom ? 1 : 0 });
            PostPreview(json, prompt, slug, custom, name, position);
        }

        // CALEA A: userul a ATASAT o poza (buton 📎) -> generam 3D direct din poza lui (op:preview cu
        // body.image). Poza = varianta unica (custom), nu se cache-uieste. Restul fluxului (card,
        // Generate 3D, mesh) e identic cu calea din text.
        public static void GenerateFromImage(string b64, string mime, string name, Vector3 position)
        {
            if (string.IsNullOrEmpty(b64)) return;
            Directory.CreateDirectory(GenDir);
            if (!VibeBackendClient.HasToken)
            {
                Debug.LogWarning("[VibeCopilot] Add your access key first (settings) to generate 3D models.");
                Skip("Add your access key in settings to generate 3D models.");
                return;
            }
            string slug = "userimg" + DateTime.UtcNow.ToString("yyMMddHHmmssfff");
            string json = JsonUtility.ToJson(new PreviewImgReq
            {
                op = "preview", prompt = "", token = VibeBackendClient.UserToken, slug = slug, lib = 1, custom = 1,
                image = new ImgData { b64 = b64, mime = mime },
            });
            Debug.Log("[VibeCopilot] Previewing 3D from your photo...");
            PostPreview(json, "", slug, true, name, position);
        }

        // Handler comun pt op:preview (text SAU poza userului). Acelasi rezultat: card cu poza + butoane.
        private static void PostPreview(string json, string prompt, string slug, bool custom, string name, Vector3 position)
        {
            Post(json, resp =>
            {
                PreviewResp s = null;
                try { s = JsonUtility.FromJson<PreviewResp>(resp); } catch { }
                if (s == null)
                {
                    Debug.LogWarning("[VibeCopilot] Couldn't start 3D preview.");
                    Skip("Couldn't start the preview. Please try again.");
                    return;
                }
                if (s.models_total > 0) VibeBackendClient.UpdateModels(s.models_left, s.models_total);
                if (s.gen_tokens_total > 0) VibeBackendClient.UpdateGenTokens(s.gen_tokens_left, s.gen_tokens_total);
                if (s.blocked)
                {
                    string msg = string.IsNullOrEmpty(s.reply) ? "You're out of 3D generations. Top up to make more models." : s.reply;
                    Debug.LogWarning("[VibeCopilot] " + msg);
                    Skip(msg);
                    return;
                }
                // Model existent in biblioteca -> instant si gratis, fara pasul de preview.
                if (!string.IsNullOrEmpty(s.url))
                {
                    Debug.Log("[VibeCopilot] Model from the VibeCopilot library (instant, no generation used).");
                    string assetPath = GenDir + "/" + slug + ".glb";
                    DownloadGlb(s.url, assetPath, () => { PlaceModel(assetPath, name, position); Skip("Added it to your scene from the 3D library."); });
                    return;
                }
                // Avem poza de concept -> o dam ferestrei; meshul se face la apasarea butonului.
                if (!string.IsNullOrEmpty(s.image_url))
                {
                    var p = new PendingGen
                    {
                        imageUrl = s.image_url, ext = s.image_ext, blackUrl = s.image_black_url, preCut = s.pre_cut, prompt = prompt, slug = slug,
                        name = name, px = position.x, py = position.y, pz = position.z, custom = custom,
                    };
                    try { if (PreviewReady != null) PreviewReady(p); } catch { }
                    return;
                }
                // Preview esuat -> plasa de siguranta: facem direct meshul (vechea cale), sa nu ramana userul fara model.
                Debug.LogWarning("[VibeCopilot] Preview image failed — generating the model directly.");
                Skip("Generating your 3D model…");
                Commit(new PendingGen { imageUrl = null, ext = null, prompt = prompt, slug = slug, name = name, px = position.x, py = position.y, pz = position.z, custom = custom });
            });
        }

        // Anunta fereastra ca fluxul s-a rezolvat FARA poza (opreste spinnerul, arata note-ul daca e).
        private static void Skip(string note) { try { if (PreviewSkipped != null) PreviewSkipped(note); } catch { } }

        // ---- pas 2 (dupa apasarea "Generate 3D"): facem MESHUL din poza de concept. Costa o generare. ----
        // Trimitem preview_url ca Tripo sa foloseasca poza deja facuta (image->3D), fara s-o refaca.
        // genId (optional) = cardul de loading care asculta progresul; null = fara raportare.
        // `rigid` = dupa ce meshul e in scena, il mai taiem o data in piese cu fizica
        // (pas separat, taxat separat). Vezi Segment().
        public static void Commit(PendingGen p, string genId = null, bool rigid = false)
        {
            if (p == null) return;
            Directory.CreateDirectory(GenDir);
            if (!string.IsNullOrEmpty(genId)) ActiveGens.Add(genId);
            // Marcam taierea in piese ca "in lucru" DE ACUM, nu abia cand incepe: altfel, in
            // fereastra dintre terminarea meshului si pornirea segmentarii, cardul ar fi
            // considerat abandonat si ar sari pe "Could not split".
            if (rigid && !string.IsNullOrEmpty(genId)) ActiveRigids.Add(genId);
            if (!VibeBackendClient.HasToken)
            {
                Debug.LogWarning("[VibeCopilot] Add your access key first (settings) to generate 3D models.");
                Fin(genId, false);
                return;
            }
            // Nume de fisier UNIC pt fiecare generare -> asset 3D distinct, nu suprascrie un model
            // deja pus in scena (cache-ul de reutilizare e oprit; fiecare "car" e altul).
            string fileSlug = p.slug + "_" + DateTime.UtcNow.ToString("yyMMddHHmmssfff");
            string assetPath = GenDir + "/" + fileSlug + ".glb";
            Vector3 pos = new Vector3(p.px, p.py, p.pz);
            Debug.Log("[VibeCopilot] Generating 3D model: \"" + p.prompt + "\" — takes ~1 min...");

            string json = JsonUtility.ToJson(new StartReq
            {
                op = "start", prompt = p.prompt, token = VibeBackendClient.UserToken, slug = p.slug,
                lib = 1, custom = p.custom ? 1 : 0, preview_url = p.imageUrl, preview_ext = p.ext,
            });
            Post(json, resp =>
            {
                StartResp s = null;
                try { s = JsonUtility.FromJson<StartResp>(resp); } catch { }
                if (s == null) { Debug.LogWarning("[VibeCopilot] Couldn't start 3D generation."); Fin(genId, false); return; }
                if (s.models_total > 0) VibeBackendClient.UpdateModels(s.models_left, s.models_total);
                if (s.gen_tokens_total > 0) VibeBackendClient.UpdateGenTokens(s.gen_tokens_left, s.gen_tokens_total);
                if (s.blocked)
                {
                    Debug.LogWarning("[VibeCopilot] " + (string.IsNullOrEmpty(s.reply) ? "You're out of 3D generations. Top up to make more models." : s.reply));
                    Fin(genId, false);
                    return;
                }
                if (!string.IsNullOrEmpty(s.url))
                {
                    Prog(genId, 90);   // gata de la server -> mai ramane descarcarea + importul
                    DownloadGlb(s.url, assetPath, () =>
                    {
                        PlaceModel(assetPath, p.name, pos); Prog(genId, 100); Fin(genId, true);
                        // Model luat din BIBLIOTECA: nu are un task Tripo al nostru, deci
                        // segmentarea nu are de ce sa se agate. Nu se taxeaza nimic.
                        if (rigid)
                        {
                            Debug.LogWarning("[VibeCopilot] This model came from the library, so it can't be split into parts.");
                            RigidFin(genId, false);
                        }
                    });
                    return;
                }
                if (string.IsNullOrEmpty(s.task_id))
                {
                    Debug.LogWarning("[VibeCopilot] Couldn't start 3D generation.");
                    Fin(genId, false); if (rigid) RigidFin(genId, false); return;
                }
                Prog(genId, 3);        // task pornit — bara pleaca de la zero vizibil
                string meshTask = s.task_id;   // ne trebuie mai tarziu ca sursa pt segmentare
                PollUntilDone(meshTask, genId, url =>
                {
                    if (string.IsNullOrEmpty(url))
                    {
                        Debug.LogWarning("[VibeCopilot] 3D generation failed or timed out.");
                        Fin(genId, false); if (rigid) RigidFin(genId, false); return;
                    }
                    // Tripo a terminat (100%), dar mai avem descarcare + import: le tinem in ultimii 10%.
                    Prog(genId, 90);
                    DownloadGlb(url, assetPath, () =>
                    {
                        GameObject placed = PlaceModel(assetPath, p.name, pos);
                        Prog(genId, 100); Fin(genId, true);
                        if (rigid) Segment(meshTask, p, pos, fileSlug, placed, genId);
                    });
                });
            });
        }

        // (RIGID BODY) Pasul 2: taiem modelul deja generat in piese si le dam fizica.
        // Meshul intreg e DEJA in scena — daca segmentarea reuseste, il inlocuim cu varianta
        // in piese; daca pica, userul ramane cu modelul intreg, adica exact ce a platit la pasul 1.
        // Serverul taxeaza SEG_COST doar daca Tripo accepta task-ul (vezi op:"segment").
        private static void Segment(string meshTaskId, PendingGen p, Vector3 pos, string fileSlug, GameObject placed, string genId)
        {
            if (string.IsNullOrEmpty(meshTaskId) || p == null) { RigidFin(genId, false); return; }
            string segPath = GenDir + "/" + fileSlug + "_parts.glb";
            Debug.Log("[VibeCopilot] Splitting the model into physics parts — takes ~1 min...");

            string json = JsonUtility.ToJson(new SegReq
            {
                op = "segment", task_id = meshTaskId, token = VibeBackendClient.UserToken,
            });
            Post(json, resp =>
            {
                SegResp s = null;
                try { s = JsonUtility.FromJson<SegResp>(resp); } catch { }
                if (s == null || s.blocked || string.IsNullOrEmpty(s.task_id))
                {
                    Debug.LogWarning("[VibeCopilot] " + ((s != null && !string.IsNullOrEmpty(s.reply))
                        ? s.reply : "Couldn't split this model into parts."));
                    RigidFin(genId, false);
                    return;
                }
                if (s.models_total > 0) VibeBackendClient.UpdateModels(s.models_left, s.models_total);
                if (s.gen_tokens_total > 0) VibeBackendClient.UpdateGenTokens(s.gen_tokens_left, s.gen_tokens_total);

                // Acelasi op:"status" ca la mesh. genId = null -> nu raportam progres pe bara
                // cardului, care e deja pe "gata"; starea pieselor se arata pe linia ei.
                PollUntilDone(s.task_id, null, url =>
                {
                    if (string.IsNullOrEmpty(url))
                    {
                        Debug.LogWarning("[VibeCopilot] Splitting into parts failed or timed out. The whole model is still in your scene.");
                        RigidFin(genId, false);
                        return;
                    }
                    DownloadGlb(url, segPath, () =>
                    {
                        GameObject parts = PlaceModel(segPath, p.name, pos);
                        if (parts == null) { RigidFin(genId, false); return; }
                        int n = ApplyPhysics(parts);
                        if (n == 0)
                        {
                            // A venit un .glb fara piese separate -> nu are rost sa inlocuim nimic.
                            Debug.LogWarning("[VibeCopilot] The split didn't produce separate parts. Keeping the whole model.");
                            Undo.DestroyObjectImmediate(parts);
                            RigidFin(genId, false);
                            return;
                        }
                        // Varianta in piese o inlocuieste pe cea intreaga (altfel raman doua in scena).
                        if (placed != null) Undo.DestroyObjectImmediate(placed);
                        Selection.activeGameObject = parts;
                        Debug.Log("[VibeCopilot] Split into " + n + " physics parts.");
                        RigidFin(genId, true);
                    });
                });
            });
        }

        // Citeste soldul REAL de tokeni 3D si il pune in cache-ul local (EditorPrefs).
        // Nu costa nimic si nu atinge Tripo. `onDone` se cheama si la esec, ca sa poata
        // interfata sa se opreasca din asteptat.
        public static void FetchUsage(Action onDone)
        {
            if (!VibeBackendClient.HasToken) { if (onDone != null) onDone(); return; }
            string json = JsonUtility.ToJson(new UsageReq { op = "usage", token = VibeBackendClient.UserToken });
            Post(json, resp =>
            {
                UsageResp u = null;
                try { u = JsonUtility.FromJson<UsageResp>(resp); } catch { }
                if (u != null && u.gen_tokens_total > 0)
                {
                    VibeBackendClient.UpdateGenTokens(u.gen_tokens_left, u.gen_tokens_total);
                    if (u.models_total > 0) VibeBackendClient.UpdateModels(u.models_left, u.models_total);
                }
                if (onDone != null) onDone();
            });
        }

        // Lista cuvintelor-cheie ale modelelor DEJA generate (numele fisierelor .glb din cache).
        // O trimitem la AI ca sa refoloseasca un model existent in loc sa regenereze un sinonim —
        // asa merge pt ORICE cuvant (auto/vehicle/...), nu doar cele din dictionarul Synonyms.
        public static string CachedSlugs()
        {
            try
            {
                if (!Directory.Exists(GenDir)) return "";
                var names = new System.Collections.Generic.List<string>();
                foreach (string f in Directory.GetFiles(GenDir, "*.glb"))
                    names.Add(Path.GetFileNameWithoutExtension(f));
                return string.Join(", ", names);
            }
            catch { return ""; }
        }

        // ---- pas 1: pornim task-ul (sau primim modelul direct din biblioteca) ----
        // ---- BIBLIOTECA: lista modelelor partajate de pe server (pt fereastra Library) ----
        // onDone(modele, mesaj) — mesajul e nenul cand serverul explica de ce lista e goala
        // (ex. planul AI Copilot nu include biblioteca 3D).
        public static void FetchLibrary(Action<LibModel[], string> onDone)
        {
            string json = JsonUtility.ToJson(new ListReq { op = "list", token = VibeBackendClient.UserToken });
            Post(json, resp =>
            {
                LibResp r = null;
                try { r = JsonUtility.FromJson<LibResp>(resp); } catch { }
                onDone(r != null && r.models != null ? r.models : new LibModel[0],
                       r != null ? r.reply : null);
            });
        }

        // Pune in scena un model ales din fereastra Library: din cache-ul local daca exista,
        // altfel il descarca din biblioteca partajata (gratis — nu consuma generari).
        public static void AddFromLibrary(LibModel model, Vector3 position)
        {
            if (model == null || string.IsNullOrEmpty(model.slug)) return;
            Directory.CreateDirectory(GenDir);
            string assetPath = GenDir + "/" + model.slug + ".glb";
            string niceName = model.slug.Replace('_', ' ');
            if (File.Exists(ToAbs(assetPath)))
            {
                PlaceModel(assetPath, niceName, position);
                return;
            }
            if (string.IsNullOrEmpty(model.url)) { Debug.LogWarning("[VibeCopilot] Library model has no download URL."); return; }
            Debug.Log("[VibeCopilot] Downloading \"" + niceName + "\" from the VibeCopilot library...");
            DownloadGlb(model.url, assetPath, () => PlaceModel(assetPath, niceName, position));
        }

        // ---- pas 2: intrebam statusul la fiecare 5s pana e gata ----
        private static void PollUntilDone(string taskId, string genId, Action<string> onDone)
        {
            double nextTime = EditorApplication.timeSinceStartup;
            int tries = 0;
            bool inFlight = false;
            EditorApplication.CallbackFunction tick = null;
            tick = () =>
            {
                if (inFlight || EditorApplication.timeSinceStartup < nextTime) return;
                nextTime = EditorApplication.timeSinceStartup + 5.0;
                tries++;
                if (tries > 60) { EditorApplication.update -= tick; onDone(null); return; } // ~5 min max

                inFlight = true;
                string json = JsonUtility.ToJson(new StatusReq { op = "status", task_id = taskId });
                Post(json, resp =>
                {
                    inFlight = false;
                    StatusResp s = null;
                    try { s = JsonUtility.FromJson<StatusResp>(resp); } catch { }
                    if (s == null) return; // o eroare punctuala; reincercam la urmatorul tick
                    if (s.status == "success" && !string.IsNullOrEmpty(s.url)) { EditorApplication.update -= tick; onDone(s.url); }
                    else if (s.status == "failed" || s.status == "banned" || s.status == "expired") { EditorApplication.update -= tick; onDone(null); }
                    else
                    {
                        Debug.Log("[VibeCopilot] Generating 3D... " + s.progress + "%");
                        // Acelasi procent care apare in Console ajunge si pe bara din card.
                        // Il tinem sub 90 ca sa ramana loc pt descarcare + import.
                        Prog(genId, Mathf.Min(s.progress, 88));
                    }
                });
            };
            EditorApplication.update += tick;
        }

        // ---- pas 3: descarcam .glb in Assets si il importam ----
        private static void DownloadGlb(string url, string assetPath, Action onDone)
        {
            UnityWebRequest req = UnityWebRequest.Get(url);
            UnityWebRequestAsyncOperation op = req.SendWebRequest();
            EditorApplication.CallbackFunction tick = null;
            tick = () =>
            {
                if (!op.isDone) return;
                EditorApplication.update -= tick;
                if (req.result != UnityWebRequest.Result.Success)
                {
                    Debug.LogWarning("[VibeCopilot] Model download failed: " + req.error);
                    req.Dispose(); return;
                }
                File.WriteAllBytes(ToAbs(assetPath), req.downloadHandler.data);
                req.Dispose();
                AssetDatabase.ImportAsset(assetPath, ImportAssetOptions.ForceSynchronousImport);
                onDone();
            };
            EditorApplication.update += tick;
        }

        // ---- pas 4: instantiem modelul in scena ----
        // Intoarce obiectul pus in scena (sau null la esec) — are nevoie fluxul de rigid body,
        // ca sa poata inlocui meshul intreg cu varianta taiata in piese. Apelantii vechi
        // ignora pur si simplu valoarea intoarsa.
        private static GameObject PlaceModel(string assetPath, string name, Vector3 position)
        {
            GameObject asset = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
            if (asset == null)
            {
                Debug.LogWarning("[VibeCopilot] Can't import the .glb model. Did you install the glTFast package? " +
                                 "(Window -> Package Manager -> + -> Add package by name -> com.unity.cloud.gltfast)");
                return null;
            }
            GameObject go = (GameObject)PrefabUtility.InstantiatePrefab(asset);
            if (go == null) go = UnityEngine.Object.Instantiate(asset);
            if (!string.IsNullOrEmpty(name)) go.name = name;
            NormalizeSize(go); // Tripo le exporta mici -> le aducem la o marime vizibila
            go.transform.position = position;
            Undo.RegisterCreatedObjectUndo(go, "VibeCopilot: generate3d " + go.name);
            Selection.activeGameObject = go;
            EditorGUIUtility.PingObject(go);
            Debug.Log("[VibeCopilot] 3D model added to scene: " + go.name);
            return go;
        }

        // (RIGID BODY) Fiecare piesa primeste corp fizic. Colliderul TREBUIE sa fie convex —
        // un MeshCollider ne-convex nu poate avea Rigidbody dinamic in Unity. Radacina ramane
        // fara Rigidbody, ca piesele sa se miste independent una de alta.
        private static int ApplyPhysics(GameObject root)
        {
            if (root == null) return 0;
            MeshFilter[] parts = root.GetComponentsInChildren<MeshFilter>();
            int n = 0;
            foreach (MeshFilter mf in parts)
            {
                if (mf == null || mf.sharedMesh == null) continue;
                GameObject go = mf.gameObject;
                MeshCollider mc = go.GetComponent<MeshCollider>();
                if (mc == null) mc = Undo.AddComponent<MeshCollider>(go);
                mc.convex = true;
                if (go.GetComponent<Rigidbody>() == null) Undo.AddComponent<Rigidbody>(go);
                n++;
            }
            return n;
        }

        // Tripo exporta modelele la o scara mica/inconstanta. Le scalam ca cea mai mare latura
        // sa fie ~TargetSize unitati -> toate ies la o marime consistenta, vizibila.
        // VREI TOTUL MAI MARE/MIC? Schimba doar numarul de mai jos (3f). Ex: 5f = mult mai mari.
        private const float TargetSize = 3f;

        private static void NormalizeSize(GameObject go)
        {
            Renderer[] renderers = go.GetComponentsInChildren<Renderer>();
            if (renderers == null || renderers.Length == 0) return; // nimic de masurat
            Bounds b = renderers[0].bounds;
            for (int i = 1; i < renderers.Length; i++) b.Encapsulate(renderers[i].bounds);
            float largest = Mathf.Max(b.size.x, Mathf.Max(b.size.y, b.size.z));
            if (largest <= 0.0001f) return; // model gol, evitam impartirea la 0
            go.transform.localScale *= TargetSize / largest;
        }

        // ---- POST JSON la serverul nostru (async) ----
        private static void Post(string json, Action<string> onText) { Post(json, onText, 2); }

        private static void Post(string json, Action<string> onText, int retriesLeft)
        {
            byte[] payload = Encoding.UTF8.GetBytes(json);
            UnityWebRequest req = new UnityWebRequest(GenUrl, "POST");
            req.uploadHandler = new UploadHandlerRaw(payload);
            req.downloadHandler = new DownloadHandlerBuffer();
            req.SetRequestHeader("Content-Type", "application/json");
            UnityWebRequestAsyncOperation op = req.SendWebRequest();
            EditorApplication.CallbackFunction tick = null;
            tick = () =>
            {
                if (!op.isDone) return;
                EditorApplication.update -= tick;
                // Eroare de conexiune (cod 0, keep-alive moarta) -> reincercam cu conexiune noua.
                if (req.result == UnityWebRequest.Result.ConnectionError && retriesLeft > 0)
                {
                    req.Dispose();
                    Post(json, onText, retriesLeft - 1);
                    return;
                }
                string text = (req.result == UnityWebRequest.Result.Success) ? req.downloadHandler.text : null;
                if (text == null)
                    Debug.LogWarning("[VibeCopilot] gen3d server (" + req.responseCode + "): " + req.downloadHandler.text);
                req.Dispose();
                onText(text);
            };
            EditorApplication.update += tick;
        }

        // ---- utilitare ----
        // Cuvinte de comanda/umplutura (RO+EN) pe care le ignoram, ca "make a car" si
        // "fă-mi o mașină" sa devina cheia obiectului ("car" / "ma_in").
        private static readonly string[] StopWords = {
            "make","create","build","spawn","add","give","put","generate","model","of","please","me","my","some","i","want","need","can","you","now","another","second","new",
            "a","an","the",
            "fa","fă","faci","creeaza","creează","creaza","construieste","construiește","pune","adauga","adaugă","genereaza","generează",
            "o","un","niste","niște","mi","imi","îmi","te","rog","vreau","sa","să","poti","poți","acum","alta","altă","inca","încă","mai"
        };

        // Sinonime -> cuvant canonic. Ex: "human"/"guy"/"dude" si "person" -> ACELASI fisier
        // din cache, fara regenerare. Adauga aici perechi noi cand observi alt sinonim
        // (ex. daca vrei si "man"/"woman" -> "person", decomenteaza linia de mai jos).
        private static readonly System.Collections.Generic.Dictionary<string, string> Synonyms =
            new System.Collections.Generic.Dictionary<string, string>
        {
            { "human", "person" }, { "humans", "person" }, { "persons", "person" },
            { "people", "person" }, { "guy", "person" }, { "dude", "person" },
            // { "man", "person" }, { "woman", "person" },  // optional: refoloseste si pt gen
            { "auto", "car" }, { "automobile", "car" },
            { "puppy", "dog" }, { "doggo", "dog" }, { "doggy", "dog" },
        };

        // Inlocuieste fiecare cuvant-sinonim din prompt cu varianta canonica (pe cuvinte).
        private static string Canonical(string prompt)
        {
            if (string.IsNullOrEmpty(prompt)) return prompt;
            string[] words = prompt.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
            for (int i = 0; i < words.Length; i++)
            {
                string lw = words[i].ToLowerInvariant();
                if (Synonyms.TryGetValue(lw, out string canon)) words[i] = canon;
            }
            return string.Join(" ", words);
        }

        private static string Slug(string s)
        {
            s = (s ?? "").ToLowerInvariant().Trim();
            string[] words = Regex.Split(s, @"[^a-z0-9ăâîșțşţ]+");
            var kept = new System.Collections.Generic.List<string>();
            foreach (string w in words)
            {
                if (string.IsNullOrEmpty(w)) continue;
                if (System.Array.IndexOf(StopWords, w) >= 0) continue;
                kept.Add(w);
            }
            string core = kept.Count > 0 ? string.Join("_", kept) : s;
            core = Regex.Replace(core, @"[^a-z0-9]+", "_").Trim('_'); // diacriticele -> _
            if (core.Length > 40) core = core.Substring(0, 40);
            return string.IsNullOrEmpty(core) ? "model" : core;
        }

        private static string ToAbs(string assetPath)
        {
            string projectRoot = Directory.GetParent(Application.dataPath).FullName;
            return Path.Combine(projectRoot, assetPath).Replace("\\", "/");
        }

        [Serializable] private class StartReq  { public string op; public string prompt; public string token; public string slug; public int lib; public int custom; public string preview_url; public string preview_ext; }
        [Serializable] private class StartResp { public string task_id; public bool blocked; public int models_left; public int models_total; public string reply; public string url; public bool library; public string image_url; public int gen_tokens_left; public int gen_tokens_total; }
        [Serializable] private class PreviewReq  { public string op; public string prompt; public string token; public string slug; public int lib; public int custom; }
        [Serializable] public  class ImgData     { public string b64; public string mime; }
        [Serializable] private class PreviewImgReq { public string op; public string prompt; public string token; public string slug; public int lib; public int custom; public ImgData image; }
        [Serializable] private class PreviewResp { public bool preview; public bool blocked; public string image_url; public string image_ext; public string image_black_url; public bool pre_cut; public string slug; public string url; public bool library; public int models_left; public int models_total; public string reply; public int gen_tokens_left; public int gen_tokens_total; }
        [Serializable] private class UsageReq  { public string op; public string token; }
        [Serializable] private class UsageResp { public int gen_tokens_left; public int gen_tokens_total; public int models_left; public int models_total; }
        [Serializable] private class SegReq  { public string op; public string task_id; public string token; }
        [Serializable] private class SegResp { public string task_id; public bool blocked; public string reply; public int gen_tokens_left; public int gen_tokens_total; public int models_left; public int models_total; }
        [Serializable] private class StatusReq { public string op; public string task_id; }
        [Serializable] private class StatusResp { public string status; public int progress; public string url; }
        [Serializable] private class ListReq   { public string op; public string token; }
        [Serializable] public  class LibModel  { public string slug; public string url; }
        [Serializable] private class LibResp   { public LibModel[] models; public string reply; }
    }
}
