/* global marked, DOMPurify */
(function (window, document) {
    'use strict';

    var renderers = [];
    var state = { content: '', agent: 0, returnFocus: null, editable: false, copyPrompts: [], previewToken: 0 };
    var focusableSelector = 'button:not([disabled]), [href], textarea:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])';

    function element(id) { return document.getElementById(id); }

    function safeMarkdown(markdown) {
        var source = String(markdown || '');
        if (!window.marked || !window.DOMPurify) {
            var fallback = document.createElement('pre');
            fallback.textContent = source;
            return { html: '', fallback: fallback };
        }
        return {
            html: window.DOMPurify.sanitize(window.marked.parse(source), {
                USE_PROFILES: { html: true },
                FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'svg', 'math', 'form'],
                FORBID_ATTR: ['style', 'srcdoc']
            }),
            fallback: null
        };
    }

    function safeUrl(value) {
        try {
            var raw = String(value || '').trim();
            if (!/^https?:\/\//i.test(raw)) return '';
            var url = new URL(raw);
            if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.username || url.password) return '';
            return url.href;
        } catch (e) { return ''; }
    }

    function unicodeWords(value) {
        var trimmed = String(value || '').trim();
        return trimmed ? trimmed.split(/\s+/u).length : 0;
    }

    function defaultViewModel(context) {
        return {
            kind: 'markdown',
            title: context.agentName || ('Agent ' + String(context.agent).padStart(2, '0')),
            badges: [],
            sections: [],
            rawMarkdown: context.content
        };
    }

    function textNode(tag, className, value) {
        var node = document.createElement(tag);
        if (className) node.className = className;
        node.textContent = String(value == null ? '' : value);
        return node;
    }

    function normalizedAsset(item, position) {
        item = item && typeof item === 'object' ? item : {};
        var signature = item.visual_signature && typeof item.visual_signature === 'object' ? item.visual_signature : {};
        return {
            filename: String(item.filename || ('image-' + position + '.webp')).trim(),
            role: String(item.role || (position === 1 ? 'cover' : 'inline')).trim(),
            // editorial_role (Ciclo 3, Fatia 6) is the FUNCTION taxonomy —
            // cover/explanatory/conceptual/evidence-data/product-ui — distinct
            // from `role`, which is just the cover/inline slot.
            editorialRole: String(item.editorial_role || '').trim(),
            marker: String(item.marker || '').trim(),
            position: String(item.position || item.section || item.section_heading || '').trim(),
            // Real backend field names (Costudio_Visual_Plan::validate_and_normalize()):
            // context_anchor = why/where this asset sits locally, editorial_purpose = its goal.
            // Legacy/manual JSON inventories won't have these — the older `context` key
            // is kept as a fallback so those still render something.
            contextAnchor: String(item.context_anchor || item.context || '').trim(),
            editorialPurpose: String(item.editorial_purpose || item.rationale || '').trim(),
            prompt: String(item.prompt || '').trim(),
            alt: String(item.alt_text || item.alt || item.altText || '').trim(),
            caption: String(item.caption || item.legenda || item.leyenda || '').trim(),
            signature: signature,
            warnings: Array.isArray(item.warnings) ? item.warnings.map(String) : []
        };
    }

    function unwrapInventory(decoded) {
        if (Array.isArray(decoded)) return decoded;
        if (!decoded || typeof decoded !== 'object') return null;
        var keys = ['assets', 'images', 'image_prompts', 'visual_prompts', 'prompts', 'items', 'data'];
        for (var i = 0; i < keys.length; i++) {
            if (Array.isArray(decoded[keys[i]])) return decoded[keys[i]];
        }
        return null;
    }

    function parseVisualPlan(content) {
        var match = String(content || '').match(/<VISUAL_PLAN>\s*([\s\S]*?)\s*<\/VISUAL_PLAN>/i);
        if (!match) return null;
        try {
            var decoded = JSON.parse(match[1]);
            var assets = unwrapInventory(decoded);
            if (!assets) return null;
            return {
                contractVersion: String(decoded.contract_version || ''),
                vocabularyVersion: String(decoded.vocabulary_version || ''),
                assets: assets.map(function (item, index) { return normalizedAsset(item, index + 1); })
            };
        } catch (e) { return null; }
    }

    function parseJsonInventories(content) {
        var source = String(content || '');
        var candidates = [];
        var fenced = /```(?:json)?\s*([\s\S]*?)\s*```/gi;
        var block;
        while ((block = fenced.exec(source)) !== null) candidates.push(block[1]);
        candidates.push(source);
        for (var i = 0; i < candidates.length; i++) {
            try {
                var decoded = JSON.parse(candidates[i].trim());
                var inventory = unwrapInventory(decoded);
                if (inventory) return inventory.map(function (item, index) { return normalizedAsset(item, index + 1); });
            } catch (e) { /* Continue with the next safe candidate. */ }
        }
        var arrays = source.match(/\[\s*\{[\s\S]*?"filename"[\s\S]*?\}\s*\]/g) || [];
        for (var j = 0; j < arrays.length; j++) {
            try {
                var list = JSON.parse(arrays[j]);
                if (Array.isArray(list)) return list.map(function (item, index) { return normalizedAsset(item, index + 1); });
            } catch (e2) { /* Continue with Markdown fallback. */ }
        }
        return null;
    }

    function labeledValue(body, labels) {
        for (var i = 0; i < labels.length; i++) {
            var escaped = labels[i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            var re = new RegExp('(?:\\*\\*)?' + escaped + '(?:\\*\\*)?\\s*:\\s*(?:\\*\\*)?\\s*([\\s\\S]*?)(?=\\n\\s*(?:\\*\\*)?(?:Prompt|Alt Text|Alt|Texto alternativo|Texto Alt|Caption|Legenda|Leyenda)(?:\\*\\*)?\\s*:|\\n\\s*---|$)', 'i');
            var match = body.match(re);
            if (match) return match[1].replace(/\s+/g, ' ').trim();
        }
        return '';
    }

    function parseLegacyVisualAssets(content) {
        var json = parseJsonInventories(content);
        if (json && json.length) return json;
        var source = String(content || '');
        var re = /^#{3,6}\s+([a-z0-9._-]+\.webp)\b[^\n]*\n([\s\S]*?)(?=^#{3,6}\s+[a-z0-9._-]+\.webp\b|^---\s*$|^###\s+(?:PART|PARTE)\s+2\b|$)/gim;
        var assets = [];
        var match;
        while ((match = re.exec(source)) !== null) {
            var prompt = labeledValue(match[2], ['Prompt']);
            if (!prompt) continue;
            assets.push(normalizedAsset({
                filename: match[1],
                role: assets.length ? 'inline' : 'cover',
                prompt: prompt,
                alt_text: labeledValue(match[2], ['Alt Text', 'Alt', 'Texto alternativo', 'Texto Alt']),
                caption: labeledValue(match[2], ['Caption', 'Legenda', 'Leyenda'])
            }, assets.length + 1));
        }
        return assets.length ? assets : null;
    }

    function visualAssetViewModel(context) {
        var plan = parseVisualPlan(context.content);
        var assets = plan ? plan.assets : parseLegacyVisualAssets(context.content);
        if (!assets || !assets.length) return defaultViewModel(context);
        return {
            kind: 'visual-assets',
            title: context.agentName || 'Visual Designer output',
            badges: [plan ? 'VISUAL_PLAN' : 'Legacy inventory', assets.length + (assets.length === 1 ? ' asset' : ' assets')],
            sections: [{ type: 'visual-assets', assets: assets }],
            rawMarkdown: context.content
        };
    }

    function chooseRenderer(context) {
        for (var i = 0; i < renderers.length; i++) {
            try {
                if (renderers[i].supports(context)) return renderers[i];
            } catch (e) { /* A broken specialized renderer must never break output access. */ }
        }
        return { render: defaultViewModel };
    }

    function appendChip(container, text) {
        var chip = document.createElement('span');
        chip.className = 'fos-output-inspector__chip';
        chip.textContent = text;
        container.appendChild(chip);
    }

    function appendLabeledValue(container, label, value) {
        if (!String(value || '').trim()) return;
        var row = document.createElement('div');
        row.className = 'fos-output-inspector__asset-field';
        row.appendChild(textNode('strong', '', label));
        row.appendChild(textNode('span', '', value));
        container.appendChild(row);
    }

    function renderVisualAssets(container, assets) {
        var grid = document.createElement('div');
        grid.className = 'fos-output-inspector__asset-grid';
        state.copyPrompts = [];
        assets.forEach(function (asset, index) {
            var card = document.createElement('article');
            card.className = 'fos-output-inspector__asset-card';
            var header = document.createElement('header');
            header.className = 'fos-output-inspector__asset-header';
            var identity = document.createElement('div');
            identity.className = 'fos-output-inspector__asset-identity';
            identity.appendChild(textNode('span', 'fos-output-inspector__asset-role', asset.role || (index ? 'inline' : 'cover')));
            if (asset.editorialRole) identity.appendChild(textNode('span', 'fos-output-inspector__asset-role fos-output-inspector__asset-role--function', asset.editorialRole));
            identity.appendChild(textNode('h3', '', asset.filename));
            header.appendChild(identity);
            var copy = textNode('button', 'fos-btn fos-btn--ghost fos-output-inspector__copy-prompt', 'Copy prompt');
            copy.type = 'button';
            copy.setAttribute('data-copy-visual-prompt', String(index));
            state.copyPrompts[index] = asset.prompt;
            header.appendChild(copy);
            card.appendChild(header);
            var body = document.createElement('div');
            body.className = 'fos-output-inspector__asset-body';
            appendLabeledValue(body, 'Marker', asset.marker);
            appendLabeledValue(body, 'Position', asset.position);
            appendLabeledValue(body, 'Context', asset.contextAnchor);
            appendLabeledValue(body, 'Editorial purpose', asset.editorialPurpose);
            var promptBox = document.createElement('div');
            promptBox.className = 'fos-output-inspector__prompt-box';
            promptBox.appendChild(textNode('strong', '', 'Generation prompt'));
            promptBox.appendChild(textNode('p', '', asset.prompt));
            body.appendChild(promptBox);
            appendLabeledValue(body, 'Alt text', asset.alt);
            appendLabeledValue(body, 'Caption', asset.caption);
            var signature = asset.signature || {};
            var values = ['subject_family', 'scene_family', 'composition', 'lighting', 'palette'].map(function (key) {
                return signature[key] ? key.replace(/_/g, ' ') + ': ' + signature[key] : '';
            }).filter(Boolean);
            if (values.length) appendLabeledValue(body, 'Visual signature', values.join(' · '));
            if (asset.warnings && asset.warnings.length) appendLabeledValue(body, 'Warnings', asset.warnings.join(' · '));
            card.appendChild(body);
            grid.appendChild(card);
        });
        container.appendChild(grid);
    }

    function renderSections(container, sections) {
        (sections || []).forEach(function (section) {
            if (section && section.type === 'visual-assets') renderVisualAssets(container, section.assets || []);
        });
    }

    function renderViewModel(vm, context) {
        var meta = element('fos-output-inspector-meta');
        var preview = element('fos-output-body-html');
        var structured = element('fos-output-inspector-structured');
        var raw = element('fos-output-raw');
        meta.replaceChildren();
        structured.replaceChildren();
        preview.replaceChildren();
        raw.textContent = context.content;

        appendChip(meta, 'Agent ' + String(context.agent).padStart(2, '0'));
        appendChip(meta, unicodeWords(context.content).toLocaleString() + ' words');
        appendChip(meta, String(context.content.length).toLocaleString() + ' characters');
        var providerLabel = String((context.metadata || {}).providerLabel || '').trim();
        if (providerLabel && '—' !== providerLabel) appendChip(meta, providerLabel);
        (vm.badges || []).forEach(function (badge) { appendChip(meta, String(badge)); });

        renderSections(structured, vm.sections || []);

        var toc = element('fos-output-inspector-toc');
        var isVisualAssets = 'visual-assets' === vm.kind;
        // Visual Designer output already has its own card-based rendering
        // (renderSections above) — showing the raw agent markdown underneath
        // would just dump the <VISUAL_PLAN> JSON block as unstyled prose.
        preview.hidden = isVisualAssets;

        // The Writer's output is the only one that maps onto a real published
        // article (Costudio_WP_Publisher::publish() always uses output_04 as
        // the article body) — so it's the only one worth fetching a faithful,
        // server-rendered preview for. Every other agent (including a
        // failed/unavailable fetch for the Writer) keeps the existing
        // client-side markdown render.
        var useServerPreview = !isVisualAssets && 4 === context.agent && context.productionId > 0 && !!context.nonce;

        if (isVisualAssets) {
            preview.replaceChildren();
            if (toc) { toc.replaceChildren(); toc.hidden = true; }
        } else if (useServerPreview) {
            fetchServerPreview(preview, toc, vm, context, state.previewToken);
        } else {
            renderMarkdownFallback(preview, vm, context);
            buildAndShowToc(toc, preview, vm);
        }

        element('fos-output-modal-title').textContent = vm.title || context.agentName || 'Agent output';
        element('fos-output-agent-badge').textContent = context.agentEmoji || String(context.agent).padStart(2, '0');
        var kicker = element('fos-output-agent-kicker');
        if (kicker && context.agentDesc) kicker.textContent = context.agentDesc;
    }

    function buildAndShowToc(toc, preview, vm) {
        if (!toc) return;
        toc.replaceChildren();
        var built = 'markdown' === vm.kind ? buildToc(preview) : null;
        toc.hidden = !built;
        if (built) toc.appendChild(built);
    }

    function renderMarkdownFallback(preview, vm, context) {
        var rendered = safeMarkdown(vm.rawMarkdown || context.content);
        if (rendered.fallback) preview.appendChild(rendered.fallback);
        else preview.innerHTML = rendered.html; // Only DOMPurify output reaches innerHTML.
        wrapTables(preview);
    }

    /**
     * Fetches the SAME HTML Costudio_WP_Publisher::publish() would actually
     * produce (Costudio_WP_Publisher::preview(), via the costudio_preview_article
     * AJAX action) instead of approximating the article with client-side
     * marked.js. Falls back to the generic client render on any failure —
     * network error, missing DOMPurify, or an empty/error response — so a
     * broken fetch never leaves the tab blank.
     */
    function fetchServerPreview(preview, toc, vm, context, requestToken) {
        preview.replaceChildren();
        var loading = document.createElement('div');
        loading.className = 'fos-output-inspector__preview-loading';
        loading.textContent = 'Loading the real published layout…';
        preview.appendChild(loading);
        if (toc) { toc.replaceChildren(); toc.hidden = true; }

        var body = 'action=costudio_preview_article'
            + '&nonce=' + encodeURIComponent(context.nonce)
            + '&production_id=' + encodeURIComponent(context.productionId);

        var fallback = function () {
            if (requestToken !== state.previewToken) return; // superseded by a newer open()
            preview.replaceChildren();
            renderMarkdownFallback(preview, vm, context);
            buildAndShowToc(toc, preview, vm);
        };

        if (!window.fetch || !window.ajaxurl) { fallback(); return; }

        window.fetch(window.ajaxurl, {
            method: 'POST',
            credentials: 'same-origin',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: body
        }).then(function (response) { return response.json(); }).then(function (res) {
            if (requestToken !== state.previewToken) return;
            var html = res && res.success && res.data ? String(res.data.html || '') : '';
            if (!html || !window.DOMPurify) { fallback(); return; }
            preview.replaceChildren();
            // Unlike the generic markdown sanitize profile, `style` attributes
            // are allowed here: the real renderer inline-styles tables and
            // callout blocks with the destination site's brand accent, which
            // is part of the fidelity this preview exists to show. DOMPurify
            // still sanitizes the attribute value itself (no expression()/
            // url(javascript:) etc.), and dangerous tags stay forbidden.
            preview.innerHTML = window.DOMPurify.sanitize(html, {
                USE_PROFILES: { html: true },
                FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'svg', 'math', 'form']
            });
            wrapTables(preview);
            buildAndShowToc(toc, preview, vm);
        }).catch(fallback);
    }

    /**
     * marked.parse() emits bare <table> elements — wrap each one in a
     * bordered/rounded container so long tables get the same card treatment
     * as everything else, without re-touching innerHTML (DOM move only).
     */
    function wrapTables(preview) {
        Array.prototype.slice.call(preview.querySelectorAll('table')).forEach(function (table) {
            var wrap = document.createElement('div');
            wrap.className = 'fos-output-inspector__table-wrap';
            table.parentNode.insertBefore(wrap, table);
            wrap.appendChild(table);
        });
    }

    /**
     * A lightweight in-flow table of contents, not a sidebar — the shell is a
     * single scrollable column and a two-column layout wasn't part of this
     * pass. Only appears when there's enough structure to be worth navigating
     * (long Agent 04 articles routinely run 4000+ words / a dozen headings).
     */
    function slugify(text, seen) {
        var base = String(text || '').toLowerCase().trim()
            .replace(/[^\p{L}\p{N}\s-]/gu, '')
            .replace(/\s+/g, '-')
            .replace(/-+/g, '-') || 'section';
        var slug = base;
        var suffix = 2;
        while (seen[slug]) { slug = base + '-' + suffix; suffix++; }
        seen[slug] = true;
        return slug;
    }

    function buildToc(preview) {
        var headings = Array.prototype.slice.call(preview.querySelectorAll('h1, h2, h3'));
        if (headings.length < 4) return null;
        var reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
        var seen = {};
        var nav = document.createElement('nav');
        nav.className = 'fos-output-inspector__toc';
        nav.setAttribute('aria-label', 'Table of contents');
        nav.appendChild(textNode('div', 'fos-output-inspector__toc-title', 'Contents'));
        var list = document.createElement('ul');
        headings.forEach(function (heading) {
            if (heading.id) seen[heading.id] = true; else heading.id = slugify(heading.textContent, seen);
            var item = document.createElement('li');
            item.className = 'fos-output-inspector__toc-item fos-output-inspector__toc-item--' + heading.tagName.toLowerCase();
            var link = textNode('a', '', heading.textContent);
            link.href = '#' + heading.id;
            link.addEventListener('click', function (event) {
                event.preventDefault();
                heading.scrollIntoView({ block: 'start', behavior: reduceMotion ? 'auto' : 'smooth' });
            });
            item.appendChild(link);
            list.appendChild(item);
        });
        nav.appendChild(list);
        return nav;
    }

    function setTab(name, focusTab) {
        document.querySelectorAll('#fos-output-modal [data-output-tab]').forEach(function (tab) {
            var active = tab.getAttribute('data-output-tab') === name;
            tab.classList.toggle('is-active', active);
            tab.setAttribute('aria-selected', active ? 'true' : 'false');
            tab.setAttribute('tabindex', active ? '0' : '-1');
            if (active && focusTab) tab.focus();
        });
        document.querySelectorAll('#fos-output-modal .fos-output-inspector__panel').forEach(function (panel) {
            var active = panel.id === 'fos-output-panel-' + name;
            panel.classList.toggle('is-active', active);
            panel.hidden = !active;
        });
        element('fos-output-save-btn').hidden = name !== 'edit';
    }

    function open(options) {
        options = options || {};
        var modal = element('fos-output-modal');
        if (!modal) return;
        state.content = String(options.content || '');
        state.agent = Number(options.agent || 0);
        state.editable = options.editable !== false;
        state.returnFocus = options.trigger || document.activeElement;
        state.previewToken += 1; // invalidates any in-flight fetchServerPreview() from a prior open()

        var context = {
            agent: state.agent,
            agentName: String(options.agentName || ''),
            agentEmoji: String(options.agentEmoji || ''),
            agentDesc: String(options.agentDesc || ''),
            content: state.content,
            metadata: options.metadata || {},
            productionId: Number(options.productionId || 0),
            nonce: String(options.nonce || '')
        };
        var renderer = chooseRenderer(context);
        var vm;
        try { vm = renderer.render(context) || defaultViewModel(context); }
        catch (e) { vm = defaultViewModel(context); }
        renderViewModel(vm, context);
        element('fos-output-editor').value = state.content;
        element('fos-output-tab-edit').hidden = !state.editable;
        setTab(options.mode === 'edit' && state.editable ? 'edit' : 'preview', false);

        modal.style.display = 'flex';
        modal.setAttribute('aria-hidden', 'false');
        document.body.classList.add('fos-modal-open');
        window.setTimeout(function () {
            var dialog = modal.querySelector('.fos-output-inspector__dialog');
            if (dialog) dialog.focus();
        }, 0);
    }

    function close() {
        var modal = element('fos-output-modal');
        if (!modal || modal.getAttribute('aria-hidden') === 'true') return;
        modal.style.display = 'none';
        modal.setAttribute('aria-hidden', 'true');
        document.body.classList.remove('fos-modal-open');
        if (state.returnFocus && typeof state.returnFocus.focus === 'function') state.returnFocus.focus();
    }

    function trapFocus(event) {
        var modal = element('fos-output-modal');
        if (!modal || modal.getAttribute('aria-hidden') === 'true') return;
        if (event.key === 'Escape') { event.preventDefault(); close(); return; }
        if (event.key !== 'Tab') return;
        var focusable = Array.prototype.slice.call(modal.querySelectorAll(focusableSelector)).filter(function (node) {
            return !node.hidden && node.offsetParent !== null;
        });
        if (!focusable.length) return;
        var first = focusable[0];
        var last = focusable[focusable.length - 1];
        if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
        else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
    }

    document.addEventListener('click', function (event) {
        var closeButton = event.target.closest('[data-action="fos-output-inspector-close"]');
        if (closeButton) { event.preventDefault(); close(); return; }
        var tab = event.target.closest('#fos-output-modal [data-output-tab]');
        if (tab) { event.preventDefault(); setTab(tab.getAttribute('data-output-tab'), true); return; }
        var copyPrompt = event.target.closest('[data-copy-visual-prompt]');
        if (copyPrompt) {
            event.preventDefault();
            var prompt = state.copyPrompts[Number(copyPrompt.getAttribute('data-copy-visual-prompt'))] || '';
            if (prompt && window.navigator && window.navigator.clipboard) {
                window.navigator.clipboard.writeText(prompt).then(function () {
                    var original = copyPrompt.textContent;
                    copyPrompt.textContent = 'Copied';
                    window.setTimeout(function () { copyPrompt.textContent = original; }, 1200);
                }).catch(function () { /* Clipboard permission is optional. */ });
            }
        }
    });
    document.addEventListener('keydown', trapFocus);

    window.CostudioOutputInspector = {
        open: open,
        close: close,
        setTab: setTab,
        safeUrl: safeUrl,
        registerRenderer: function (renderer) {
            if (renderer && typeof renderer.supports === 'function' && typeof renderer.render === 'function') renderers.unshift(renderer);
        },
        _test: {
            parseVisualPlan: parseVisualPlan,
            parseLegacyVisualAssets: parseLegacyVisualAssets,
            visualAssetViewModel: visualAssetViewModel,
            safeUrl: safeUrl
        }
    };

    renderers.push({
        supports: function (context) { return Number(context.agent) === 6; },
        render: visualAssetViewModel
    });
}(window, document));
