/* importer.js — bridges the panel to the Premiere Pro host via ExtendScript.
 * Calls into jsx/host.jsx exposed functions.
 */
const Importer = (function () {
    const cs = new CSInterface();

    function esc(str) {
        return String(str).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
    }

    /* Sanitiza o XML de um .prproj descomprimido: troca TODOS os paths externos
     * (que não existem nesta máquina — eram placeholders do PC do dev do AtomX)
     * por arquivos placeholder dentro do plugin. Sem isso, Premiere abre popup
     * "Vincular mídia" e o render trava por clip offline.
     *
     * Tags patcheadas (todas que carregam path absoluto de mídia):
     *   <ActualMediaFilePath>X:\...</ActualMediaFilePath>
     *   <FilePath>X:\...</FilePath>
     *   <ConformedAudioPath>X:\...</ConformedAudioPath>
     *   <PeakFilePath>X:\...</PeakFilePath>
     *
     * Heurística por extensão pra escolher placeholder:
     *   .wav .mp3 .aif .aiff .m4a → empty.wav
     *   resto                     → empty.png
     */
    /* Detecta path absoluto Windows (drive letter + : + \ ou /). Usa códigos de
     * caractere em vez de regex pra evitar bug clássico de [\\/] em literal JS. */
    function isAbsoluteWinPath(s) {
        if (!s || s.length < 3) return false;
        var c0 = s.charCodeAt(0);
        var isLetter = (c0 >= 65 && c0 <= 90) || (c0 >= 97 && c0 <= 122);
        if (!isLetter) return false;
        if (s.charCodeAt(1) !== 58) return false; // :
        var c2 = s.charCodeAt(2);
        return c2 === 92 /* \ */ || c2 === 47 /* / */;
    }

    function sanitizePrprojXml(xmlString) {
        const path = require("path");
        const fs = require("fs");
        // cs.getSystemPath retorna file:///C:/... — precisa normalizar pra path nativo
        // antes de path.join, senão vira "file:/C:/.../assets/empty.wav" (inválido).
        let extPath = cs.getSystemPath(CSInterface.SystemPath.EXTENSION);
        extPath = decodeURI(extPath).replace(/^file:[\\\/]+/i, "").replace(/\//g, "\\");
        const wavPlaceholder = path.join(extPath, "assets", "empty.wav");
        const pngPlaceholder = path.join(extPath, "assets", "empty.png");

        // Tags com path absoluto (drive letter X:\). IDs numéricos como "1112293707"
        // ficam intactos porque charCode test rejeita.
        const tags = ["ActualMediaFilePath", "FilePath", "ConformedAudioPath", "PeakFilePath"];
        const audioExt = /\.(wav|mp3|aif|aiff|m4a|aac|flac|ogg)$/i;
        let patched = 0;

        for (const tag of tags) {
            const re = new RegExp("<" + tag + ">([^<]*)</" + tag + ">", "g");
            xmlString = xmlString.replace(re, function (full, inner) {
                if (!isAbsoluteWinPath(inner)) return full;
                // Depois do preparo AtomX-style, paths locais reais em _Assets devem
                // ficar intactos. Só trocamos referências absolutas que não existem.
                try { if (fs.existsSync(inner)) return full; } catch (e) {}
                const placeholder = audioExt.test(inner) ? wavPlaceholder : pngPlaceholder;
                patched++;
                return "<" + tag + ">" + placeholder + "</" + tag + ">";
            });
        }

        // PathDirectory tags também aparecem (raras). Aplica mesma lógica.
        xmlString = xmlString.replace(/<PathDirectory>([^<]*)<\/PathDirectory>/g, function (full, inner) {
            if (!isAbsoluteWinPath(inner)) return full;
            try { if (fs.existsSync(inner)) return full; } catch (e) {}
            patched++;
            return "<PathDirectory>" + path.join(extPath, "assets") + "</PathDirectory>";
        });

        return { xml: xmlString, patchCount: patched, wavPlaceholder: wavPlaceholder, pngPlaceholder: pngPlaceholder };
    }

    function call(fnExpr) {
        return new Promise((resolve, reject) => {
            cs.evalScript(fnExpr, function (res) {
                if (!res || res === "undefined") return resolve(null);
                if (res === "EvalScript error.") return reject(new Error("ExtendScript error"));
                try {
                    const parsed = JSON.parse(res);
                    if (parsed && parsed.error) return reject(new Error(parsed.error));
                    return resolve(parsed);
                } catch (e) { return resolve(res); }
            });
        });
    }

    async function ensureHostLoaded() {
        // host.jsx is auto-loaded via ScriptPath in manifest, but warm it up
        try { return await call("$.global.MotionVault && MotionVault.ping()"); }
        catch (e) { return null; }
    }

    async function importMogrt(absPath) {
        if (!absPath) throw new Error("Caminho do .mogrt vazio");
        const expr = 'MotionVault.importMogrt("' + esc(absPath) + '")';
        return await call(expr);
    }

    async function importAudio(absPath) {
        if (!absPath) throw new Error("Caminho do áudio vazio");
        const expr = 'MotionVault.importAudio("' + esc(absPath) + '")';
        return await call(expr);
    }

    async function importPresetProject(absPath) {
        if (!absPath) throw new Error("Caminho do preset vazio");
        const expr = 'MotionVault.importPresetProject("' + esc(absPath) + '")';
        return await call(expr);
    }

    /* Importa um .atomxasset (base64+gzip de um .prproj) e aplica na timeline ativa.
     * Descomprime no JS-side (Node.js disponível em CEP), grava num .prproj temporário,
     * e delega pra MotionVault.applyAtomxAsset(path, subType, niceName) no host.
     *
     * subType controla o track destino:
     *   "transition"     → V2 atravessando o ponto de corte
     *   "color_grading"  → V2 cobrindo o clip do V1 (look)
     *   "effect"         → V3 (overlay)
     */
    /* Log persistente — escreve em %TEMP%/motionvault-debug.log pra inspeção depois */
    function dbgLog(line) {
        try {
            const fs = require("fs"), pp = require("path"), os = require("os");
            const logPath = pp.join(os.tmpdir(), "motionvault-debug.log");
            const ts = new Date().toISOString();
            fs.appendFileSync(logPath, "[" + ts + "] " + line + "\n");
        } catch (e) { /* swallow */ }
    }

    function generateGuid() {
        return "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx".replace(/x/g, function () {
            return Math.floor(Math.random() * 16).toString(16);
        });
    }

    function escapeXmlText(s) {
        return String(s == null ? "" : s)
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");
    }

    function getText(el) {
        return el ? (el.textContent || "") : "";
    }

    function setText(el, value) {
        if (el) el.textContent = String(value == null ? "" : value);
    }

    function firstChildByTag(parent, tagName) {
        if (!parent) return null;
        const nodes = parent.getElementsByTagName(tagName);
        return nodes && nodes.length ? nodes[0] : null;
    }

    function appendSimpleNode(doc, parent, tagName, value) {
        const el = doc.createElement(tagName);
        el.appendChild(doc.createTextNode(String(value == null ? "" : value)));
        parent.appendChild(el);
        return el;
    }

    function deriveAtomxAssetsDir(assetPath) {
        const path = require("path");
        const marker = "\\Atom Premiere Pro\\";
        const p = String(assetPath || "").replace(/\//g, "\\");
        const idx = p.toLowerCase().indexOf(marker.toLowerCase());
        if (idx < 0) return "";
        return path.join(p.slice(0, idx + marker.length), "_Assets");
    }

    function basenameFromAnyPath(p) {
        const m = String(p || "").match(/([^\\\/]+)$/);
        return m ? m[1] : "";
    }

    function resolveAtomxMediaPath(originalPath, assetsDir, fallbackWav, fallbackPng) {
        const path = require("path");
        const fs = require("fs");
        const name = basenameFromAnyPath(originalPath);
        const isAudio = /\.(wav|mp3|aif|aiff|m4a|aac|flac|ogg)$/i.test(name || originalPath);
        if (assetsDir && name) {
            const candidate = path.join(assetsDir, name);
            try { if (fs.existsSync(candidate)) return candidate; } catch (e) {}
        }
        return isAudio ? fallbackWav : fallbackPng;
    }

    function scaleTimingNodes(doc, factor) {
        if (!factor || factor === 1) return 0;
        const tags = ["Start", "End", "InPoint", "OutPoint"];
        let patched = 0;
        tags.forEach(function (tag) {
            const nodes = doc.getElementsByTagName(tag);
            for (let i = 0; i < nodes.length; i++) {
                const n = Number(getText(nodes[i]));
                if (!isNaN(n)) {
                    setText(nodes[i], n * factor);
                    patched++;
                }
            }
        });
        const keyframes = doc.getElementsByTagName("Keyframes");
        for (let k = 0; k < keyframes.length; k++) {
            const raw = getText(keyframes[k]);
            if (!raw) continue;
            const parts = raw.indexOf(";") >= 0 ? raw.split(";") : [raw];
            const out = [];
            for (let p = 0; p < parts.length; p++) {
                if (!parts[p]) continue;
                const vals = parts[p].split(",");
                if (vals.length < 2) continue;
                const t = Number(vals[0]);
                if (!isNaN(t)) vals[0] = String(t * factor);
                out.push(vals.join(","));
            }
            if (out.length) {
                setText(keyframes[k], raw.indexOf(";") >= 0 ? out.join(";") + ";" : out.join(";"));
                patched++;
            }
        }
        return patched;
    }

    function prepareAtomxPasteXml(xmlString, sourceAssetPath, opts) {
        opts = opts || {};
        const path = require("path");

        let extPath = cs.getSystemPath(CSInterface.SystemPath.EXTENSION);
        extPath = decodeURI(extPath).replace(/^file:[\\\/]+/i, "").replace(/\//g, "\\");
        const fallbackWav = path.join(extPath, "assets", "empty.wav");
        const fallbackPng = path.join(extPath, "assets", "empty.png");
        const assetsDir = deriveAtomxAssetsDir(sourceAssetPath);

        const parser = new DOMParser();
        const doc = parser.parseFromString(xmlString, "text/xml");
        if (doc.getElementsByTagName("parsererror").length) {
            throw new Error("XML do .atomxasset inválido após descompressão");
        }

        const sequenceNode = doc.getElementsByTagName("Sequence")[0];
        const originalSequenceGuid = sequenceNode && sequenceNode.getAttribute("ObjectURef");
        if (!originalSequenceGuid) throw new Error("Sequence ObjectURef não encontrado no .atomxasset");

        const pasteSequenceGuid = generateGuid();
        const mediaGuid = generateGuid();
        const nameSuffix = "ATOMX_" + Math.round(Math.random() * 990);
        let mediaPathsPatched = 0;
        let clipIdsPatched = 0;
        let namesPatched = 0;

        const props = doc.getElementsByTagName("Properties")[0];
        if (props) {
            while (props.firstChild) props.removeChild(props.firstChild);
            appendSimpleNode(doc, props, "CopyPasteSequenceGUID", pasteSequenceGuid);
            appendSimpleNode(doc, props, "CopyPasteSequenceID", "1");
        }

        const mediaNodes = doc.getElementsByTagName("Media");
        for (let i = 0; i < mediaNodes.length; i++) {
            const media = mediaNodes[i];
            if (!media.hasAttribute("ObjectUID")) continue;
            const filePathNode = firstChildByTag(media, "FilePath");
            const actualNode = firstChildByTag(media, "ActualMediaFilePath");
            const sourcePath = getText(filePathNode) || getText(actualNode);
            if (!sourcePath) continue;
            const localPath = resolveAtomxMediaPath(sourcePath, assetsDir, fallbackWav, fallbackPng);
            setText(filePathNode, localPath);
            setText(actualNode, localPath);
            mediaPathsPatched++;
        }

        if (opts.frameRect && opts.frameRect.length) {
            const frameRects = doc.getElementsByTagName("FrameRect");
            for (let f = 0; f < frameRects.length; f++) setText(frameRects[f], opts.frameRect.join(","));
        }

        const videoMediaSource = doc.getElementsByTagName("VideoMediaSource")[0];
        const videoMediaRef = firstChildByTag(videoMediaSource, "Media");
        if (videoMediaRef) {
            const oldMediaRef = videoMediaRef.getAttribute("ObjectURef");
            if (oldMediaRef) {
                videoMediaRef.setAttribute("ObjectURef", mediaGuid);
                for (let m = 0; m < mediaNodes.length; m++) {
                    if (mediaNodes[m].getAttribute("ObjectUID") === oldMediaRef) {
                        mediaNodes[m].setAttribute("ObjectUID", mediaGuid);
                        break;
                    }
                }
            }
        }

        const videoClips = doc.getElementsByTagName("VideoClip");
        for (let v = 0; v < videoClips.length; v++) {
            const clipIds = videoClips[v].getElementsByTagName("ClipID");
            for (let c = 0; c < clipIds.length; c++) {
                setText(clipIds[c], generateGuid());
                clipIdsPatched++;
            }
        }

        function patchAdjustmentNames(tagName) {
            const nodes = doc.getElementsByTagName(tagName);
            for (let i = 0; i < nodes.length; i++) {
                const txt = getText(nodes[i]);
                const low = txt.toLowerCase();
                if (low === "adjustment" || low === "adjustment layer" || low === "do not touch" || low === "do not edit" || low === "adj layer") {
                    setText(nodes[i], txt + " " + nameSuffix);
                    namesPatched++;
                }
            }
        }
        patchAdjustmentNames("Name");
        patchAdjustmentNames("ClipName");

        if (opts.sequenceSpeed && Number(opts.sequenceSpeed) !== 100) {
            scaleTimingNodes(doc, 100 / Number(opts.sequenceSpeed));
        }

        const xmlOut = new XMLSerializer().serializeToString(doc)
            .replace(new RegExp(originalSequenceGuid, "g"), pasteSequenceGuid);

        return {
            xml: xmlOut,
            sequenceGuid: pasteSequenceGuid,
            assetsDir: assetsDir,
            mediaPathsPatched: mediaPathsPatched,
            clipIdsPatched: clipIdsPatched,
            namesPatched: namesPatched
        };
    }

    /* AtomX-style application: usa pprsequencer.exe (engine nativo do AtomX).
     *
     * Fluxo confirmado por reverse engineering:
     *  - argv[1] = filepath do arquivo a aplicar (.atomxasset ou .prproj)
     *  - argv[2] = GUID/cookie gerado por aplicação
     *  - O exe lê o arquivo, coloca no clipboard com formato custom Premiere,
     *    encontra a janela do Premiere via FindWindowA, foca via SetForegroundWindow,
     *    e dispara Ctrl+V via SendInput.
     */
    function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }

    async function applyViaPprSequencer(filePath, cookieGuid) {
        const path = require("path");
        const fs = require("fs");

        // Resolve EXT_PATH normalizado (file:/// → C:\)
        let extPath = cs.getSystemPath(CSInterface.SystemPath.EXTENSION);
        extPath = decodeURI(extPath).replace(/^file:[\\\/]+/i, "").replace(/\//g, "\\");
        const binPath = path.join(extPath, "bin", "pprsequencer.exe");

        if (!fs.existsSync(binPath))   throw new Error("pprsequencer.exe não encontrado: " + binPath);
        if (!fs.existsSync(filePath))  throw new Error("Arquivo não encontrado: " + filePath);

        const cookie = cookieGuid || generateGuid();

        // Foca a Timeline do Premiere antes de rodar o pprsequencer.
        // O exe faz Ctrl+V via SendInput — só cola na timeline se O FOCO DE KEYBOARD
        // estiver no panel Timeline (não no panel CEP do MotionVault que acabou
        // de receber o click). Sequência:
        //   1) AppActivate na janela main do Premiere (traz ela pra frente)
        //   2) AppActivate na palavra "Timeline" (foca o panel Timeline dentro
        //      da janela main do Premiere)
        //   3) sleep pra DOM/foco assentar antes do SendInput do exe
        try {
            var psCmd =
                'powershell -NoProfile -WindowStyle Hidden -Command "' +
                '$wsh = New-Object -ComObject WScript.Shell;' +
                '$wsh.AppActivate(\'Adobe Premiere\') | Out-Null;' +
                'Start-Sleep -Milliseconds 120;' +
                '$wsh.AppActivate(\'Timeline\') | Out-Null;' +
                '" > nul 2>&1';
            window.cep.process.createProcess("cmd.exe", "/c", psCmd);
            dbgLog("Foco Premiere + Timeline enviado");
        } catch (e) { dbgLog("focus non-critical: " + (e.message || e)); }
        await sleep(600);

        if (window.cep && window.cep.process && window.cep.process.createProcess) {
            dbgLog("→ cep.process.createProcess (AtomX-style GUID cookie)");
            const result = window.cep.process.createProcess(
                binPath, filePath, cookie
            );
            dbgLog("← createProcess result: " + JSON.stringify(result));

            if (result && (result.err === 0 || result.err === "0")) {
                const pid = result.data;
                dbgLog("✓ pprsequencer lançado, pid=" + pid + " cookie=" + cookie);
                return await new Promise(function (resolve) {
                    let done = false;
                    function finish(reason) {
                        if (done) return;
                        done = true;
                        sleep(1600).then(function () {
                            resolve({
                                ok: true,
                                applied: true,
                                strategy: "cep.process + atomx-guid",
                                pid: pid,
                                cookie: cookie,
                                quit: reason
                            });
                        });
                    }
                    try {
                        if (window.cep.process.onquit) {
                            window.cep.process.onquit(pid, function () {
                                dbgLog("← pprsequencer onquit pid=" + pid);
                                finish("onquit");
                            });
                            setTimeout(function () { finish("timeout"); }, 4500);
                        } else {
                            setTimeout(function () { finish("no-onquit"); }, 2200);
                        }
                    } catch (e) {
                        dbgLog("onquit attach failed: " + (e.message || e));
                        setTimeout(function () { finish("onquit-error"); }, 2200);
                    }
                });
            }
            dbgLog("✗ createProcess falhou: err=" + (result && result.err) + " — caindo pro spawn fallback");
        }

        // ─── FALLBACK: child_process.spawn (menos confiável pra foreground) ───
        const { spawn } = require("child_process");
        const args = [filePath, cookie];
        dbgLog("→ spawn fallback: " + binPath + " " + args.join(" "));

        return new Promise((resolve, reject) => {
            let stdout = "", stderr = "";
            let proc;
            try {
                proc = spawn(binPath, args, { windowsHide: true });
            } catch (e) {
                dbgLog("✗ spawn throw: " + e.message);
                return reject(new Error("Falha ao spawn pprsequencer: " + e.message));
            }
            proc.stdout && proc.stdout.on("data", d => stdout += d.toString());
            proc.stderr && proc.stderr.on("data", d => stderr += d.toString());
            proc.on("error", e => reject(new Error("pprsequencer error: " + e.message)));
            proc.on("close", code => {
                dbgLog("← spawn fallback exit=" + code);
                setTimeout(function () {
                    resolve({ ok: true, applied: true, strategy: "spawn-fallback", exitCode: code, cookie: cookie, stdout: stdout.trim(), stderr: stderr.trim() });
                }, 1600);
            });
        });
    }

    async function importAtomxAsset(absPath, subType, niceName, category) {
        if (!absPath) throw new Error("Caminho do .atomxasset vazio");
        const fs = require("fs");
        const path = require("path");
        const os = require("os");
        const zlib = require("zlib");

        dbgLog("================ NEW CALL ================");
        dbgLog("absPath=" + absPath);
        dbgLog("subType=" + subType + " niceName=" + niceName + " category=" + category);

        const sub_top = String(subType || "color_grading");

        if (!fs.existsSync(absPath)) {
            dbgLog("✗ asset não encontrado no disco");
            throw new Error("Arquivo não encontrado: " + absPath);
        }
        const sizeIn = fs.statSync(absPath).size;
        dbgLog("size_in=" + sizeIn);

        const raw = fs.readFileSync(absPath, "utf8").trim();
        let buf;
        try {
            const b64 = raw.replace(/[^A-Za-z0-9+/=]/g, "");
            buf = zlib.gunzipSync(Buffer.from(b64, "base64"));
        } catch (e) {
            dbgLog("✗ gunzip ERR: " + (e.message || e));
            throw new Error("Falha ao descomprimir .atomxasset: " + (e.message || e));
        }
        dbgLog("decoded_bytes=" + buf.length + " (xml head: " + buf.slice(0, 40).toString("utf8").replace(/\s+/g, " ") + ")");

        let xmlText = buf.toString("utf8");
        let pasteCookie = generateGuid();

        // ─── PREPARO ATOMX-STYLE ───
        // O AtomX não entrega o XML bruto pro pprsequencer. Antes ele injeta
        // CopyPasteSequenceGUID/ID, recria GUIDs internos e remapeia mídias para
        // _Assets. Sem isso o Premiere recebe o clipboard, mas ignora o paste.
        let prepared = null;
        if (sub_top === "transition" || sub_top === "effect") {
            prepared = prepareAtomxPasteXml(xmlText, absPath, { sequenceSpeed: 100 });
            xmlText = prepared.xml;
            pasteCookie = generateGuid();
            dbgLog("prepared AtomX XML: seqGuid=" + prepared.sequenceGuid +
                " assetsDir=" + prepared.assetsDir +
                " media=" + prepared.mediaPathsPatched +
                " clipIds=" + prepared.clipIdsPatched +
                " names=" + prepared.namesPatched +
                " cookie=" + pasteCookie);
        }

        // ─── SANITIZAR paths externos antes de gravar ───
        // Sem isso, Premiere abre popup "Vincular mídia" e trava no render porque
        // as mídias placeholder do AtomX dev (E:\... etc) não existem aqui.
        const sanitized = sanitizePrprojXml(xmlText);
        dbgLog("sanitized: patched=" + sanitized.patchCount + " refs externas → placeholders locais");
        dbgLog("  wav→ " + sanitized.wavPlaceholder);
        dbgLog("  png→ " + sanitized.pngPlaceholder);
        const sanitizedBuf = Buffer.from(sanitized.xml, "utf8");

        const tmpDir = path.join(os.tmpdir(), "motionvault-atomx");
        try { fs.mkdirSync(tmpDir, { recursive: true }); } catch (e) {}
        const base = path.basename(absPath).replace(/\.atomxasset$/i, "");
        const safe = base.replace(/[^A-Za-z0-9._-]+/g, "_");
        const tmpFile = path.join(tmpDir, safe + "_" + Date.now() + ".prproj");
        fs.writeFileSync(tmpFile, sanitizedBuf);
        dbgLog("tmpFile=" + tmpFile + " (" + sanitizedBuf.length + " bytes after sanitize)");

        const sub = String(subType || "color_grading");
        const nm  = String(niceName || base);
        const cat = String(category || "");

        // ─── pprsequencer.exe foi DESATIVADO pra transition/effect (2026-05-30).
        //     Motivo: RE confirmou que o exe NÃO tem filtro de license/Id, mas o
        //     paste só cola quando o ExtendScript do AtomX (JSXBIN compilado opaco)
        //     prepara estado interno do projeto antes — algo que não conseguimos
        //     replicar sem decompilar JSXBIN. Resultado: exe rodava, retornava
        //     SUCCESS, mas paste era ignorado pela timeline.
        //
        //     Estratégia atual: TUDO (color_grading, transition, effect) passa
        //     pelo host.jsx applyAtomxAsset → import + overwriteClip.
        //       - color_grading → V2 (sempre funciona, Lumetri built-in)
        //       - transition/effect → V3 (pode falhar pra assets com effect AE
        //         custom; nesse caso devolvemos "imported_only" e user arrasta da bin)

        // ─── via host.jsx ───
        const expr = 'MotionVault.applyAtomxAsset("' + esc(tmpFile) + '","' + esc(sub) + '","' + esc(nm) + '","' + esc(cat) + '")';
        dbgLog("→ evalScript: " + expr.slice(0, 200));

        const t0 = Date.now();
        let res;
        try {
            res = await call(expr);
            dbgLog("← returned in " + (Date.now() - t0) + "ms");
            dbgLog("← payload: " + JSON.stringify(res));
        } catch (e) {
            dbgLog("✗ host throw after " + (Date.now() - t0) + "ms: " + (e.message || e));
            throw e;
        }
        return res;
    }

    async function getActiveSequenceInfo() {
        return await call("MotionVault.getActiveSequenceInfo()");
    }

    return { ensureHostLoaded, importMogrt, importAudio, importPresetProject, importAtomxAsset, getActiveSequenceInfo };
})();
window.Importer = Importer; // expõe pro status-bar (botão "Testar transição")
