from __future__ import annotations

import json
import os
import webbrowser
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Iterable, Literal


DetectedKind = Literal["package", "module"]


@dataclass(frozen=True)
class Detected:
    name: str
    group: str
    kind: DetectedKind


def _iter_py_files(base_dir: Path) -> Iterable[Path]:
    for path in base_dir.rglob("*.py"):
        if "__pycache__" in path.parts:
            continue
        yield path


def _is_package_dir(dir_path: Path) -> bool:
    return (dir_path / "__init__.py").exists()


def _to_module_name(app_dir: Path, file_path: Path) -> str:
    rel = file_path.relative_to(app_dir)
    parts = list(rel.parts)
    if parts[-1] == "__init__.py":
        parts = parts[:-1]
    else:
        parts[-1] = parts[-1].removesuffix(".py")
    return ".".join(["app", *parts])


def _group_for(module_name: str) -> str:
    parts = module_name.split(".")
    if len(parts) < 2:
        return "app"
    second = parts[1]
    if second in {"core", "core_modules", "addons", "api", "tasks", "models", "schemas"}:
        return second
    return "app"


def detect_backend(app_dir: Path) -> list[Detected]:
    packages: set[str] = set()
    modules: set[str] = set()

    for file_path in _iter_py_files(app_dir):
        modules.add(_to_module_name(app_dir, file_path))

        parent = file_path.parent
        while parent != app_dir.parent and parent.is_dir():
            if _is_package_dir(parent):
                pkg_name = "app" + "." + ".".join(parent.relative_to(app_dir).parts)
                packages.add(pkg_name)
            parent = parent.parent

    detected: list[Detected] = []
    for name in sorted(packages):
        detected.append(Detected(name=name, group=_group_for(name), kind="package"))

    for name in sorted(modules):
        if name.endswith(".__init__"):
            continue
        detected.append(Detected(name=name, group=_group_for(name), kind="module"))
    return detected


def _core_tags(detected: list[Detected], limit: int = 10) -> list[str]:
    tags: list[str] = []
    for d in detected:
        if d.kind != "package":
            continue
        if not d.name.startswith("app.core."):
            continue
        if len(d.name.split(".")) == 3:
            tags.append(d.name)
    return sorted(tags)[:limit]


def _detect_mcp_status() -> dict[str, str | bool]:
    enabled_raw = os.getenv("MCP_ENABLED", "")
    enabled = enabled_raw.strip().lower() in {"1", "true", "yes", "on"}
    local_url = os.getenv("MCP_LOCAL_ENDPOINT_URL")
    ext_key = os.getenv("MCP_OPENAI_API_KEY") or os.getenv("MCP_ANTHROPIC_API_KEY")
    auth = os.getenv("MCP_AUTH_TOKEN")

    if not enabled:
        return {"mode": "Inactive", "active": False}

    has_local = bool(local_url and local_url.strip())
    has_external = bool(ext_key and ext_key.strip())

    if has_local and has_external:
        mode = "Hybrid"
    elif has_local:
        mode = "Local"
    elif has_external:
        mode = "External"
    else:
        mode = "Inactive"

    return {"mode": mode, "active": bool(auth and auth.strip() and mode != "Inactive")}


def build_html(detected: list[Detected]) -> str:
    mcp = _detect_mcp_status()
    payload = {
        "detected": [asdict(d) for d in detected],
        "core_tags": _core_tags(detected),
        "mcp": mcp,
    }
    data_json = json.dumps(payload, ensure_ascii=False)
    template = """<!doctype html><html lang="es"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>ERP3 — Living Map</title><link rel="preconnect" href="https://fonts.googleapis.com"><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;600;700&family=Syne:wght@400;700;800&display=swap" rel="stylesheet"><style>*{box-sizing:border-box}html,body{height:100%;margin:0;overflow:hidden;background:#080b10;color:#c9d1d9;font-family:'JetBrains Mono',monospace}:root{--surface:#0d1117;--border:#1e2530;--text-muted:#4a5568;--text-dim:#2d3748;--accent-g:#00e599;--accent-b:#4299e1;--accent-o:#f6ad55;--node-bg:rgba(13,17,23,.95)}#sidebar{position:fixed;left:0;top:0;bottom:0;width:300px;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;z-index:100;user-select:none}#sidebar-header{padding:24px 20px 16px;border-bottom:1px solid var(--border)}#sidebar-header .logo{font-family:'Syne',sans-serif;font-size:22px;font-weight:800;letter-spacing:-.5px;color:#fff}#sidebar-header .logo em{color:var(--accent-g);font-style:normal}#sidebar-header .sub{font-size:9px;text-transform:uppercase;letter-spacing:3px;color:var(--text-muted);margin-top:4px}#module-list{flex:1;overflow-y:auto;padding:16px 20px}.group-title{font-size:9px;text-transform:uppercase;letter-spacing:3px;color:var(--text-muted);margin:16px 0 10px}.module-item{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;cursor:pointer;transition:background .15s;font-size:11px;color:#c9d1d9}.module-item:hover{background:rgba(255,255,255,.04)}.module-item.active{background:rgba(0,229,153,.08);color:var(--accent-g)}.module-dot{width:6px;height:6px;border-radius:50%;background:var(--accent-g);opacity:.4;flex-shrink:0}.module-item.active .module-dot{opacity:1;box-shadow:0 0 6px var(--accent-g)}#sidebar-footer{padding:14px 20px;border-top:1px solid var(--border);font-size:9px;color:var(--text-dim);line-height:1.8}.status-row{display:flex;align-items:center;gap:6px}.status-dot{width:5px;height:5px;border-radius:50%;background:var(--accent-g);animation:blink 2s infinite}@keyframes blink{0%,100%{opacity:1}50%{opacity:.2}}#canvas-wrap{position:fixed;left:300px;top:0;right:0;bottom:0;overflow:hidden;cursor:grab;background:#080b10;background-image:radial-gradient(circle at 30% 40%,rgba(0,229,153,.03) 0,transparent 60%),radial-gradient(circle at 80% 70%,rgba(66,153,225,.03) 0,transparent 50%),repeating-linear-gradient(0deg,transparent,transparent 39px,#111820 39px,#111820 40px),repeating-linear-gradient(90deg,transparent,transparent 39px,#111820 39px,#111820 40px)}#canvas-wrap.dragging{cursor:grabbing}#canvas{position:absolute;top:0;left:0;transform-origin:0 0;will-change:transform}#connections{position:absolute;top:0;left:0;pointer-events:none;overflow:visible}.node{position:absolute;background:var(--node-bg);border:1px solid var(--border);border-radius:12px;transition:border-color .25s,box-shadow .25s,transform .2s;cursor:pointer;user-select:none}.node:hover,.node.selected{border-color:rgba(255,255,255,.25);box-shadow:0 0 24px rgba(0,0,0,.6),inset 0 0 0 1px rgba(255,255,255,.04);transform:translateY(-2px);z-index:10}.node-label{font-size:8px;text-transform:uppercase;letter-spacing:3px;margin-bottom:4px}.node-title{font-family:'Syne',sans-serif;font-weight:800;letter-spacing:-.5px;line-height:1}.node-frontend{left:60px;top:320px;width:180px;padding:20px}.node-frontend .node-label{color:var(--text-muted)}.node-frontend .node-title{font-size:22px;color:#fff}.node-rest{left:310px;top:350px;width:100px;height:44px;border-radius:8px;display:flex;align-items:center;justify-content:center}.node-rest span{font-size:10px;font-weight:700;color:var(--text-muted);letter-spacing:1px}.node-core{left:490px;top:260px;width:200px;border-radius:50%!important;border-color:rgba(0,229,153,.25)!important;box-shadow:0 0 60px rgba(0,229,153,.08),inset 0 0 40px rgba(0,229,153,.03)!important;padding:28px 20px 20px;aspect-ratio:1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center}.node-core .node-label{color:var(--accent-g)}.node-core .node-title{font-size:28px;color:#fff}.core-tags{margin-top:10px;display:flex;gap:4px;justify-content:center;max-height:80px;overflow:hidden;flex-wrap:wrap}.core-tag{font-size:8px;padding:2px 6px;border-radius:4px;background:rgba(0,229,153,.08);border:1px solid rgba(0,229,153,.15);color:rgba(0,229,153,.6)}.node-rhombus{transform:rotate(45deg);border-radius:16px!important}.node-rhombus .rhombus-content{transform:rotate(-45deg);height:100%;width:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;line-height:1.1}.node-db-master{left:600px;top:540px;width:150px;height:90px;border-color:rgba(0,229,153,.3)!important}.node-db-master .node-label{color:var(--accent-g)}.node-db-master .node-title{font-size:16px;color:#fff}.node-db-replica{left:420px;top:540px;width:150px;height:90px;border-color:rgba(66,153,225,.2)!important;opacity:.75}.node-db-replica .node-label{color:var(--accent-b)}.node-db-replica .node-title{font-size:16px;color:#fff}.node-cdc{left:530px;top:140px;width:160px;height:48px;border-color:rgba(246,173,85,.3)!important;display:flex;align-items:center;justify-content:center;border-radius:10px!important}.node-cdc .node-label{color:var(--accent-o);margin:0}.node-kafka{left:540px;top:60px;width:140px;height:38px;border-color:var(--border)!important;background:rgba(13,17,23,.6)!important;display:flex;align-items:center;justify-content:center;border-radius:6px!important}.node-kafka span{font-size:9px;font-weight:700;color:var(--text-muted);letter-spacing:2px}.node-rabbit{left:780px;top:345px;width:140px;padding:14px 16px;border-radius:10px!important}.node-rabbit .node-title{font-size:14px;color:#fff}#controls{position:fixed;right:20px;bottom:20px;z-index:200;display:flex;flex-direction:column;gap:6px}.ctrl-btn{width:36px;height:36px;border-radius:8px;background:var(--surface);border:1px solid var(--border);color:#c9d1d9;font-size:16px;font-family:'Syne',sans-serif;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s;user-select:none}.ctrl-btn:hover{background:var(--border);color:var(--accent-g);border-color:var(--accent-g)}#zoom-level{font-size:9px;color:var(--text-muted);text-align:center}#minimap{position:fixed;right:20px;top:20px;z-index:200;width:160px;height:100px;border-radius:8px;background:var(--surface);border:1px solid var(--border);overflow:hidden;cursor:pointer}#minimap canvas{width:100%;height:100%}#minimap-label{position:absolute;bottom:4px;left:6px;font-size:8px;color:var(--text-dim);text-transform:uppercase;letter-spacing:2px}#tooltip{position:fixed;z-index:300;pointer-events:none;background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:10px 14px;font-size:11px;line-height:1.6;max-width:240px;box-shadow:0 8px 32px rgba(0,0,0,.5);opacity:0;transition:opacity .15s}#tooltip.visible{opacity:1}#tooltip .tt-title{font-family:'Syne',sans-serif;font-weight:700;font-size:13px;color:#fff;margin-bottom:4px}#tooltip .tt-desc{color:var(--text-muted);font-size:10px}#tooltip .tt-badge{display:inline-block;margin-top:6px;padding:2px 7px;border-radius:4px;font-size:9px;background:rgba(0,229,153,.1);color:var(--accent-g);border:1px solid rgba(0,229,153,.2)}::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}</style></head><body><aside id="sidebar"><div id="sidebar-header"><div class="logo">ERP3 <em>OS</em></div><div class="sub">Living Backend Map</div></div><div id="module-list"><div class="group-title" id="detected-count"></div><div id="module-items"></div></div><div id="sidebar-footer"><div class="status-row"><span class="status-dot"></span>SYSTEM ONLINE</div><div style="margin-top:4px;">BUILD · SUCCESSFUL</div><div style="margin-top:2px;font-size:8px;opacity:.5;">scroll / drag / pinch</div></div></aside><div id="canvas-wrap"><div id="canvas"><svg id="connections" xmlns="http://www.w3.org/2000/svg"><defs><marker id="arr" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L0,8 L8,4 Z" fill="#2d3748"/></marker><marker id="arr-g" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L0,8 L8,4 Z" fill="rgba(0,229,153,0.5)"/></marker><marker id="arr-b" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L0,8 L8,4 Z" fill="rgba(66,153,225,0.4)"/></marker><marker id="arr-o" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L0,8 L8,4 Z" fill="rgba(246,173,85,0.45)"/></marker><filter id="glow"><feGaussianBlur stdDeviation="3" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs></svg><div class="node node-frontend" data-id="frontend" data-title="Frontend" data-desc="Capa UI consumiendo el backend por REST." data-badge="UI Layer"><div class="node-label">Interface</div><div class="node-title">FRONT<br>END</div><div style="margin-top:8px;font-size:9px;color:var(--text-dim);">React / Next.js</div></div><div class="node node-rest" data-id="rest" data-title="REST API Gateway" data-desc="HTTP API layer para clientes internos/externos." data-badge="HTTP / JSON"><span>REST API</span></div><div class="node node-core" data-id="core" data-title="Backend (app/)" data-desc="Paquetes detectados en backend/app/ agrupados por dominio." data-badge="Backend"><div class="node-label">ENGINE</div><div class="node-title">BACKEND</div><div class="core-tags" id="core-tags"></div></div><div class="node node-db-master node-rhombus" data-id="db-master" data-title="POSTGRES MASTER" data-desc="Base principal de escritura (writer)." data-badge="WRITER"><div class="rhombus-content"><div class="node-label">WRITER</div><div class="node-title">POSTGRES<br>MASTER</div></div></div><div class="node node-db-replica node-rhombus" data-id="db-replica" data-title="DB REPLICA" data-desc="Replica read-only para lecturas/analítica." data-badge="READ ONLY"><div class="rhombus-content"><div class="node-label">READ ONLY</div><div class="node-title">DB<br>REPLICA</div></div></div><div class="node node-cdc" data-id="cdc" data-title="Debezium CDC" data-desc="Change Data Capture (WAL) hacia Kafka." data-badge="CDC"><div class="node-label">Debezium CDC</div></div><div class="node node-kafka" data-id="kafka" data-title="Kafka Bus" data-desc="Event streaming para eventos públicos/async." data-badge="Event Bus"><span>:: KAFKA_BUS ::</span></div><div class="node node-rabbit" data-id="rabbitmq" data-title="RabbitMQ" data-desc="Broker para colas de jobs/workers." data-badge="Message Broker"><div class="node-label" style="color:var(--text-muted);">MESSENGER</div><div class="node-title">RABBIT<br>MQ</div></div></div></div><div id="minimap"><canvas id="minimap-canvas"></canvas><div id="minimap-label">Overview</div></div><div id="controls"><button class="ctrl-btn" id="btn-zoom-in" title="Zoom in">+</button><div id="zoom-level">100%</div><button class="ctrl-btn" id="btn-zoom-out" title="Zoom out">−</button><button class="ctrl-btn" id="btn-reset" title="Reset view" style="font-size:11px;">↺</button></div><div id="tooltip"><div class="tt-title" id="tt-title"></div><div class="tt-desc" id="tt-desc"></div><div class="tt-badge" id="tt-badge"></div></div><script>const DATA=__DATA__;const DETECTED=DATA.detected||[];const CORE_TAGS=DATA.core_tags||[];const LABELS={core:"core",core_modules:"core_modules",addons:"addons",api:"api",tasks:"tasks",models:"models",schemas:"schemas",app:"app"};const groupOrder=["core","core_modules","addons","api","tasks","models","schemas","app"];function byGroup(items){const m={};items.forEach(i=>{(m[i.group]||(m[i.group]=[])).push(i)});return m}const grouped=byGroup(DETECTED.filter(x=>x.kind==="package"));const moduleItems=document.getElementById("module-items");const countEl=document.getElementById("detected-count");countEl.textContent=`Módulos Detectados · ${DETECTED.filter(x=>x.kind==="package").length} paquetes`;groupOrder.forEach(g=>{const arr=grouped[g];if(!arr||arr.length===0)return;const h=document.createElement("div");h.className="group-title";h.textContent=`${LABELS[g]} · ${arr.length}`;moduleItems.appendChild(h);arr.forEach(d=>{const el=document.createElement("div");el.className="module-item";el.innerHTML=`<span class="module-dot"></span>${d.name}`;el.addEventListener("click",()=>{document.querySelectorAll(".module-item").forEach(x=>x.classList.remove("active"));el.classList.toggle("active");highlightNode("core")});moduleItems.appendChild(el)})});const coreTagsEl=document.getElementById("core-tags");CORE_TAGS.forEach(t=>{const s=document.createElement("span");s.className="core-tag";s.textContent=t;coreTagsEl.appendChild(s)});const wrap=document.getElementById("canvas-wrap");const canvas=document.getElementById("canvas");let tx=80,ty=60,scale=1;const MIN_SCALE=.2,MAX_SCALE=3;function applyTransform(){canvas.style.transform=`translate(${tx}px,${ty}px) scale(${scale})`;document.getElementById("zoom-level").textContent=Math.round(scale*100)+"%";drawMinimap()}let dragging=false,dragStartX=0,dragStartY=0,dragTX=0,dragTY=0;wrap.addEventListener("mousedown",e=>{if(e.target.closest(".node"))return;dragging=true;dragStartX=e.clientX;dragStartY=e.clientY;dragTX=tx;dragTY=ty;wrap.classList.add("dragging")});window.addEventListener("mousemove",e=>{if(!dragging)return;tx=dragTX+(e.clientX-dragStartX);ty=dragTY+(e.clientY-dragStartY);applyTransform()});window.addEventListener("mouseup",()=>{dragging=false;wrap.classList.remove("dragging")});wrap.addEventListener("wheel",e=>{e.preventDefault();const delta=-e.deltaY;const zoomFactor=delta>0?1.08:.92;const rect=wrap.getBoundingClientRect();const mx=e.clientX-rect.left;const my=e.clientY-rect.top;const wx=(mx-tx)/scale;const wy=(my-ty)/scale;const newScale=Math.min(MAX_SCALE,Math.max(MIN_SCALE,scale*zoomFactor));tx=mx-wx*newScale;ty=my-wy*newScale;scale=newScale;applyTransform()},{passive:false});document.getElementById("btn-zoom-in").addEventListener("click",()=>{scale=Math.min(MAX_SCALE,scale*1.15);applyTransform()});document.getElementById("btn-zoom-out").addEventListener("click",()=>{scale=Math.max(MIN_SCALE,scale*.87);applyTransform()});document.getElementById("btn-reset").addEventListener("click",()=>{tx=80;ty=60;scale=1;applyTransform()});const EDGES=[["frontend","rest","arr","#2d3748"],["rest","core","arr-g","rgba(0,229,153,0.35)"],["core","rabbitmq","arr","#2d3748"],["db-master","cdc","arr-o","rgba(246,173,85,0.4)"],["cdc","kafka","arr-o","rgba(246,173,85,0.4)"],["db-master","db-replica","arr-b","rgba(66,153,225,0.35)"],["core","db-master","arr-g","rgba(0,229,153,0.28)"]];function nodeCenter(id){const el=document.querySelector(`[data-id="${id}"]`);if(!el)return null;return {x:el.offsetLeft+el.offsetWidth/2,y:el.offsetTop+el.offsetHeight/2}}function drawEdges(){const svg=document.getElementById("connections");svg.setAttribute("width","1100");svg.setAttribute("height","800");const defs=svg.querySelector("defs").outerHTML;svg.innerHTML=defs;EDGES.forEach(([fromId,toId,marker,stroke])=>{const a=nodeCenter(fromId),b=nodeCenter(toId);if(!a||!b)return;const path=document.createElementNS("http://www.w3.org/2000/svg","path");const dx=(b.x-a.x)*.5;const c1x=a.x+dx,c1y=a.y,c2x=b.x-dx,c2y=b.y;path.setAttribute("d",`M${a.x},${a.y} C${c1x},${c1y} ${c2x},${c2y} ${b.x},${b.y}`);path.setAttribute("stroke",stroke);path.setAttribute("stroke-width","1.4");path.setAttribute("fill","none");path.setAttribute("marker-end",`url(#${marker})`);path.setAttribute("filter","url(#glow)");svg.appendChild(path)})}function showTooltip(e,node){document.getElementById("tt-title").textContent=node.dataset.title||"";document.getElementById("tt-desc").textContent=node.dataset.desc||"";document.getElementById("tt-badge").textContent=node.dataset.badge||"";const tt=document.getElementById("tooltip");tt.style.left=(e.clientX+12)+"px";tt.style.top=(e.clientY+12)+"px";tt.classList.add("visible")}function hideTooltip(){document.getElementById("tooltip").classList.remove("visible")}document.querySelectorAll(".node").forEach(n=>{n.addEventListener("mousemove",e=>showTooltip(e,n));n.addEventListener("mouseleave",hideTooltip)});let selectedNode=null;function highlightNode(id){document.querySelectorAll(".node").forEach(n=>n.classList.remove("selected"));if(id){const n=document.querySelector(`[data-id="${id}"]`);if(n)n.classList.add("selected")}}document.querySelectorAll(".node").forEach(node=>{node.addEventListener("click",()=>{const id=node.dataset.id;if(selectedNode===id){selectedNode=null;highlightNode(null)}else{selectedNode=id;highlightNode(id)}})});const mmCanvas=document.getElementById("minimap-canvas");const mmCtx=mmCanvas.getContext("2d");function drawMinimap(){const mm=document.getElementById("minimap");mmCanvas.width=mm.offsetWidth*devicePixelRatio;mmCanvas.height=mm.offsetHeight*devicePixelRatio;mmCtx.clearRect(0,0,mmCanvas.width,mmCanvas.height);mmCtx.fillStyle="#0d1117";mmCtx.fillRect(0,0,mmCanvas.width,mmCanvas.height);const CANVAS_W=1100,CANVAS_H=800;const sx=mmCanvas.width/CANVAS_W,sy=mmCanvas.height/CANVAS_H;EDGES.forEach(([fromId,toId,,stroke])=>{const a=nodeCenter(fromId),b=nodeCenter(toId);if(!a||!b)return;mmCtx.beginPath();mmCtx.moveTo(a.x*sx,a.y*sy);mmCtx.lineTo(b.x*sx,b.y*sy);mmCtx.strokeStyle=stroke;mmCtx.lineWidth=devicePixelRatio*.5;mmCtx.stroke()});document.querySelectorAll(".node").forEach(n=>{const l=parseInt(n.offsetLeft||0),t=parseInt(n.offsetTop||0);const w=n.offsetWidth*sx,h=n.offsetHeight*sy;mmCtx.fillStyle=n.dataset.id==="core"?"rgba(0,229,153,0.25)":"rgba(45,55,72,0.8)";mmCtx.fillRect(l*sx,t*sy,w,h)});const vpW=wrap.clientWidth/scale*sx,vpH=wrap.clientHeight/scale*sy;const vpX=(-tx/scale)*sx,vpY=(-ty/scale)*sy;mmCtx.strokeStyle="rgba(255,255,255,0.2)";mmCtx.lineWidth=devicePixelRatio;mmCtx.strokeRect(vpX,vpY,vpW,vpH)}document.getElementById("minimap").addEventListener("click",e=>{const mm=document.getElementById("minimap");const rect=mm.getBoundingClientRect();const CANVAS_W=1100,CANVAS_H=800;const cx=(e.clientX-rect.left)/mm.offsetWidth*CANVAS_W;const cy=(e.clientY-rect.top)/mm.offsetHeight*CANVAS_H;tx=wrap.clientWidth/2-cx*scale;ty=wrap.clientHeight/2-cy*scale;applyTransform()});window.addEventListener("resize",()=>{drawEdges();drawMinimap()});window.addEventListener("load",()=>{drawEdges();applyTransform();document.querySelectorAll(".node").forEach((n,i)=>{n.style.opacity="0";n.style.transition=`opacity .4s ${i*.06}s,border-color .25s,box-shadow .25s,transform .2s`;requestAnimationFrame(()=>{n.style.opacity="1"})})});</script></body></html>"""
    template = template.replace(
        "--accent-o:#f6ad55;--node-bg:rgba(13,17,23,.95)",
        "--accent-o:#f6ad55;--accent-m:#d53f8c;--node-bg:rgba(13,17,23,.95)",
    )
    template = template.replace(
        '<div style="margin-top:4px;">BUILD · SUCCESSFUL</div>',
        '<div style="margin-top:4px;">BUILD · SUCCESSFUL</div><div style="margin-top:4px;">MCP · <span id="mcp-mode" style="color:var(--accent-m)"></span></div>',
    )
    template = template.replace(
        ".node-rabbit .node-title{font-size:14px;color:#fff}",
        ".node-rabbit .node-title{font-size:14px;color:#fff}.node-mcp{left:720px;top:210px;width:170px;padding:14px 16px;border-color:rgba(213,63,140,.35)!important;box-shadow:0 0 30px rgba(213,63,140,.08),inset 0 0 18px rgba(213,63,140,.03)!important}.node-mcp .node-label{color:var(--accent-m)}.node-mcp .node-title{font-size:16px;color:#fff}",
    )
    template = template.replace(
        '</marker><filter id="glow">',
        '</marker><marker id="arr-m" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto"><path d="M0,0 L0,8 L8,4 Z" fill="rgba(213,63,140,0.5)"/></marker><filter id="glow">',
    )
    template = template.replace(
        '</div><div class="node node-db-master',
        '</div><div class="node node-mcp" data-id="mcp" data-title="MCP Server (Opcional)" data-desc="Módulo de inteligencia opcional (Add-on activable). Herramientas DB_R read-only para análisis." data-badge="MCP · OPTIONAL"><div class="node-label">INTEL</div><div class="node-title">MCP<br>SERVER</div><div style="margin-top:6px;font-size:9px;color:var(--text-dim);">modo: <span id="mcp-mode-inline" style="color:var(--accent-m);"></span></div></div><div class="node node-db-master',
    )
    template = template.replace(
        'const CORE_TAGS=DATA.core_tags||[];',
        'const CORE_TAGS=DATA.core_tags||[];const MCP=DATA.mcp||{mode:"Inactive",active:false};(function(){const mode=(MCP.mode||"Inactive");const off=!MCP.active;const label=off?`${mode} (OFF)`:mode;const color=mode==="Inactive"?"#4a5568":"var(--accent-m)";const el=document.getElementById("mcp-mode");const el2=document.getElementById("mcp-mode-inline");[el,el2].forEach(x=>{if(!x)return;x.textContent=label;x.style.color=color})})();',
    )
    template = template.replace(
        '["core","db-master","arr-g","rgba(0,229,153,0.28)"]];',
        '["core","db-master","arr-g","rgba(0,229,153,0.28)"],["core","mcp","arr-m","rgba(213,63,140,0.35)"],["mcp","db-replica","arr-m","rgba(213,63,140,0.25)"]];',
    )
    return template.replace("__DATA__", data_json)


def main() -> int:
    repo_root = Path(__file__).resolve().parents[2]
    app_dir = repo_root / "backend" / "app"
    doc_root = repo_root / ".Documentacion"
    out_dir = doc_root / "docs"
    out_dir.mkdir(parents=True, exist_ok=True)
    out_file = out_dir / "infrastructure.html"

    detected = detect_backend(app_dir)
    out_file.write_text(build_html(detected), encoding="utf-8")

    should_open = os.getenv("ERP3_LIVING_MAP_OPEN", "1").lower() in {"1", "true", "yes"}
    if should_open:
        try:
            webbrowser.open(out_file.as_uri())
        except Exception:
            pass

    return 0


if __name__ == "__main__":
    raise SystemExit(main())
