#!/usr/bin/env python3
"""Produz Design do Agente offline e equivalente textual. Python 3.9+, stdlib."""
from pathlib import Path
import argparse
import html
import importlib.util
import json
import re
import sys

sys.dont_write_bytecode = True
BASE = Path(__file__).resolve().parents[1]
ID = re.compile(r"[a-z][a-z0-9-]*\Z")
ICONS = {"workflow":"v-acao","blocks":"v-servico","folder-tree":"v-documento","folder":"v-documento","plug":"v-servico","users":"v-pessoa","history":"v-dados","inbox":"v-documento","package-check":"v-resultado","book-open":"v-documento"}
AREAS = {"inputs":"Entradas","agent":"Seu agente","outputs":"Entregas"}

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

def text(obj, key):
    require(isinstance(obj, dict), "Objeto inválido.")
    value = obj.get(key)
    require(isinstance(value, str) and bool(value.strip()), "Texto ausente: " + key)
    return value

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

def identity(obj):
    value = text(obj, "id")
    require(ID.fullmatch(value) is not None, "ID inválido: " + value)
    return value

def validate(model):
    require(isinstance(model, dict) and model.get("schema_version") == "design-do-agente-1", "Esquema esperado: design-do-agente-1.")
    for field in ("titulo","versao","data","condicao"):
        text(model, field)
    cases = array(model, "casos")
    case_ids = set()
    for case in cases:
        cid = identity(case)
        require(cid not in case_ids, "Agente repetido: " + cid)
        case_ids.add(cid)
        for field in ("nome","contribuicao","entrada","trabalho","saida","condicao"):
            text(case, field)
        sources = array(case, "fontes")
        source_ids = set()
        for source in sources:
            sid = identity(source)
            require(sid not in source_ids, "Fonte repetida: " + sid)
            source_ids.add(sid)
            for field in ("titulo","localizador","edicao","alcance"):
                text(source, field)
        def claims(item):
            if "condicao" in item:
                text(item,"condicao")
            if "fontes" in item:
                require(all(isinstance(ref,str) and ref in source_ids for ref in array(item,"fontes")), "Fonte desconhecida no componente.")
        def check_item(item):
            identity(item)
            for field in ("nome","resumo"):
                text(item,field)
            claims(item)
            for row in array(item,"campos"):
                require(isinstance(row,list) and len(row)==2 and all(isinstance(v,str) and bool(v.strip()) for v in row), "Campo deve conter rótulo e conteúdo.")
            for link in array(item,"links",empty=True):
                text(link,"rotulo")
                require(link.get("area") in AREAS, "Área de navegação inválida.")
                for field in ("grupo","item"):
                    require(isinstance(link.get(field),str) and ID.fullmatch(link[field]) is not None, "Destino inválido.")
        lookup = set()
        items = []
        group_ids = set()
        for group in array(case,"groups"):
            gid = identity(group)
            require(gid not in group_ids, "Grupo repetido: " + gid)
            group_ids.add(gid)
            for field in ("nome","resumo"):
                text(group,field)
            require(group.get("icone") in ICONS, "Ícone de grupo fora da biblioteca do mapa.")
            ids = set()
            for item in array(group,"itens"):
                check_item(item)
                require(item["id"] not in ids, "Componente repetido no grupo.")
                ids.add(item["id"])
                lookup.add(("agent",gid,item["id"]))
                items.append(item)
        for area,gid in (("inputs","fontes"),("outputs","entregas")):
            ids = set()
            for item in array(case,area):
                check_item(item)
                require(item["id"] not in ids, "Componente repetido na área.")
                ids.add(item["id"])
                lookup.add((area,gid,item["id"]))
                items.append(item)
        for item in items:
            for link in item["links"]:
                require((link["area"],link["grupo"],link["item"]) in lookup, "Vínculo aponta para recurso ausente: " + link["rotulo"])
    return model

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

def link_description(case, link):
    if link["area"] == "agent":
        group = next(group for group in case["groups"] if group["id"] == link["grupo"])
        group_name, items = group["nome"], group["itens"]
    else:
        group_name, items = AREAS[link["area"]], case[link["area"]]
    target = next(item for item in items if item["id"] == link["item"])
    reference = "/".join((link["area"], link["grupo"], link["item"]))
    return f'Consulte {target["nome"]} em {group_name}. No mapa interativo, use “{link["rotulo"]}” (referência interna: {reference}).'

def fields_html(item, case):
    fields = "".join(f"<dt>{esc(k)}</dt><dd>{esc(v)}</dd>" for k,v in item["campos"])
    metadata = ""
    if item.get("condicao"):
        metadata += f'<p class="cm-meta">Condição: {esc(item["condicao"])}</p>'
    if item.get("fontes"):
        metadata += '<p class="cm-meta">Fontes: ' + esc(", ".join(item["fontes"])) + "</p>"
    links = ""
    if item["links"]:
        links = "".join("<p>" + esc(link_description(case, link)) + "</p>" for link in item["links"])
    return f"<dl>{fields}</dl>{metadata}{links}"

def static_reading(model):
    out = ['<details class="cm-reading" id="cases-reading" open><summary class="cursor-interaction">Leitura completa e fontes</summary><div class="cm-reading-body">', f'<p>{esc(model["condicao"])}</p>', f'<p class="cm-meta">Versão {esc(model["versao"])} · {esc(model["data"])}</p>']
    for case in model["casos"]:
        out += [f'<section><h3>{esc(case["nome"])}</h3><p>{esc(case["contribuicao"])}</p><p class="cm-meta">{esc(case["condicao"])}</p>']
        out.append(f'<dl><dt>Entradas</dt><dd>{esc(case["entrada"])}</dd><dt>Trabalho</dt><dd>{esc(case["trabalho"])}</dd><dt>Entregas</dt><dd>{esc(case["saida"])}</dd></dl>')
        collections = [("Entradas",case["entrada"],case["inputs"])] + [(x["nome"],x["resumo"],x["itens"]) for x in case["groups"]] + [("Entregas",case["saida"],case["outputs"])]
        for name,summary,items in collections:
            out.append(f"<h4>{esc(name)}</h4><p>{esc(summary)}</p>")
            for item in items:
                out.append(f'<article><h5>{esc(item["nome"])}</h5><p>{esc(item["resumo"])}</p>{fields_html(item, case)}</article>')
        out.append("<h4>Fontes desta descrição</h4>")
        for source in case["fontes"]:
            out.append(f'<p><b>{esc(source["id"])} · {esc(source["titulo"])}</b><br>{esc(source["localizador"])}<br>{esc(source["edicao"])} · {esc(source["alcance"])}</p>')
        out.append("</section>")
    out.append("</div></details>")
    return "\n".join(out)

def markdown(model):
    out = ["# " + model["titulo"],"",model["condicao"],"",f'{model["versao"]} · {model["data"]}',""]
    for case in model["casos"]:
        out += ["## " + case["nome"],"",case["contribuicao"],"",case["condicao"],"", "Entradas: " + case["entrada"], "", "Trabalho: " + case["trabalho"], "", "Entregas: " + case["saida"], ""]
        for title,summary,items in [("Entradas",case["entrada"],case["inputs"])] + [(g["nome"],g["resumo"],g["itens"]) for g in case["groups"]] + [("Entregas",case["saida"],case["outputs"])]:
            out += ["### " + title,"",summary,""]
            for item in items:
                out += ["#### " + item["nome"],"",item["resumo"],""]
                for label,value in item["campos"]:
                    out += [label + ": " + value,""]
                if item.get("condicao"):
                    out += ["Condição: " + item["condicao"],""]
                if item.get("fontes"):
                    out += ["Fontes: " + ", ".join(item["fontes"]),""]
                for link in item["links"]:
                    out += [link_description(case, link),""]
        out += ["### Fontes",""]
        for source in case["fontes"]:
            out += [source["id"] + " · " + source["titulo"],"",source["localizador"],"",source["edicao"] + " · " + source["alcance"],""]
    return "\n".join(out)

def symbol_sprite():
    spec = importlib.util.spec_from_file_location("arquitetura_base", BASE/"scripts/gerar_mapa.py")
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    symbols = []
    for symbol in sorted(set(ICONS.values())):
        symbols.append(f'<symbol id="mn-{symbol}" viewBox="0 0 24 24">{module.icon_shapes(symbol)}</symbol>')
    return '<svg aria-hidden="true" style="display:none"><defs>' + "".join(symbols) + "</defs></svg>"

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")
    args = parser.parse_args()
    try:
        model = validate(json.loads(args.modelo.read_text(encoding="utf-8")))
        template = BASE/"assets/design-do-agente.html"
        output, md = args.html, args.html.with_suffix(".md")
        require(output.suffix.lower()==".html", "Saída deve terminar em .html.")
        require(len({args.modelo.resolve(),output.resolve(),md.resolve(),template.resolve()})==4,"Entrada, template e derivados precisam ser distintos.")
        require(output.parent.is_dir(),"Pasta de destino precisa existir.")
        require(args.substituir or not (output.exists() or md.exists()),"Derivado existente; escolha nova edição ou --substituir.")
        first = model["casos"][0]
        replacements = {
            "@@TITULO@@":esc(model["titulo"]),
            "@@NOME@@":esc(first["nome"]),
            "@@CONTRIBUICAO@@":esc(first["contribuicao"]),
            "@@ENTRADA@@":esc(first["entrada"]),
            "@@TRABALHO@@":esc(first["trabalho"]),
            "@@SAIDA@@":esc(first["saida"]),
            "@@CONDICAO@@":esc(first["condicao"]),
            "@@CASOS@@":'<div class="cm-case-controls" aria-label="Escolher agente">'+"".join(f'<button class="cursor-interaction" type="button" data-case="{esc(c["id"])}" aria-pressed="{str(i==0).lower()}">{esc(c["nome"])}</button>' for i,c in enumerate(model["casos"]))+"</div>" if len(model["casos"])>1 else "",
            "@@SIMBOLOS@@":symbol_sprite(),
            "@@LEITURA@@":static_reading(model),
            "@@DADOS@@":json.dumps(model,ensure_ascii=False).replace("<","\\u003c").replace(">","\\u003e").replace("&","\\u0026"),
        }
        raw = template.read_text(encoding="utf-8")
        require(set(re.findall(r"@@[A-Z]+@@",raw))==set(replacements),"Contrato de template incompatível.")
        rendered = re.sub(r"@@[A-Z]+@@",lambda match:replacements[match.group(0)],raw)
        output.write_text(rendered,encoding="utf-8")
        md.write_text(markdown(model),encoding="utf-8")
        print(json.dumps({"html":str(output),"markdown":str(md),"agentes":len(model["casos"]),"entrega":"Design do Agente"},ensure_ascii=False))
    except (ValueError,TypeError,KeyError,OSError) as exc:
        print("Erro: "+str(exc),file=sys.stderr)
        return 2
    return 0

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

