<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Automation Map — Cartographer</title>
    <!-- Pinned, deliberately. This used to load "latest", which meant every customer
         environment silently took whatever Mermaid had most recently published: the tag
         moved to 12.0.0, a new major, without a line of our code changing. Diagram
         rendering is the one thing on this page, and it must not change underneath a
         shipped product. The integrity hash is this exact file's; upgrading means
         re-testing a diagram and recomputing it, which is the point. -->
    <script src="https://cdn.jsdelivr.net/npm/mermaid@11.17.2/dist/mermaid.min.js"
            integrity="sha384-EOXBFmc3gx5mb+vn0vPvvGqACToJD24hhacX5Yx+8NUUQrHIle/Qi5Bg9o3zKwW2"
            crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <style>
        *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
        body {
            font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
            background: #f3f4f6; color: #1f2937; min-height: 100vh;
            display: flex; flex-direction: column;
        }
        .toolbar {
            background: #ffffff; border-bottom: 1px solid #e5e7eb;
            padding: 12px 20px; display: flex; flex-wrap: wrap; gap: 12px;
            align-items: center; box-shadow: 0 1px 3px rgba(0,0,0,0.06);
        }
        .toolbar label { font-size: 13px; font-weight: 600; color: #6b7280; margin-right: 4px; }
        .toolbar select, .toolbar button {
            font-family: inherit; font-size: 13px; padding: 6px 12px;
            border-radius: 6px; border: 1px solid #d1d5db; background: #fff; color: #1f2937;
        }
        .toolbar select:focus, .toolbar button:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 2px rgba(37,99,235,0.15); }
        .toolbar-group { display: flex; align-items: center; gap: 4px; }
        .toolbar-separator { width: 1px; height: 24px; background: #e5e7eb; }
        .btn-primary { background: #2563eb; color: #fff; border: none; cursor: pointer; font-weight: 600; }
        .btn-primary:hover { background: #1d4ed8; }
        .btn-primary:disabled { background: #93c5fd; cursor: not-allowed; }
        .btn-secondary { background: #f3f4f6; color: #374151; cursor: pointer; font-weight: 500; }
        .btn-secondary:hover { background: #e5e7eb; }
        .btn-secondary:disabled { color: #9ca3af; cursor: not-allowed; }
        .content { flex: 1; padding: 16px 20px; display: flex; flex-direction: column; }
        .diagram-card {
            background: #fff; border-radius: 10px; border: 1px solid #e5e7eb;
            flex: 1; min-height: 400px; padding: 24px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.04); overflow: auto;
        }
        .diagram-card svg { max-width: 100%; height: auto; }
        .message { text-align: center; padding: 60px 20px; color: #6b7280; }
        .message h2 { font-size: 18px; font-weight: 600; color: #374151; margin: 12px 0 8px; }
        .message p { font-size: 14px; }
        .message-icon { font-size: 48px; }
        .message button { font-family: inherit; font-size: 13px; padding: 8px 16px; margin-top: 16px; border-radius: 6px; cursor: pointer; }
        .error-banner {
            background: #fef2f2; color: #991b1b; padding: 10px 20px;
            font-size: 13px; display: none; border-bottom: 1px solid #fecaca;
        }
        .loading { text-align: center; padding: 60px; }
        .spinner {
            display: inline-block; width: 32px; height: 32px;
            border: 3px solid #e5e7eb; border-top-color: #2563eb;
            border-radius: 50%; animation: spin 0.7s linear infinite;
        }
        @keyframes spin { to { transform: rotate(360deg); } }
        .loading-text { margin-top: 12px; font-size: 14px; color: #6b7280; }
        .legend { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 12px; padding: 8px 0; font-size: 12px; color: #6b7280; }
        .legend-item { display: flex; align-items: center; gap: 4px; }
        .legend-dot { width: 10px; height: 10px; border-radius: 50%; }
        .node { cursor: pointer !important; }
    </style>
</head>
<body>
    <div class="toolbar">
        <div class="toolbar-group">
            <label for="entitySelect">Table:</label>
            <select id="entitySelect" disabled>
                <option value="">Loading tables...</option>
            </select>
        </div>
        <button id="btnGenerateDiagram" class="btn-primary" disabled>Generate Diagram</button>
        <div class="toolbar-separator"></div>
        <button id="btnDownload" class="btn-secondary" disabled>Download as SVG</button>
    </div>

    <div id="errorBanner" class="error-banner"></div>

    <div class="content">
        <div class="diagram-card" id="diagramContainer">
            <div class="message" id="placeholderMessage">
                <div class="message-icon">&#9889;</div>
                <h2>Automation Map</h2>
                <p>Select a table to see all automations — plugins, cloud flows, business rules, and workflows — that fire on it.</p>
            </div>
        </div>
        <div class="legend" id="legend" style="display:none;">
            <div class="legend-item"><div class="legend-dot" style="background:#3b82f6;"></div> Plugin Step</div>
            <div class="legend-item"><div class="legend-dot" style="background:#8b5cf6;"></div> Cloud Flow</div>
            <div class="legend-item"><div class="legend-dot" style="background:#f59e0b;"></div> Business Rule</div>
            <div class="legend-item"><div class="legend-dot" style="background:#6b7280;"></div> Classic Workflow</div>
            <div class="legend-item"><div class="legend-dot" style="background:#10b981;"></div> Custom Action / BPF</div>
        </div>
    </div>

    <script>
        // Vendor analytics ping: fire-and-forget product-usage telemetry sent to Cartographer's
        // own backend (not the customer's Dataverse). Cartographer is free now, but this call is
        // kept deliberately for adoption metrics - it must never block the UI or surface an error,
        // so both the outer request and the inner beacon are swallowed on failure.
        function vbAnalytics(eventType, details) {
            try {
                fetch('/api/data/v9.2/vb_configurations?$select=vb_license_key,vb_org_id&$top=1', {
                    headers: { 'Accept': 'application/json', 'OData-MaxVersion': '4.0', 'OData-Version': '4.0' },
                    credentials: 'include'
                }).then(function(r) { return r.json(); }).then(function(d) {
                    if (!d.value || !d.value[0]) return;
                    var c = d.value[0];
                    return fetch('https://cartographer-api.azurewebsites.net/api/analytics/event', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({
                            licenseKey: c.vb_license_key,
                            orgId: c.vb_org_id || '',
                            eventType: eventType,
                            details: JSON.stringify(details || {}),
                            timestamp: new Date().toISOString()
                        })
                    });
                }).catch(function () { /* non-blocking, silent on failure by design */ });
            } catch(e) { /* silent */ }
        }
        vbAnalytics('page_view', {page: 'automation_map'});

        (function () {
            'use strict';

            var API_BASE = '/api/data/v9.2/';
            var diagramContainer = document.getElementById('diagramContainer');
            var entitySelect = document.getElementById('entitySelect');
            var btnDownload = document.getElementById('btnDownload');
            var btnGenerateDiagram = document.getElementById('btnGenerateDiagram');
            var errorBanner = document.getElementById('errorBanner');
            var legend = document.getElementById('legend');
            var currentDiagramSvg = null;
            var componentMap = {};
            // Entity logical name (lowercased) -> crawl configuration id, so the Generate
            // Diagram button knows which vb_CrawlConfiguration to pass to
            // vb_GenerateEntityDiagram. Populated in loadEntities() from the same crawled-
            // components query it already runs; when an entity was crawled under more than
            // one solution, the first crawl configuration found for it wins.
            var entityCrawlConfigMap = {};

            // Automation component types only
            var AUTOMATION_TYPES = [10001001,10001002,10001003,10001004,10001005,10001006,10001007,10001023];
            var TYPE_LABELS = {
                10001001: 'Plugin Step', 10001002: 'Cloud Flow (Auto)', 10001003: 'Cloud Flow (Sched)',
                10001004: 'Cloud Flow (Instant)', 10001005: 'Business Rule', 10001006: 'Classic Workflow',
                10001007: 'Custom Action', 10001023: 'BPF'
            };
            var TYPE_COLORS = {
                10001001: '#3b82f6', 10001002: '#8b5cf6', 10001003: '#8b5cf6',
                10001004: '#8b5cf6', 10001005: '#f59e0b', 10001006: '#6b7280',
                10001007: '#10b981', 10001023: '#10b981'
            };

            // Trigger event choice values
            var TRIGGER_LABELS = {
                10001000: 'Create', 10001001: 'Update', 10001002: 'Delete',
                10001003: 'Retrieve', 10001004: 'Associate', 10001005: 'SetState',
                10001006: 'On-Demand', 10001007: 'Scheduled', 10001008: 'Other'
            };

            var STAGE_LABELS = {
                10001000: 'Pre-Validation', 10001001: 'Pre-Operation',
                10001002: 'Post-Operation', 10001003: 'Async', 10001004: ''
            };

            // htmlLabels off keeps plain node labels on native SVG <text> rather than inside a
            // <foreignObject>. Note what it does NOT do: the markdown-string labels this page
            // builds are rendered through a <foreignObject> regardless of this flag - measured on
            // the pinned 11.17.2 build, a rendered diagram carries one per node. That is why the
            // download handler below writes the SVG out directly instead of drawing it onto a
            // <canvas>: a <foreignObject> taints the canvas and canvas.toBlob() then throws a
            // SecurityError, so a rasterising export cannot coexist with styled labels. Keeping
            // the flag off still costs nothing and helps any label that is not a markdown string.
            //
            // The Mermaid version is pinned in the <script> tag above; mermaid.render(id,
            // definition).then(...) is the v10+ promise API, and flowchart.htmlLabels is where
            // this flag lives on that generation.
            mermaid.initialize({
                startOnLoad: false, theme: 'default', securityLevel: 'loose',
                flowchart: { useMaxWidth: true, htmlLabels: false, curve: 'basis' }
            });

            function apiGet(url) {
                return fetch(API_BASE + url, {
                    headers: { 'Accept': 'application/json', 'OData-MaxVersion': '4.0', 'OData-Version': '4.0',
                               'Prefer': 'odata.include-annotations="*",odata.maxpagesize=500' },
                    credentials: 'include'
                }).then(function (r) {
                    if (!r.ok) return r.text().then(function (t) { throw new Error('API error (' + r.status + '): ' + t); });
                    return r.json();
                });
            }

            function apiPost(url, body) {
                return fetch(API_BASE + url, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json', 'Accept': 'application/json',
                               'OData-MaxVersion': '4.0', 'OData-Version': '4.0' },
                    credentials: 'include', body: JSON.stringify(body)
                }).then(function (r) {
                    if (!r.ok) return r.text().then(function (t) { throw new Error('API error (' + r.status + '): ' + t); });
                    return r.json().catch(function () { return {}; });
                });
            }

            function showError(msg) { errorBanner.textContent = msg; errorBanner.style.display = 'block'; }
            function clearError() { errorBanner.style.display = 'none'; }
            function showLoading(msg) {
                diagramContainer.innerHTML = '<div class="loading"><div class="spinner"></div><div class="loading-text">' + (msg || 'Loading...') + '</div></div>';
                currentDiagramSvg = null; legend.style.display = 'none';
                refreshGenerateButtonState();
            }
            function showMessage(title, body, actionHtml) {
                diagramContainer.innerHTML = '<div class="message"><div class="message-icon">&#128196;</div><h2>' + title + '</h2><p>' + body + '</p>' + (actionHtml || '') + '</div>';
                currentDiagramSvg = null; btnDownload.disabled = true; legend.style.display = 'none';
                refreshGenerateButtonState();
            }
            function htmlEncode(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
            function sanitizeMermaidId(s) { return s.replace(/[^a-zA-Z0-9_]/g, '_').substring(0, 40); }

            // Reflects entity-selection + diagram state onto the toolbar's Generate/Regenerate
            // Diagram button. Called from every function that changes what diagramContainer
            // shows, so the button never goes stale.
            function refreshGenerateButtonState() {
                var hasEntity = !!entitySelect.value;
                btnGenerateDiagram.disabled = !hasEntity;
                btnGenerateDiagram.textContent = currentDiagramSvg ? 'Regenerate Diagram' : 'Generate Diagram';
            }

            // --- Generate / regenerate a diagram via vb_GenerateEntityDiagram ---
            // This is the only place in the app that calls vb_GenerateEntityDiagram. Unlike the
            // diagram built above (from LIVE sdkmessageprocessingsteps/workflows), the plugin
            // reads vb_components as of the last crawl and upserts vb_EntityDiagram - so this
            // both keeps that cached record fresh and lets a user produce a first diagram, or
            // refresh a stale one after a re-crawl, without waiting on anything else.
            function generateDiagram(entityLogicalName) {
                entityLogicalName = entityLogicalName || entitySelect.value;
                if (!entityLogicalName) return;

                var crawlConfigId = entityCrawlConfigMap[entityLogicalName.toLowerCase()];
                if (!crawlConfigId) {
                    showError('Could not find a crawl configuration for "' + entityLogicalName + '".');
                    return;
                }

                clearError();

                var buttons = [btnGenerateDiagram, document.getElementById('btnGenerateInline')]
                    .filter(function (b) { return !!b; });
                buttons.forEach(function (b) {
                    b.disabled = true;
                    b.innerHTML = '<span class="spinner" style="width:12px;height:12px;border-width:2px;margin-right:4px;vertical-align:middle;"></span>Generating...';
                });

                apiPost('vb_GenerateEntityDiagram', {
                    CrawlConfigurationId: crawlConfigId,
                    EntityLogicalName: entityLogicalName
                }).then(function (result) {
                    // The user may have switched tables while this was in flight - don't
                    // clobber whatever they're looking at now with this stale response.
                    if (entitySelect.value !== entityLogicalName) return;

                    if (result && result.Success) {
                        if (result.MermaidDefinition) {
                            renderMermaid(result.MermaidDefinition);
                        } else {
                            // Success: true with an empty MermaidDefinition is the plugin
                            // correctly reporting "nothing to draw", not a failure - it persists
                            // no vb_EntityDiagram row when a table has no entity-bound plugins,
                            // cloud flows, business rules or workflows, which is the common case.
                            // Saying Cartographer "generated a diagram" here was a lie: there is
                            // no diagram, and there never will be one for this table unless an
                            // automation gets added to it.
                            showMessage('No Automations Found',
                                '<strong>' + htmlEncode(entityLogicalName) + '</strong> has no automations to diagram - ' +
                                'Cartographer checked the latest crawl and found no entity-bound plugins, cloud flows, ' +
                                'business rules, or workflows for this table.');
                        }
                    } else {
                        var err = (result && result.error && result.error.message) ||
                            (result && result.Message) ||
                            'Diagram generation failed. Please try again.';
                        showError(err);
                    }
                }).catch(function (err) {
                    showError('Failed to generate diagram: ' + (err && err.message ? err.message : err));
                }).then(function () {
                    refreshGenerateButtonState();
                    var inlineBtn = document.getElementById('btnGenerateInline');
                    if (inlineBtn) { inlineBtn.disabled = false; inlineBtn.textContent = 'Generate Diagram'; }
                });
            }

            // Clean up plugin step names — strip namespaces, add spaces before capitals
            function friendlyName(rawName) {
                if (!rawName) return 'Unknown';
                var name = rawName;
                // Strip everything before the last colon (message info) e.g. "Namespace.Plugin: Create of entity"
                var colonIdx = name.indexOf(':');
                if (colonIdx > 0) {
                    var afterColon = name.substring(colonIdx + 1).trim();
                    var beforeColon = name.substring(0, colonIdx);
                    // Get just the class name from the namespace
                    var parts = beforeColon.split('.');
                    var className = parts[parts.length - 1];
                    // Add spaces before capitals: "OpportunityClose" -> "Opportunity Close"
                    className = className.replace(/([a-z])([A-Z])/g, '$1 $2');
                    name = className + ': ' + afterColon;
                } else {
                    // No colon — try stripping namespace dots
                    var dotParts = name.split('.');
                    if (dotParts.length > 2) {
                        // Take last 2 segments: "Microsoft.Dynamics.Sales.OpportunityRevenue" -> "Sales.Opportunity Revenue"
                        var last = dotParts[dotParts.length - 1].replace(/([a-z])([A-Z])/g, '$1 $2');
                        var secondLast = dotParts[dotParts.length - 2];
                        name = secondLast + ' — ' + last;
                    }
                }
                return name;
            }

            // --- Load entity definitions, restricted to tables Cartographer actually crawled ---
            // EntityDefinitions returns every table in the environment (typically 800+, including
            // every Microsoft system table) - unfiltered, that's an unusably long picker and,
            // worse, building that many <option> elements one appendChild() at a time is what
            // froze the renderer on first load inside the app shell. Cross-referencing against
            // vb_components keeps the list to tables this tool has automation data for (which is
            // the only kind of table this page can show anything useful for anyway), and
            // buildEntityOptions() below writes the whole list in a single innerHTML assignment
            // instead of N synchronous DOM mutations.
            function loadEntities() {
                entitySelect.innerHTML = '<option value="">Loading tables...</option>';
                entitySelect.disabled = true;

                Promise.all([
                    apiGet("EntityDefinitions?$select=LogicalName,DisplayName,IsIntersect,IsCustomizable"),
                    apiGet("vb_components?$select=vb_entity_logical_name,_vb_crawlconfiguration_value&$filter=vb_entity_logical_name ne null")
                ]).then(function (results) {
                    var data = results[0];
                    var crawledRows = results[1].value || [];

                    var crawledNames = {};
                    entityCrawlConfigMap = {};
                    crawledRows.forEach(function (c) {
                        if (!c.vb_entity_logical_name) return;
                        var key = c.vb_entity_logical_name.toLowerCase();
                        crawledNames[key] = true;
                        // First crawl configuration seen for this entity wins - good enough
                        // since this picker isn't scoped to a single solution to begin with.
                        if (!entityCrawlConfigMap[key] && c._vb_crawlconfiguration_value) {
                            entityCrawlConfigMap[key] = c._vb_crawlconfiguration_value;
                        }
                    });

                    var entities = (data.value || []).filter(function (e) {
                        // Filter to non-intersect entities with a display name that were actually
                        // crawled (i.e. Cartographer has at least one component tied to them).
                        if (e.IsIntersect) return false;
                        var label = e.DisplayName && e.DisplayName.UserLocalizedLabel;
                        if (!label || !label.Label) return false;
                        return crawledNames[e.LogicalName.toLowerCase()] === true;
                    }).sort(function (a, b) {
                        var la = a.DisplayName.UserLocalizedLabel.Label;
                        var lb = b.DisplayName.UserLocalizedLabel.Label;
                        return la.localeCompare(lb);
                    });

                    buildEntityOptions(entities);
                }).catch(function (err) {
                    showError('Failed to load tables: ' + err.message);
                    entitySelect.innerHTML = '<option value="">Error loading tables</option>';
                });
            }

            // Renders the <option> list as a single HTML string + one innerHTML assignment
            // instead of one appendChild() per table, so even a large crawled set renders in one
            // reflow rather than hundreds.
            function buildEntityOptions(entities) {
                var html = ['<option value="">-- Select Table (' + entities.length + ') --</option>'];
                entities.forEach(function (e) {
                    var label = e.DisplayName.UserLocalizedLabel.Label;
                    html.push('<option value="' + htmlEncode(e.LogicalName) + '">' +
                        htmlEncode(label) + ' (' + htmlEncode(e.LogicalName) + ')</option>');
                });
                entitySelect.innerHTML = html.join('');
                entitySelect.disabled = false;

                if (entities.length === 0) {
                    showMessage('No Crawled Tables Yet',
                        'Run a crawl for a solution first - the Automation Map only lists tables Cartographer has component data for.');
                }
            }

            // --- Load automations from LIVE system tables (not crawled data) ---
            function loadAutomationsForEntity(entityLogicalName) {
                clearError();
                showLoading('Loading automations for ' + entityLogicalName + '...');
                btnDownload.disabled = true;
                componentMap = {};

                // Query 3 sources in parallel:
                // 1. Plugin steps (sdkmessageprocessingstep) via filter on primary entity
                // 2. Business rules (workflow category=2)
                // 3. Workflows & cloud flows (workflow category=0,5)
                var pluginQuery = "sdkmessageprocessingsteps?$select=name,stage,mode,sdkmessageprocessingstepid,rank" +
                    "&$expand=sdkmessageid($select=name)" +
                    "&$filter=sdkmessagefilterid/primaryobjecttypecode eq '" + entityLogicalName + "' and ishidden/Value eq false and statecode eq 0";

                var brQuery = "workflows?$select=name,workflowid,primaryentity,category,statuscode,statecode" +
                    "&$filter=primaryentity eq '" + entityLogicalName + "' and category eq 2 and statecode eq 1";

                var flowQuery = "workflows?$select=name,workflowid,primaryentity,category,statuscode,statecode,type" +
                    "&$filter=primaryentity eq '" + entityLogicalName + "' and (category eq 0 or category eq 5) and statecode eq 1";

                // Also query crawled components for AI summaries
                var crawledQuery = "vb_components?$select=vb_source_id,vb_ai_summary,vb_componentid" +
                    "&$filter=vb_entity_logical_name eq '" + entityLogicalName + "' and vb_ai_summary ne null";

                Promise.all([
                    apiGet(pluginQuery).catch(function () { return { value: [] }; }),
                    apiGet(brQuery).catch(function () { return { value: [] }; }),
                    apiGet(flowQuery).catch(function () { return { value: [] }; }),
                    apiGet(crawledQuery).catch(function () { return { value: [] }; })
                ]).then(function (results) {
                    // Build a lookup of source_id -> ai_summary from crawled data
                    var summaryMap = {};
                    (results[3].value || []).forEach(function (c) {
                        if (c.vb_source_id && c.vb_ai_summary) {
                            summaryMap[c.vb_source_id.toLowerCase()] = c.vb_ai_summary;
                        }
                        if (c.vb_componentid) {
                            componentMap[c.vb_source_id] = c.vb_componentid;
                        }
                    });
                    var components = [];

                    // Process plugin steps
                    (results[0].value || []).forEach(function (step) {
                        var msgName = step.sdkmessageid ? step.sdkmessageid.name : 'Unknown';
                        var stageVal = step.stage; // 10=PreValidation, 20=PreOperation, 40=PostOperation
                        var stageMap = { 10: 10001000, 20: 10001001, 40: 10001002 };
                        var triggerMap = { 'Create': 10001000, 'Update': 10001001, 'Delete': 10001002,
                                          'Retrieve': 10001003, 'Associate': 10001004, 'SetState': 10001005,
                                          'SetStateDynamicEntity': 10001005 };

                        var stepId = step.sdkmessageprocessingstepid;
                        components.push({
                            name: friendlyName(step.name || 'Plugin Step'),
                            type: 10001001,
                            trigger: triggerMap[msgName] !== undefined ? triggerMap[msgName] : 10001008,
                            stage: stageMap[stageVal] || 10001004,
                            id: stepId,
                            isAsync: step.mode === 1,
                            summary: summaryMap[stepId ? stepId.toLowerCase() : ''] || ''
                        });
                    });

                    // Process business rules
                    (results[1].value || []).forEach(function (br) {
                        var brId = br.workflowid;
                        components.push({
                            name: br.name || 'Business Rule',
                            type: 10001005,
                            trigger: 10001008,
                            stage: 10001004,
                            id: brId,
                            summary: summaryMap[brId ? brId.toLowerCase() : ''] || ''
                        });
                    });

                    // Process workflows and cloud flows
                    (results[2].value || []).forEach(function (wf) {
                        var compType;
                        if (wf.category === 0) {
                            compType = 10001006; // Classic Workflow
                        } else if (wf.category === 5) {
                            compType = 10001002; // Cloud Flow (default to Automated)
                        } else {
                            compType = 10001006;
                        }
                        var wfId = wf.workflowid;
                        components.push({
                            name: wf.name || 'Workflow',
                            type: compType,
                            trigger: 10001008,
                            stage: 10001003,
                            summary: summaryMap[wfId ? wfId.toLowerCase() : ''] || '',
                            id: wf.workflowid
                        });
                    });

                    if (components.length === 0) {
                        showMessage('No Automations Found',
                            'No active plugins, cloud flows, business rules, or workflows are registered on <strong>' +
                            htmlEncode(entityLogicalName) + '</strong> right now. If a crawl has run since, ' +
                            'generating a diagram will check the latest crawled data for this table.',
                            '<button id="btnGenerateInline" class="btn-primary">Generate Diagram</button>');
                        var inlineBtn = document.getElementById('btnGenerateInline');
                        if (inlineBtn) {
                            inlineBtn.addEventListener('click', function () { generateDiagram(entityLogicalName); });
                        }
                        return;
                    }

                    buildAndRenderDiagram(entityLogicalName, components);
                }).catch(function (err) {
                    showError('Failed to load automations: ' + err.message);
                    showMessage('Error', 'Could not load automation data.');
                });
            }

            // --- Build Mermaid diagram client-side ---
            function buildAndRenderDiagram(entityLogicalName, components) {
                // Group by trigger event
                var groups = {};
                components.forEach(function (c) {
                    var eventLabel = TRIGGER_LABELS[c.trigger] || 'Other';
                    if (!groups[eventLabel]) groups[eventLabel] = [];
                    groups[eventLabel].push(c);
                });

                // Build Mermaid flowchart
                var lines = ['flowchart LR'];
                lines.push('');

                // Entity node in center
                var entityId = sanitizeMermaidId(entityLogicalName);
                lines.push('    ' + entityId + '[("' + entityLogicalName + '")]');
                lines.push('    style ' + entityId + ' fill:#1e40af,stroke:#1e3a8a,color:#fff,stroke-width:2px');
                lines.push('');

                var nodeIndex = 0;
                var eventOrder = ['Create', 'Update', 'Delete', 'Retrieve', 'Associate', 'SetState', 'On-Demand', 'Scheduled', 'Other'];

                eventOrder.forEach(function (eventLabel) {
                    var comps = groups[eventLabel];
                    if (!comps || comps.length === 0) return;

                    var subId = 'event_' + sanitizeMermaidId(eventLabel);
                    lines.push('    subgraph ' + subId + '["' + eventLabel + '"]');

                    comps.forEach(function (comp) {
                        nodeIndex++;
                        var nodeId = 'n' + nodeIndex;
                        var typeLabel = TYPE_LABELS[comp.type] || 'Unknown';
                        var stageLabel = STAGE_LABELS[comp.stage] || '';
                        if (comp.isAsync) stageLabel = 'Async';
                        // Strip backticks/asterisks/underscores too, not just quotes and angle
                        // brackets - the node text below is now a Mermaid markdown string (see
                        // mermaid.initialize above), so a stray backtick would close the label
                        // early. Underscores are NOT stripped: they are how every Power
                        // Platform logical name is built, and removing them rendered
                        // msdyn_analysiscomponent as msdynanalysiscomponent with nothing on
                        // screen to say the name was wrong.
                        // early and a stray */_ would trigger emphasis the source data never asked
                        // for.
                        var label = comp.name.replace(/"/g, "'").replace(/[<>`#]/g, '');
                        if (label.length > 45) label = label.substring(0, 42) + '...';

                        // Mermaid markdown-string node text ("`...`"), not raw HTML: with
                        // flowchart.htmlLabels off, <b>/<i>/<small> tags would no longer be
                        // interpreted and would show up as literal characters on every node.
                        // **bold**/*italic* plus real newlines render the same bold name / italic
                        // type / stacked summary line through Mermaid's own tspan-based text
                        // renderer, which never touches <foreignObject>.
                        var nodeLabel = '**' + label + '**\n*' + typeLabel + '*';
                        if (stageLabel) nodeLabel += ' · ' + stageLabel;

                        // Add AI summary snippet if available
                        if (comp.summary) {
                            var snippet = comp.summary.replace(/"/g, "'").replace(/[<>#`]/g, '');
                            if (snippet.length > 60) snippet = snippet.substring(0, 57) + '...';
                            nodeLabel += '\n' + snippet;
                        }

                        lines.push('        ' + nodeId + '["`' + nodeLabel + '`"]');

                        var color = TYPE_COLORS[comp.type] || '#6b7280';
                        lines.push('        style ' + nodeId + ' fill:' + color + ',stroke:' + color + ',color:#fff,stroke-width:1px');
                    });

                    lines.push('    end');
                    lines.push('    ' + entityId + ' --> ' + subId);
                    lines.push('');
                });

                var mermaidDef = lines.join('\n');
                renderMermaid(mermaidDef);
            }

            // --- Render Mermaid ---
            function renderMermaid(definition) {
                var id = 'mermaid-' + Date.now();
                mermaid.render(id, definition).then(function (result) {
                    diagramContainer.innerHTML = result.svg;
                    currentDiagramSvg = result.svg;
                    btnDownload.disabled = false;
                    legend.style.display = 'flex';
                    attachNodeClickHandlers();
                    refreshGenerateButtonState();
                }).catch(function (err) {
                    showError('Mermaid rendering error: ' + err.message);
                    showMessage('Rendering Error', 'The diagram could not be rendered.');
                });
            }

            function attachNodeClickHandlers() {
                diagramContainer.querySelectorAll('.node').forEach(function (node) {
                    node.style.cursor = 'pointer';
                    node.addEventListener('click', function () {
                        var nodeId = node.id ? node.id.replace(/^flowchart-/, '').split('-')[0] : '';
                        var compId = componentMap[nodeId];
                        if (compId) openComponentRecord(compId);
                    });
                });
            }

            function openComponentRecord(componentId) {
                try {
                    if (parent && parent.Xrm && parent.Xrm.Navigation) {
                        parent.Xrm.Navigation.openForm({ entityName: 'vb_component', entityId: componentId });
                        return;
                    }
                } catch (e) {}
                window.open(window.location.origin + '/main.aspx?etn=vb_component&id=' + componentId + '&pagetype=entityrecord', '_blank');
            }

            // --- Download as SVG ---
            //
            // Deliberately NOT a PNG. Rasterising means drawing the SVG onto a <canvas>, and
            // Mermaid renders node labels inside <foreignObject> elements whenever markdown
            // strings are used - which taints the canvas, so canvas.toBlob() throws SecurityError
            // and no file is ever produced. Turning markdown labels off would buy the PNG back at
            // the cost of every label's formatting and line break.
            //
            // The SVG needs no canvas at all, so nothing can taint it. It is also the better
            // artifact: the diagram is vector line art, so it stays sharp at any size, and Word,
            // PowerPoint and every browser open it directly.
            btnDownload.addEventListener('click', function () {
                if (!currentDiagramSvg) return;
                var svgEl = diagramContainer.querySelector('svg');
                if (!svgEl) return;

                clearError();

                try {
                    var clone = svgEl.cloneNode(true);
                    // A standalone file has no page around it: the namespace has to be declared on
                    // the element itself, and the size has to be baked in rather than inherited
                    // from the container's CSS, or the file opens blank or unscaled.
                    clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
                    clone.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
                    var bbox = svgEl.getBoundingClientRect();
                    if (bbox.width && bbox.height) {
                        clone.setAttribute('width', Math.ceil(bbox.width));
                        clone.setAttribute('height', Math.ceil(bbox.height));
                    }

                    var svgData = new XMLSerializer().serializeToString(clone);
                    if (svgData.indexOf('<?xml') !== 0) {
                        svgData = '<?xml version="1.0" encoding="UTF-8"?>' + svgData;
                    }

                    var blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
                    var url = URL.createObjectURL(blob);
                    var a = document.createElement('a');
                    a.href = url;
                    a.download = (entitySelect.value || 'automation-map') + '.svg';
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    setTimeout(function () { try { URL.revokeObjectURL(url); } catch (e) {} }, 60000);
                } catch (e) {
                    // A click must always end in either a file or a reason it did not happen.
                    showError('Could not export the diagram' + (e && e.message ? ' (' + e.message + ')' : '') + '.');
                }
            });

            // --- Entity select change ---
            entitySelect.addEventListener('change', function () {
                if (entitySelect.value) {
                    loadAutomationsForEntity(entitySelect.value);
                } else {
                    showMessage('Automation Map', 'Select a table to see all automations.');
                }
            });

            // --- Generate / Regenerate Diagram (toolbar) ---
            btnGenerateDiagram.addEventListener('click', function () {
                generateDiagram(entitySelect.value);
            });

            // --- Init: the Automation Map is no longer gated by tier (Cartographer's
            // free tier is now the feature superset; the old Professional/Enterprise
            // gate is gone). ---
            loadEntities();
        })();
    </script>
</body>
</html>
