#!/usr/bin/env python3
"""
parse_process.py — Parser de arquivos .process (XML/XMI) do TOTVS Fluig

Extrai atividades BPMN, gateways, swimlanes, sequenceFlows e gera:
- Tabela de atividades com activityId
- Esqueleto de displayFields.js
- Diagrama Mermaid

Uso:
    python parse_process.py arquivo.process
    python parse_process.py arquivo.process --mermaid
    python parse_process.py arquivo.process --display-fields
    python parse_process.py arquivo.process --all

Autor: Nevoa — Assistente IA especializado em TOTVS Fluig
"""

import xml.etree.ElementTree as ET
import re
import sys
import argparse
import os


# ---------------------------------------------------------------------------
# Namespaces usados no .process
# ---------------------------------------------------------------------------
NS = {
    "xmi": "http://www.omg.org/XMI",
    "bpmn2": "http://www.omg.org/spec/BPMN/20100524/MODEL-XMI",
    "pi": "http://eclipse.org/graphiti/mm/pictograms",
    "al": "http://eclipse.org/graphiti/mm/algorithms",
    "xsi": "http://www.w3.org/2001/XMLSchema-instance",
}


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def extract_activity_id(element_id):
    """Extrai o sufixo numerico do id BPMN (ex: 'task5' -> 5)."""
    match = re.search(r"(\d+)$", element_id or "")
    return int(match.group(1)) if match else None


def decode_xml_entities(text):
    """Decodifica entidades XML comuns em nomes de atividades."""
    if not text:
        return text
    replacements = {
        "&#xA;": " ",
        "&#10;": " ",
        "&#227;": "a",
        "&#231;": "c",
        "&#233;": "e",
        "&#225;": "a",
        "&#243;": "o",
        "&#250;": "u",
        "&#237;": "i",
        "&#245;": "o",
        "&#234;": "e",
        "&#226;": "a",
        "&#244;": "o",
        "&#252;": "u",
        "&#199;": "C",
        "&#195;": "A",
        "&#218;": "U",
        "&#201;": "E",
        "&#211;": "O",
        "&#205;": "I",
    }
    for entity, char in replacements.items():
        text = text.replace(entity, char)
    return text.strip()


def sanitize_name(name):
    """Remove quebras de linha e espacos extras de nomes."""
    if not name:
        return "(sem nome)"
    name = name.replace("\n", " ").replace("\r", " ")
    name = re.sub(r"\s+", " ", name).strip()
    return name if name else "(sem nome)"


# ---------------------------------------------------------------------------
# Classes de dados
# ---------------------------------------------------------------------------
class BpmnElement:
    """Representa um elemento BPMN extraido do .process."""

    def __init__(self, element_type, element_id, name, activity_id, attrs=None):
        self.element_type = element_type  # StartEvent, Task, EndEvent, ExclusiveGW, ParallelGW, ServiceTask
        self.element_id = element_id      # id original (ex: task5)
        self.name = sanitize_name(name)
        self.activity_id = activity_id
        self.attrs = attrs or {}
        self.swimlane = None              # preenchido depois

    def __repr__(self):
        return f"BpmnElement({self.element_type}, id={self.element_id}, act={self.activity_id}, name={self.name})"


class SequenceFlow:
    """Representa um fluxo de sequencia entre dois elementos."""

    def __init__(self, flow_id, source_ref, target_ref, name="", permite_retorno=False):
        self.flow_id = flow_id
        self.source_ref = source_ref
        self.target_ref = target_ref
        self.name = sanitize_name(name) if name else ""
        self.permite_retorno = permite_retorno

    def __repr__(self):
        return f"Flow({self.flow_id}: {self.source_ref} -> {self.target_ref})"


class SwimLane:
    """Representa uma swimlane (raia) do processo."""

    def __init__(self, lane_id, name, y=0, height=0):
        self.lane_id = lane_id
        self.name = sanitize_name(name)
        self.y = y
        self.height = height

    def contains_y(self, element_y):
        """Verifica se a coordenada Y esta dentro desta swimlane."""
        return self.y <= element_y <= (self.y + self.height)


class ProcessInfo:
    """Metadados do processo."""

    def __init__(self, process_id, name, card_index=None, version=None, form_source=None):
        self.process_id = process_id
        self.name = sanitize_name(name)
        self.card_index = card_index
        self.version = version
        self.form_source = form_source


# ---------------------------------------------------------------------------
# Parser principal
# ---------------------------------------------------------------------------
class FluigProcessParser:
    """Parser de arquivos .process do TOTVS Fluig."""

    def __init__(self, filepath):
        self.filepath = filepath
        self.elements = []        # BpmnElement
        self.flows = []           # SequenceFlow
        self.swimlanes = []       # SwimLane
        self.process_info = None  # ProcessInfo
        self._element_map = {}    # id -> BpmnElement
        self._layout_coords = {}  # id -> (x, y)

    def parse(self):
        """Executa o parsing completo do arquivo .process."""
        tree = ET.parse(self.filepath)
        root = tree.getroot()

        self._parse_bpmn_elements(root)
        self._parse_sequence_flows(root)
        self._parse_process_info(root)
        self._parse_layout(root)
        self._assign_swimlanes()

        return self

    # -- Parsing de elementos BPMN ------------------------------------------

    def _parse_bpmn_elements(self, root):
        """Extrai todos os elementos BPMN semanticos."""

        # Mapeamento de tags para tipos
        tag_type_map = {
            "{%s}BpmnStartEvent" % NS["bpmn2"]: "StartEvent",
            "{%s}BpmnTask" % NS["bpmn2"]: "Task",
            "{%s}BpmnServiceTask" % NS["bpmn2"]: "ServiceTask",
            "{%s}BpmnExclusiveGateway" % NS["bpmn2"]: "ExclusiveGW",
            "{%s}BpmnParallelGateway" % NS["bpmn2"]: "ParallelGW",
            "{%s}BpmnEndEvent" % NS["bpmn2"]: "EndEvent",
            "{%s}BpmnSwimLane" % NS["bpmn2"]: "SwimLane",
            "{%s}BpmnPool" % NS["bpmn2"]: "Pool",
        }

        for child in root:
            tag = child.tag
            if tag in tag_type_map:
                element_type = tag_type_map[tag]
                element_id = child.get("id", "")
                name = child.get("name", "")
                activity_id = extract_activity_id(element_id)

                if element_type == "SwimLane":
                    self.swimlanes.append(SwimLane(element_id, name))
                elif element_type == "Pool":
                    # Pool e informativo, nao gera atividade
                    pass
                else:
                    attrs = dict(child.attrib)
                    elem = BpmnElement(element_type, element_id, name, activity_id, attrs)
                    self.elements.append(elem)
                    self._element_map[element_id] = elem

    def _parse_sequence_flows(self, root):
        """Extrai todos os SequenceFlows."""
        for child in root:
            if child.tag == "{%s}SequenceFlow" % NS["bpmn2"]:
                flow_id = child.get("id", "")
                source = child.get("sourceRef", "")
                target = child.get("targetRef", "")
                name = child.get("name", "")
                retorno = child.get("permiteRetorno", "false") == "true"
                self.flows.append(SequenceFlow(flow_id, source, target, name, retorno))

        # Detecta gateways implicitos (referenciados nos flows mas sem elemento bpmn2)
        self._detect_implicit_gateways()

    def _detect_implicit_gateways(self):
        """Detecta gateways que aparecem nos flows mas nao tem elemento bpmn2 explicito."""
        all_refs = set()
        for flow in self.flows:
            all_refs.add(flow.source_ref)
            all_refs.add(flow.target_ref)

        for ref in all_refs:
            if ref not in self._element_map:
                # Detecta pelo prefixo do id
                if "exclusivegateway" in ref.lower():
                    activity_id = extract_activity_id(ref)
                    elem = BpmnElement("ExclusiveGW", ref, "(decisao)", activity_id)
                    self.elements.append(elem)
                    self._element_map[ref] = elem
                elif "parallelgateway" in ref.lower():
                    activity_id = extract_activity_id(ref)
                    elem = BpmnElement("ParallelGW", ref, "(paralelo)", activity_id)
                    self.elements.append(elem)
                    self._element_map[ref] = elem
                elif "inclusivegateway" in ref.lower():
                    activity_id = extract_activity_id(ref)
                    elem = BpmnElement("ExclusiveGW", ref, "(inclusivo)", activity_id)
                    self.elements.append(elem)
                    self._element_map[ref] = elem

    def _parse_process_info(self, root):
        """Extrai metadados do processo (bpmn2:BpmnProcess)."""
        for child in root:
            if child.tag == "{%s}BpmnProcess" % NS["bpmn2"]:
                self.process_info = ProcessInfo(
                    process_id=child.get("id", ""),
                    name=child.get("name", ""),
                    card_index=child.get("cardIndex"),
                    version=child.get("version"),
                    form_source=child.get("formSource"),
                )
                break

    # -- Parsing de layout visual -------------------------------------------

    def _parse_layout(self, root):
        """Extrai coordenadas (x, y) dos elementos a partir do pi:Diagram."""
        diagram = root.find("{%s}Diagram" % NS["pi"])
        if diagram is None:
            return

        self._parse_layout_recursive(diagram)

    def _parse_layout_recursive(self, element):
        """Percorre recursivamente o layout visual para extrair coordenadas."""
        for child in element:
            # Procura por link com businessObjects
            link = child.find("{%s}link" % NS["pi"])
            if link is None:
                # Tambem busca como atributo direto
                for sub in child:
                    if sub.tag.endswith("}link") or sub.tag == "link":
                        link = sub
                        break

            if link is not None:
                biz_obj = link.get("businessObjects", "")
                if biz_obj:
                    # Extrai coordenadas do graphicsAlgorithm
                    ga = child.find("{%s}Rectangle" % NS["al"])
                    if ga is None:
                        ga = child.find("{%s}Ellipse" % NS["al"])
                    if ga is None:
                        # Busca no graphicsAlgorithm direto
                        for sub in child:
                            if sub.tag.endswith("}graphicsAlgorithm") or sub.tag == "graphicsAlgorithm":
                                ga = sub
                                break

                    if ga is not None:
                        x = int(ga.get("x", "0"))
                        y = int(ga.get("y", "0"))
                        self._layout_coords[biz_obj] = (x, y)

                        # Se for swimlane, atualizar coordenadas
                        for lane in self.swimlanes:
                            if lane.lane_id == biz_obj:
                                lane.y = y
                                lane.height = int(ga.get("height", "0"))

                    # Extrai label do elemento visual (MultiText ou Text com value)
                    self._extract_visual_label(child, biz_obj)

            # Recurse nos filhos
            self._parse_layout_recursive(child)

    def _extract_visual_label(self, container, biz_obj):
        """Extrai label de texto do layout visual e atualiza gateways sem nome."""
        if biz_obj not in self._element_map:
            return
        elem = self._element_map[biz_obj]
        # So atualiza se o nome for generico
        if elem.name not in ("(decisao)", "(paralelo)", "(inclusivo)", "(sem nome)"):
            return

        # Busca nos filhos por MultiText ou Text com value
        for sub in container:
            for inner in sub:
                value = inner.get("value", "")
                if value:
                    clean = sanitize_name(decode_xml_entities(value))
                    if clean and clean != "(sem nome)":
                        elem.name = clean
                        return

    def _assign_swimlanes(self):
        """Associa cada elemento a sua swimlane com base nas coordenadas Y."""
        if not self.swimlanes:
            return

        for elem in self.elements:
            coords = self._layout_coords.get(elem.element_id)
            if coords:
                _, ey = coords
                for lane in self.swimlanes:
                    if lane.height > 0 and lane.contains_y(ey):
                        elem.swimlane = lane.name
                        break

    # -- Getters ------------------------------------------------------------

    def get_tasks(self):
        """Retorna apenas as atividades humanas (BpmnTask)."""
        return [e for e in self.elements if e.element_type == "Task"]

    def get_start_events(self):
        """Retorna os StartEvents."""
        return [e for e in self.elements if e.element_type == "StartEvent"]

    def get_end_events(self):
        """Retorna os EndEvents."""
        return [e for e in self.elements if e.element_type == "EndEvent"]

    def get_gateways(self):
        """Retorna os Gateways (exclusive e parallel)."""
        return [e for e in self.elements if e.element_type in ("ExclusiveGW", "ParallelGW")]

    # -- Formatacao de saida ------------------------------------------------

    def format_table(self):
        """Gera tabela formatada de todas as atividades."""
        lines = []
        process_name = self.process_info.name if self.process_info else os.path.basename(self.filepath)
        lines.append(f"=== Processo: {process_name} ===")

        if self.process_info:
            if self.process_info.card_index:
                lines.append(f"    Formulario (cardIndex): {self.process_info.card_index}")
            if self.process_info.version:
                lines.append(f"    Versao: {self.process_info.version}")
            if self.process_info.form_source:
                lines.append(f"    Form Source: {self.process_info.form_source}")

        lines.append("")
        lines.append(f"{'activityId':<12} {'Tipo':<18} {'Nome':<40} {'Swimlane':<20}")
        lines.append("-" * 92)

        # Ordenar: start events primeiro, depois tasks por activityId, gateways, end events
        sorted_elements = sorted(
            self.elements,
            key=lambda e: (
                0 if e.element_type == "StartEvent" else
                1 if e.element_type == "Task" else
                1 if e.element_type == "ServiceTask" else
                2 if e.element_type in ("ExclusiveGW", "ParallelGW") else
                3,
                e.activity_id or 0,
            ),
        )

        for elem in sorted_elements:
            act_id = str(elem.activity_id) if elem.activity_id is not None else "—"
            if elem.element_type == "StartEvent":
                act_id = f"{act_id} (start)"
            elif elem.element_type == "EndEvent":
                act_id = f"{act_id} (end)"

            lane = elem.swimlane or "—"
            name = elem.name[:38] if len(elem.name) > 38 else elem.name
            lines.append(f"{act_id:<12} {elem.element_type:<18} {name:<40} {lane:<20}")

        lines.append("")
        lines.append(f"Total: {len(self.get_tasks())} tasks, "
                      f"{len(self.get_gateways())} gateways, "
                      f"{len(self.get_start_events())} start, "
                      f"{len(self.get_end_events())} end")

        if self.swimlanes:
            lines.append(f"Swimlanes: {', '.join(l.name for l in self.swimlanes)}")

        return "\n".join(lines)

    def format_display_fields(self):
        """Gera esqueleto de displayFields.js."""
        lines = []
        process_name = self.process_info.name if self.process_info else "Processo"

        lines.append(f"// displayFields.js — {process_name}")
        lines.append(f"// Gerado automaticamente pelo parser de .process")
        lines.append(f"// Atividades extraidas: {len(self.get_tasks())}")
        lines.append("")
        lines.append("function displayFields(form, customHTML, logger) {")
        lines.append('    var numState = parseInt(getValue("WKNumState"));')
        lines.append("")

        tasks = sorted(self.get_tasks(), key=lambda t: t.activity_id or 0)

        for i, task in enumerate(tasks):
            act_id = task.activity_id
            lane_info = f" — Swimlane: {task.swimlane}" if task.swimlane else ""
            lines.append(f"    // Atividade {act_id} — {task.name}{lane_info}")
            lines.append(f"    if (numState == {act_id}) {{")
            lines.append(f"        // TODO: configurar campos visiveis/ocultos para '{task.name}'")
            lines.append(f"    }}")

            if i < len(tasks) - 1:
                lines.append("")

        lines.append("}")

        return "\n".join(lines)

    def format_enable_fields(self):
        """Gera esqueleto de enableFields.js."""
        lines = []
        process_name = self.process_info.name if self.process_info else "Processo"

        lines.append(f"// enableFields.js — {process_name}")
        lines.append(f"// Gerado automaticamente pelo parser de .process")
        lines.append("")
        lines.append("function enableFields(form) {")
        lines.append('    var numState = parseInt(getValue("WKNumState"));')
        lines.append("")

        tasks = sorted(self.get_tasks(), key=lambda t: t.activity_id or 0)

        for i, task in enumerate(tasks):
            act_id = task.activity_id
            lines.append(f"    // Atividade {act_id} — {task.name}")
            lines.append(f"    if (numState == {act_id}) {{")
            lines.append(f"        // TODO: habilitar/desabilitar campos para '{task.name}'")
            lines.append(f"    }}")

            if i < len(tasks) - 1:
                lines.append("")

        lines.append("}")

        return "\n".join(lines)

    def format_mermaid(self):
        """Gera diagrama Mermaid do fluxo."""
        lines = []
        lines.append("```mermaid")
        lines.append("graph TD")

        # Mapeia id -> label para Mermaid
        node_labels = {}
        for elem in self.elements:
            safe_id = elem.element_id.replace("-", "_")
            name = elem.name if elem.name != "(sem nome)" else elem.element_id

            if elem.element_type == "StartEvent":
                node_labels[elem.element_id] = f"    {safe_id}(({name}))"
            elif elem.element_type == "EndEvent":
                node_labels[elem.element_id] = f"    {safe_id}(({name}))"
            elif elem.element_type in ("ExclusiveGW", "ParallelGW"):
                gw_name = name if name != "(sem nome)" else "Decisao"
                node_labels[elem.element_id] = f"    {safe_id}{{{gw_name}}}"
            elif elem.element_type == "ServiceTask":
                node_labels[elem.element_id] = f"    {safe_id}[/{name}/]"
            else:
                node_labels[elem.element_id] = f"    {safe_id}[{name}]"

        # Adiciona nodes sem conexoes (standalone)
        declared = set()

        # Adiciona flows
        for flow in self.flows:
            src = flow.source_ref.replace("-", "_")
            tgt = flow.target_ref.replace("-", "_")

            # Declara nodes na primeira vez
            if flow.source_ref not in declared:
                if flow.source_ref in node_labels:
                    lines.append(node_labels[flow.source_ref])
                    declared.add(flow.source_ref)

            if flow.target_ref not in declared:
                if flow.target_ref in node_labels:
                    lines.append(node_labels[flow.target_ref])
                    declared.add(flow.target_ref)

            # Tipo de seta
            if flow.permite_retorno:
                arrow = f"-.->|{flow.name}|" if flow.name else "-.->"
            else:
                arrow = f"-->|{flow.name}|" if flow.name else "-->"

            lines.append(f"    {src} {arrow} {tgt}")

        lines.append("```")

        return "\n".join(lines)

    def format_all(self):
        """Gera saida completa: tabela + mermaid + displayFields + enableFields."""
        sections = []
        sections.append(self.format_table())
        sections.append("\n" + "=" * 60)
        sections.append("DIAGRAMA MERMAID")
        sections.append("=" * 60 + "\n")
        sections.append(self.format_mermaid())
        sections.append("\n" + "=" * 60)
        sections.append("ESQUELETO displayFields.js")
        sections.append("=" * 60 + "\n")
        sections.append(self.format_display_fields())
        sections.append("\n" + "=" * 60)
        sections.append("ESQUELETO enableFields.js")
        sections.append("=" * 60 + "\n")
        sections.append(self.format_enable_fields())
        return "\n".join(sections)


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main():
    parser = argparse.ArgumentParser(
        description="Parser de arquivos .process do TOTVS Fluig",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Exemplos:
  python parse_process.py MeuProcesso.process
  python parse_process.py MeuProcesso.process --mermaid
  python parse_process.py MeuProcesso.process --display-fields
  python parse_process.py MeuProcesso.process --all
  python parse_process.py MeuProcesso.process --all --output resultado.txt
        """,
    )
    parser.add_argument("filepath", help="Caminho para o arquivo .process")
    parser.add_argument("--mermaid", action="store_true", help="Gerar diagrama Mermaid")
    parser.add_argument("--display-fields", action="store_true", help="Gerar esqueleto displayFields.js")
    parser.add_argument("--enable-fields", action="store_true", help="Gerar esqueleto enableFields.js")
    parser.add_argument("--all", action="store_true", help="Gerar tudo (tabela + mermaid + displayFields + enableFields)")
    parser.add_argument("--output", "-o", help="Salvar saida em arquivo")

    args = parser.parse_args()

    if not os.path.isfile(args.filepath):
        print(f"ERRO: Arquivo nao encontrado: {args.filepath}", file=sys.stderr)
        sys.exit(1)

    if not args.filepath.endswith(".process"):
        print(f"AVISO: Arquivo nao possui extensao .process: {args.filepath}", file=sys.stderr)

    try:
        fp = FluigProcessParser(args.filepath)
        fp.parse()
    except ET.ParseError as e:
        print(f"ERRO: Falha ao parsear XML: {e}", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"ERRO: {e}", file=sys.stderr)
        sys.exit(1)

    # Determina saida
    if args.all:
        output = fp.format_all()
    elif args.mermaid:
        output = fp.format_mermaid()
    elif args.display_fields:
        output = fp.format_display_fields()
    elif args.enable_fields:
        output = fp.format_enable_fields()
    else:
        output = fp.format_table()

    # Exibe ou salva
    if args.output:
        with open(args.output, "w", encoding="utf-8") as f:
            f.write(output)
        print(f"Saida salva em: {args.output}")
    else:
        print(output)


if __name__ == "__main__":
    main()
