#!/usr/bin/env python3
"""Gera documento-mapa offline e Markdown do mesmo modelo. Python 3.9+, stdlib."""
import argparse
import datetime
import html
import json
import math
from pathlib import Path
import re
import sys
import xml.etree.ElementTree as ET
import textwrap

AFIRMACOES = {"observado", "documentado", "declarado", "inferido", "proposto", "decidido", "não observado", "conflito"}
ARQUITETURAS = {"atual", "proposta", "permanece", "entra", "sai", "muda"}
NATUREZAS = {"informacao", "conhecimento", "dependencia", "controle", "retorno"}
ID = re.compile(r"[a-z][a-z0-9-]*\Z")
W, H, NW, NH = 1100, 640, 220, 96

def require(ok, message):
    if not ok:
        raise ValueError(message)

def txt(obj, key):
    value = obj.get(key)
    require(isinstance(value, str) and bool(value.strip()), f"Texto ausente: {key}")
    return value

def seq(obj, key, empty=False):
    value = obj.get(key)
    require(isinstance(value, list) and (empty or bool(value)), f"Lista ausente/vazia: {key}")
    return value

def point(value):
    return (isinstance(value, list) and len(value) == 2
            and all(isinstance(x, (int, float)) and not isinstance(x, bool) and math.isfinite(x) for x in value))


BASE = Path(__file__).resolve().parents[1]
PREPARO = {"disponivel": "Disponível", "preparar": "A preparar", "opcional": "Expansão possível"}

def geometry(view):
    canvas = view.get("canvas", [W, H])
    require(point(canvas) and 600 <= canvas[0] <= 3000 and 400 <= canvas[1] <= 2000, "Canvas inválido.")
    return canvas

def size(view, node_id):
    value = view.get("tamanhos", {}).get(node_id, [NW, NH])
    require(point(value) and 160 <= value[0] <= 520 and 80 <= value[1] <= 360, "Tamanho de nó inválido.")
    return value

def icon_shapes(icon):
    if not icon:
        return ""
    require(isinstance(icon, str) and re.fullmatch(r"(?:logo|v)-[a-z0-9-]+", icon), "Identificador de ícone inválido.")
    if icon.startswith("logo-"):
        catalog = json.loads((BASE / "assets/catalogo-icones.json").read_text())
        record = next((x for x in catalog["logos"] if x["id"] == icon), None)
        require(record is not None, "Logo ausente da biblioteca: " + icon)
        path = (BASE / "assets" / record["arquivo"]).resolve()
        require(path.is_relative_to((BASE / "assets").resolve()), "Logo fora da biblioteca.")
        root = ET.fromstring(path.read_text())
        require(root.attrib.get("viewBox") == "0 0 24 24", "viewBox de logo incompatível.")
        style = 'fill="currentColor" stroke="none"'
    else:
        sprite = ET.fromstring((BASE / "assets/simbolos-arquitetura.svg").read_text())
        root = next((x for x in sprite if x.attrib.get("id") == icon), None)
        require(root is not None, "Símbolo ausente: " + icon)
        style = 'fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"'
    allowed = {"path", "circle", "rect", "ellipse", "line", "polyline", "polygon"}
    attrs = {"d", "cx", "cy", "r", "rx", "ry", "x", "y", "x1", "y1", "x2", "y2", "width", "height", "points", "stroke-dasharray"}
    shapes = []
    for element in root:
        tag = element.tag.split("}")[-1]
        if tag in {"title", "desc"}:
            continue
        require(tag in allowed and not list(element), "Geometria não permitida no ícone.")
        require(all(k in attrs for k in element.attrib), "Atributo não permitido no ícone.")
        for key, value in element.attrib.items():
            pattern = r"[MmLlHhVvCcSsQqTtAaZz0-9.,eE+\-\s]+" if key == "d" else r"[0-9.,eE+\-\s]+"
            require(re.fullmatch(pattern, value) is not None, "Geometria inválida no ícone.")
        properties = " ".join(f'{k}="{html.escape(v, quote=True)}"' for k, v in element.attrib.items())
        shapes.append(f"<{tag} {properties}/>")
    return f"<g {style}>" + "".join(shapes) + "</g>"

def extension_validate(model):
    nodes = {x["id"]: x for x in model["nos"]}
    relations = {x["id"]: x for x in model["relacoes"]}
    for node in nodes.values():
        if node.get("icone"):
            icon_shapes(node["icone"])
        if "preparo" in node:
            require(node["preparo"] in PREPARO, "Preparo inválido.")
            txt(node, "necessidade")
        if "modelo_entrega" in node:
            delivery = node["modelo_entrega"]
            for key in ("para_que", "destinatario", "formato", "destino", "aceitacao", "template"):
                txt(delivery, key)
    ipo = model.get("ipo")
    if ipo:
        require(ipo.get("ciclo") in {"operacao", "construcao"}, "Declare ciclo IPO.")
        classified = []
        for key in ("entradas", "transformacoes", "saidas"):
            ids = seq(ipo, key)
            require(all(x in nodes for x in ids), "IPO aponta para nó ausente.")
            classified += ids
        require(len(classified) == len(set(classified)), "Separe papéis IPO; um nó não deve representar vários papéis neste mapa.")
        for output in ipo["saidas"]:
            require("modelo_entrega" in nodes[output], "Saída IPO sem modelo de entrega.")
        for route in seq(model, "rotas_entrega"):
            require(route.get("entrega") in ipo["saidas"], "Percurso aponta para saída inexistente.")
            ids = seq(route, "relacoes")
            require(all(x in relations for x in ids), "Percurso contém relação inexistente.")
            require(any(relations[r]["destino"] == route["entrega"] for r in ids), "Percurso não chega à entrega.")
            view = next((v for v in model["vistas"] if v["id"] == route.get("vista")), None)
            require(view is not None, "Vista da entrega ausente.")
            require(set(ids) <= {e["relacao"] for e in view["arestas"]}, "Percurso fora da vista.")
        require({r["entrega"] for r in model["rotas_entrega"]} == set(ipo["saidas"]), "Há saída sem percurso explícito.")


def validate(model):
    require(isinstance(model, dict), "A raiz deve ser um objeto.")
    require(model.get("schema_version") == "1", "schema_version deve ser 1.")
    for field in ("titulo", "pergunta", "publico", "escopo", "fora_do_escopo", "versao", "data", "condicao", "conclusao"):
        txt(model, field)
    require(re.fullmatch(r"\d{4}-\d{2}-\d{2}", model["data"]) is not None, "Data deve usar AAAA-MM-DD.")
    datetime.date.fromisoformat(model["data"])
    for item in seq(model, "limites"):
        require(isinstance(item, str) and bool(item.strip()), "Limite deve ser texto.")
    used = set()
    def identity(obj):
        require(isinstance(obj, dict), "Cada objeto deve ser um dicionário.")
        value = txt(obj, "id")
        require(ID.fullmatch(value) is not None and value not in used, f"ID inválido ou duplicado: {value}")
        used.add(value)
    sources = seq(model, "fontes")
    for source in sources:
        identity(source)
        for field in ("titulo", "localizador", "edicao", "alcance"):
            txt(source, field)
    source_ids = {x["id"] for x in sources}
    def claim(obj):
        require(obj.get("arquitetura") in ARQUITETURAS, f"Arquitetura inválida em {obj['id']}.")
        require(obj.get("afirmacao") in AFIRMACOES, f"Afirmação inválida em {obj['id']}.")
        refs = seq(obj, "fontes")
        require(all(isinstance(x, str) and x in source_ids for x in refs), f"Fonte inexistente em {obj['id']}.")
        txt(obj, "evidencia")
    nodes = seq(model, "nos")
    for node in nodes:
        identity(node)
        for field in ("nome", "tipo", "papel", "detalhe"):
            txt(node, field)
        claim(node)
    node_ids = {x["id"] for x in nodes}
    relations = seq(model, "relacoes")
    for rel in relations:
        identity(rel)
        claim(rel)
        for field in ("verbo", "objeto", "meio", "inicio", "autoridade", "registro", "falha", "confirmacao"):
            txt(rel, field)
        require(rel.get("origem") in node_ids and rel.get("destino") in node_ids, f"Extremo inexistente em {rel['id']}.")
        require(rel["origem"] != rel["destino"], f"Separe autorrelação em {rel['id']}.")
        require(rel.get("natureza") in NATUREZAS, f"Natureza inválida em {rel['id']}.")
    by_rel = {x["id"]: x for x in relations}
    views = seq(model, "vistas")
    represented = set()
    for view in views:
        identity(view)
        for field in ("nome", "pergunta", "nota"):
            txt(view, field)
        vw, vh = geometry(view)
        positions = view.get("posicoes")
        require(isinstance(positions, dict) and bool(positions), f"Posições ausentes: {view['id']}.")
        require(len(positions) <= 12, "Divida vistas com mais de 12 protagonistas.")
        for node_id, pos in positions.items():
            require(node_id in node_ids and point(pos), f"Posição inválida: {node_id}.")
            x, y = pos
            nw, nh = size(view, node_id)
            require(24 <= x <= vw - nw - 24 and 40 <= y <= vh - nh - 24, f"Nó fora do canvas: {node_id}.")
        for label in view.get("rotulos", []):
            require(isinstance(label, dict) and point([label.get("x"), label.get("y")]), "Posição de rótulo inválida.")
            require(0 <= label["x"] <= vw and 0 <= label["y"] <= vh, "Rótulo fora do canvas.")
            txt(label, "texto")
        for group in seq(view, "grupos", empty=True):
            identity(group)
            txt(group, "nome")
            members = seq(group, "nos")
            require(all(x in positions for x in members), f"Grupo com nó fora da vista: {group['id']}.")
        edge_ids = set()
        for edge in seq(view, "arestas"):
            rel_id = edge.get("relacao")
            require(isinstance(rel_id, str) and rel_id in by_rel and rel_id not in edge_ids, f"Aresta inválida/duplicada: {rel_id}.")
            edge_ids.add(rel_id)
            represented.add(rel_id)
            rel = by_rel[rel_id]
            require(rel["origem"] in positions and rel["destino"] in positions, f"Extremo fora da vista: {rel_id}.")
            pts = seq(edge, "pontos")
            require(len(pts) >= 2 and all(point(p) and 0 <= p[0] <= vw and 0 <= p[1] <= vh for p in pts), f"Traçado inválido: {rel_id}.")
            require(point(edge.get("rotulo")) and 0 <= edge["rotulo"][0] <= vw and 0 <= edge["rotulo"][1] <= vh, f"Rótulo inválido: {rel_id}.")
    require(represented == set(by_rel), "Há relação sem representação em nenhuma vista.")
    route = model.get("percurso")
    require(isinstance(route, dict), "Percurso ausente.")
    identity(route)
    for field in ("titulo", "inicio", "resultado"):
        txt(route, field)
    by_view = {x["id"]: x for x in views}
    for step in seq(route, "etapas"):
        identity(step)
        for field in ("titulo", "entrada", "transformacao", "saida", "excecao"):
            txt(step, field)
        view = by_view.get(step.get("vista"))
        require(view is not None, f"Vista ausente na etapa {step['id']}.")
        require(step.get("relacao") in {x["relacao"] for x in view["arestas"]}, f"Relação fora da vista na etapa {step['id']}.")
    extension_validate(model)
    return model

def esc(value):
    return html.escape(str(value), quote=True)

def status(obj):
    return f"{obj['arquitetura']} · {obj['afirmacao']}"

def paragraphs(value):
    return "".join(f"<p>{esc(x)}</p>" for x in value.split("\n") if x.strip())

def metadata(obj, model, relation=False):
    fields = [("Condição arquitetural", obj["arquitetura"]), ("Sustentação", obj["afirmacao"]),
              ("Evidência / fundamento", obj["evidencia"])]
    if relation:
        fields += [(name, obj[key]) for name, key in (
            ("Como acontece", "meio"), ("Quem ou o que inicia", "inicio"), ("Autoridade e alcance", "autoridade"),
            ("Onde fica o resultado", "registro"), ("Falha e recuperação", "falha"), ("Como conferir", "confirmacao"))]
    else:
        fields += [("Papel no trabalho", obj["papel"]), ("Como contribui", obj["detalhe"])]
    if not relation:
        if "preparo" in obj:
            fields += [("O que preparar", PREPARO[obj["preparo"]] + ": " + obj["necessidade"])]
        for key, label in (("canal", "Canal / conector / pasta"), ("operacao", "Operação necessária")):
            if obj.get(key): fields.append((label, obj[key]))
        if obj.get("modelo_entrega"):
            fields += [(label, obj["modelo_entrega"][key]) for key, label in (("para_que", "Resultado gerencial"), ("destinatario", "Quem recebe"), ("formato", "Formato"), ("destino", "Destino"), ("aceitacao", "Como reconhecer a entrega"), ("template", "Modelo de entrega"))]
    sources = {x["id"]: x for x in model["fontes"]}
    fields += [("Fonte " + ref, sources[ref]["titulo"] + " · " + sources[ref]["edicao"] + " · " + sources[ref]["localizador"] + " · " + sources[ref]["alcance"]) for ref in obj["fontes"]]
    return "<dl>" + "".join(f"<dt>{esc(k)}</dt><dd>{paragraphs(v)}</dd>" for k, v in fields) + "</dl>"

def make_content(model):
    nodes = {x["id"]: x for x in model["nos"]}
    relations = {x["id"]: x for x in model["relacoes"]}
    parts = [f'<header><p class="eyebrow">Couto-IAS · documento-mapa · {esc(model["versao"])} · {esc(model["data"])}</p><h1>{esc(model["titulo"])}</h1>',
             f'<p class="question">{esc(model["pergunta"])}</p><p class="condition">{esc(model["condicao"])}</p><p>{esc(model["conclusao"])}</p></header>',
             '<a class="skip" href="#leitura">Ir à leitura textual completa</a>',
             '<nav class="view-controls js-only" aria-label="Escolher vista">']
    for i, view in enumerate(model["vistas"]):
        parts.append(f'<button type="button" data-view="{esc(view["id"])}" aria-pressed="{str(i == 0).lower()}">{esc(view["nome"])}</button>')
    parts += ['<button type="button" id="clear">Limpar destaque</button></nav>',
              '<p class="legend glossary">Linha com seta: relação direcional · tracejado: proposta · azul com contorno: seleção. O estado escrito acompanha cada afirmação.</p>',
              '<p id="notice" role="status" aria-live="polite"></p>']
    for view in model["vistas"]:
        vid = esc(view["id"])
        vw, vh = geometry(view)
        parts.append(f'<section class="view" id="{vid}" aria-labelledby="title-{vid}"><h2 id="title-{vid}">{esc(view["nome"])}</h2><p>{esc(view["pergunta"])}</p><p class="note">{esc(view["nota"])}</p><figure><div class="map-scroll" role="region" tabindex="0" aria-label="Mapa {esc(view["nome"])}; pode rolar horizontalmente"><div class="map" style="width:{vw}px;height:{vh}px">')
        parts.append(f'<svg style="width:{vw}px;height:{vh}px" viewBox="0 0 {vw} {vh}" role="img" aria-labelledby="svg-title-{vid} svg-desc-{vid}"><title id="svg-title-{vid}">{esc(view["nome"])}</title><desc id="svg-desc-{vid}">Relações com direção e rótulo. A lista desta vista e a leitura textual abaixo oferecem conteúdo equivalente.</desc><defs><marker id="arrow-{vid}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z"/></marker></defs>')
        for group in view["grupos"]:
            pts = [view["posicoes"][x] for x in group["nos"]]
            x, y = min(p[0] for p in pts) - 16, min(p[1] for p in pts) - 40
            w = max(view["posicoes"][n][0] + size(view,n)[0] for n in group["nos"]) + 16 - x
            h = max(view["posicoes"][n][1] + size(view,n)[1] for n in group["nos"]) + 16 - y
            parts.append(f'<g class="boundary"><rect x="{x}" y="{y}" width="{w}" height="{h}" rx="10"/><text x="{x+12}" y="{y+24}">{esc(group["nome"])}</text></g>')
        for edge in view["arestas"]:
            rel = relations[edge["relacao"]]
            pts = " ".join(f'{p[0]},{p[1]}' for p in edge["pontos"])
            proposed = rel["arquitetura"] == "proposta" or rel["afirmacao"] == "proposto"
            cls = "edge proposed" if proposed else "edge"
            x, y = edge["rotulo"]
            parts.append(f'<g class="{cls}" data-edge="{esc(rel["id"])}"><polyline points="{pts}" marker-end="url(#arrow-{vid})"/><text x="{x}" y="{y}" text-anchor="middle">{esc(rel["verbo"] + " " + rel["objeto"])}</text></g>')
        for label in view.get("rotulos", []):
            parts.append(f'<text class="layer-label" x="{label["x"]}" y="{label["y"]}">{esc(label["texto"])}</text>')
        parts.append("</svg>")
        for node_id, (x, y) in view["posicoes"].items():
            node = nodes[node_id]
            nw, nh = size(view,node_id)
            icon = f'<svg class="brand" viewBox="0 0 24 24" aria-hidden="true">{icon_shapes(node.get("icone"))}</svg>' if node.get("icone") else ""
            preparation = f'<span class="preparation">{esc(PREPARO[node["preparo"]])}</span>' if "preparo" in node else ""
            cls = "node proposed" if node["arquitetura"] == "proposta" or node["afirmacao"] == "proposto" else "node"
            parts.append(f'<button type="button" class="{cls}" style="left:{x}px;top:{y}px;width:{nw}px;height:{nh}px" data-node="{esc(node_id)}" aria-pressed="false">{icon}<span class="type">{esc(node["tipo"])}</span><strong>{esc(node["nome"])}</strong><span class="operational">{esc(node.get("linha", ""))}</span><span class="node-state">{esc(status(node))}</span>{preparation}</button>')
        parts.append(f'</div></div><figcaption>{esc(view["pergunta"])} A lista abaixo e a leitura completa conservam o sentido do mapa sem interação.</figcaption></figure><div class="view-text" data-diagram-description><h3>Relações desta vista</h3><ul>')
        for edge in view["arestas"]:
            rel = relations[edge["relacao"]]
            parts.append(f'<li><a href="#obj-{esc(rel["origem"])}" data-object="{esc(rel["origem"])}">{esc(nodes[rel["origem"]]["nome"])}</a> → <a href="#rel-{esc(rel["id"])}" data-relation="{esc(rel["id"])}">{esc(rel["verbo"] + " " + rel["objeto"])}</a> → <a href="#obj-{esc(rel["destino"])}" data-object="{esc(rel["destino"])}">{esc(nodes[rel["destino"]]["nome"])}</a> <span class="small">({esc(status(rel))})</span></li>')
        parts.append("</ul></div></section>")
    route = model["percurso"]
    parts.append(f'<section id="percurso"><h2>{esc(route["titulo"])}</h2><p><b>Início:</b> {esc(route["inicio"])} <b>Resultado:</b> {esc(route["resultado"])}</p><p class="note">Selecione uma etapa para ir à vista, destacar sua passagem e abrir a explicação. Este percurso é explicativo; não executa o trabalho.</p><ol class="steps">')
    for i, step in enumerate(route["etapas"]):
        parts.append(f'<li><a href="#step-{esc(step["id"])}" data-step="{esc(step["id"])}"><span>{i+1:02}</span>{esc(step["titulo"])}</a></li>')
    parts.append('</ol></section><section id="leitura"><h2>Leitura textual completa</h2>')
    parts.append(f'<p><b>Público:</b> {esc(model["publico"])}</p><p><b>Escopo:</b> {esc(model["escopo"])}</p><p><b>Fora do escopo:</b> {esc(model["fora_do_escopo"])}</p><h3>Componentes e contribuições</h3>')
    for node in model["nos"]:
        parts.append(f'<article id="obj-{esc(node["id"])}"><h4>{esc(node["nome"])} <span class="small">· {esc(node["tipo"])} · {esc(node["id"])}</span></h4>{metadata(node, model)}</article>')
    parts.append('<h3>Relações e condições de operação</h3>')
    for rel in model["relacoes"]:
        phrase = nodes[rel["origem"]]["nome"] + " → " + rel["verbo"] + " " + rel["objeto"] + " → " + nodes[rel["destino"]]["nome"]
        parts.append(f'<article id="rel-{esc(rel["id"])}"><h4>{esc(phrase)}</h4><p class="small">{esc(rel["id"])} · {esc(rel["natureza"])}</p>{metadata(rel, model, True)}</article>')
    parts.append('<h3>Percurso, entradas, saídas e exceções</h3>')
    for i, step in enumerate(route["etapas"]):
        parts.append(f'<article id="step-{esc(step["id"])}"><h4>{i+1}. {esc(step["titulo"])}</h4><p>Vista: {esc(step["vista"])} · Relação: {esc(step["relacao"])}</p><dl>')
        for title, key in (("Entrada", "entrada"), ("Transformação", "transformacao"), ("Saída", "saida"), ("Exceção e retorno", "excecao")):
            parts.append(f'<dt>{title}</dt><dd>{paragraphs(step[key])}</dd>')
        parts.append('</dl></article>')
    parts.append('<h3>Fontes</h3><ul>')
    for source in model["fontes"]:
        parts.append(f'<li><b>{esc(source["id"])} · {esc(source["titulo"])}</b> · {esc(source["edicao"])}<br>{esc(source["localizador"])}<br>{esc(source["alcance"])}</li>')
    parts.append('</ul><h3>Limites e condições a provar</h3><ul>')
    parts.extend(f"<li>{esc(limit)}</li>" for limit in model["limites"])
    parts.append('</ul></section><footer>Documento derivado de modelo versionado. A aparência e a interação não comprovam instalação, operação ou compreensão humana.</footer>')
    return "\n".join(parts)

def make_markdown(model):
    nodes = {x["id"]: x for x in model["nos"]}
    lines = [f'# {model["titulo"]}', "", model["pergunta"], "", f'{model["condicao"]} · {model["versao"]} · {model["data"]}', "", model["conclusao"], ""]
    for key in ("publico", "escopo", "fora_do_escopo"):
        lines += [f'{key}: {model[key]}', ""]
    for view in model["vistas"]:
        lines += ["## " + view["nome"], "", view["pergunta"], "", view["nota"], ""]
        for group in view["grupos"]:
            lines += [f'Fronteira {group["nome"]}: ' + ", ".join(nodes[n]["nome"] for n in group["nos"]), ""]
        rels = {r["id"]: r for r in model["relacoes"]}
        for edge in view["arestas"]:
            r = rels[edge["relacao"]]
            lines += [f'- {nodes[r["origem"]]["nome"]} → {r["verbo"]} {r["objeto"]} → {nodes[r["destino"]]["nome"]} ({status(r)}; {r["id"]}).']
        lines.append("")
    lines += ["## Componentes", ""]
    for node in model["nos"]:
        lines += [f'### {node["nome"]} · {node["id"]}', ""]
        for key in ("tipo", "papel", "detalhe", "arquitetura", "afirmacao", "evidencia"):
            lines += [f'{key}: {node[key]}', ""]
        if "preparo" in node:
            lines += ["preparo: " + PREPARO[node["preparo"]], "", "necessidade: " + node["necessidade"], ""]
        for key in ("canal", "operacao"):
            if node.get(key): lines += [key + ": " + node[key], ""]
        if node.get("modelo_entrega"):
            for key, value in node["modelo_entrega"].items(): lines += [key + ": " + value, ""]
        lines += ["fontes: " + ", ".join(node["fontes"]), ""]
    lines += ["## Relações e condições de operação", ""]
    for r in model["relacoes"]:
        lines += [f'### {r["id"]}: {nodes[r["origem"]]["nome"]} → {r["verbo"]} {r["objeto"]} → {nodes[r["destino"]]["nome"]}', ""]
        for key in ("natureza", "arquitetura", "afirmacao", "evidencia", "meio", "inicio", "autoridade", "registro", "falha", "confirmacao"):
            lines += [f'{key}: {r[key]}', ""]
        lines += ["fontes: " + ", ".join(r["fontes"]), ""]
    route = model["percurso"]
    lines += ["## " + route["titulo"], "", "Início: " + route["inicio"], "", "Resultado: " + route["resultado"], ""]
    for i, step in enumerate(route["etapas"]):
        lines += [f'### {i+1}. {step["titulo"]}', ""]
        for key in ("vista", "relacao", "entrada", "transformacao", "saida", "excecao"):
            lines += [f'{key}: {step[key]}', ""]
    lines += ["## Fontes", ""]
    for source in model["fontes"]:
        lines += ["### " + source["id"] + " · " + source["titulo"], "", source["localizador"], "", "Edição: " + source["edicao"], "", "Alcance: " + source["alcance"], ""]
    lines += ["## Limites e condições a provar", ""]
    lines.extend("- " + x for x in model["limites"])
    return "\n".join(lines) + "\n"

def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("modelo", type=Path)
    parser.add_argument("html", type=Path)
    parser.add_argument("--substituir", action="store_true", help="Permite substituir apenas os derivados.")
    args = parser.parse_args()
    try:
        model = validate(json.loads(args.modelo.read_text(encoding="utf-8")))
        template = Path(__file__).resolve().parents[1] / "assets" / "documento-mapa.html"
        output, markdown = args.html, args.html.with_suffix(".md")
        require(output.suffix.lower() == ".html", "A saída deve terminar em .html.")
        require(len({args.modelo.resolve(), output.resolve(), markdown.resolve(), template.resolve()}) == 4, "Entrada, template e saídas precisam ser arquivos distintos.")
        require(output.parent.is_dir(), "A pasta de destino precisa existir.")
        require(args.substituir or not (output.exists() or markdown.exists()), "Derivado já existe; escolha nova edição ou --substituir.")
        raw = template.read_text(encoding="utf-8")
        require(raw.count("@@CONTEUDO@@") == raw.count("@@DADOS@@") == 1, "Template inválido.")
        data = json.dumps(model, ensure_ascii=False).replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")
        replacements = {"@@CONTEUDO@@": make_content(model), "@@DADOS@@": data}
        rendered = re.sub(r"@@(?:CONTEUDO|DADOS)@@", lambda match: replacements[match.group(0)], raw)
        output.write_text(rendered, encoding="utf-8")
        markdown.write_text(make_markdown(model), encoding="utf-8")
        print(json.dumps({"html": str(output), "markdown": str(markdown), "nos": len(model["nos"]), "relacoes": len(model["relacoes"]), "vistas": len(model["vistas"]), "etapas": len(model["percurso"]["etapas"])}, ensure_ascii=False))
    except (ValueError, KeyError, TypeError, OSError, ET.ParseError) as exc:
        print(f"Erro: {exc}", file=sys.stderr)
        return 2
    return 0

if __name__ == "__main__":
    sys.exit(main())

