#!/usr/bin/env python3
"""Deep audit of golden JSON analysis exports."""
import json
import re
from collections import defaultdict
from pathlib import Path

GOLDEN = Path(__file__).resolve().parent.parent / "src/test/resources/golden"
PH = re.compile(r"\$\{")


def fn_form(identifier):
    if not identifier:
        return identifier
    last = identifier.rfind(".")
    if last <= 0:
        return identifier
    prev = identifier.rfind(".", 0, last)
    return identifier[prev + 1 :] if prev > 0 else identifier


def sid(st):
    if isinstance(st, dict):
        return st.get("fullIdentifier") or st.get("rawName")
    return st


def eid(ev):
    if isinstance(ev, dict):
        return ev.get("fullIdentifier") or ev.get("rawName")
    return ev


def audit_file(path, data):
    name = data.get("name", path.stem)
    issues = []
    st_fqn = data.get("stateTypeFqn")
    ev_fqn = data.get("eventTypeFqn")
    transitions = data.get("transitions") or []
    starts = list(data.get("startStates") or [])
    ends = list(data.get("endStates") or [])
    states = data.get("states") or []
    meta = data.get("metadata") or {}

    is_pkg_enum = st_fqn and "." in st_fqn and st_fqn not in ("String", "java.lang.String")
    is_string = st_fqn in (None, "String", "java.lang.String") or (
        not st_fqn and not ev_fqn
    )

    if PH.search(json.dumps(data)):
        issues.append("BUG: unresolved ${placeholder} in export")

    # Duplicate fullIdentifiers in states[]
    by_full = defaultdict(list)
    for s in states:
        by_full[sid(s)].append(s.get("rawName"))
    for fid, raws in by_full.items():
        if fid and len(raws) > 1 and len(set(raws)) > 1:
            issues.append(f"NOISE: duplicate state entry {fid!r} with rawNames {raws}")

    # Collect transition state/event ids
    t_events, t_sources, t_targets = set(), set(), set()
    for t in transitions:
        if eid(t.get("event")):
            t_events.add(eid(t.get("event")))
        for s in t.get("sourceStates") or []:
            t_sources.add(sid(s))
        for s in t.get("targetStates") or []:
            t_targets.add(sid(s))

    graph = t_sources | t_targets

    # startStates canonical consistency
    for ss in starts:
        if is_string and ss and not ss.startswith("String."):
            if any(x.startswith("String.") for x in graph):
                issues.append(f"BUG: startStates {ss!r} bare but transitions use String.* form")
        if ss not in graph and fn_form(ss) not in {fn_form(x) for x in graph}:
            issues.append(f"WARN: startStates {ss!r} not in transition graph")

    # endStates
    for es in ends:
        if es not in graph and fn_form(es) not in {fn_form(x) for x in graph}:
            issues.append(f"WARN: endStates {es!r} not in transition graph")

    # Enum canonical fullIdentifiers
    if is_pkg_enum:
        for i, t in enumerate(transitions):
            for side in ("sourceStates", "targetStates"):
                for s in t.get(side) or []:
                    fid = sid(s)
                    if fid and not fid.startswith(st_fqn + ".") and not fid.startswith('"'):
                        if "." in fid and not fid.startswith("<"):
                            issues.append(f"BUG: transitions[{i}].{side} {fid!r} not under {st_fqn}")
            ev = eid(t.get("event"))
            if ev and ev_fqn and "." in ev_fqn and not ev.startswith("<") and not ev.startswith(ev_fqn + "."):
                if ev not in ("event",) and "." in ev and not ev.startswith(ev_fqn):
                    issues.append(f"BUG: transitions[{i}].event {ev!r} not under {ev_fqn}")
        for i, tr in enumerate(meta.get("triggers") or []):
            for field in ("event", "sourceState"):
                val = tr.get(field)
                if not val or val in ("event", "payload", "customMessage", "eventProvider"):
                    continue
                type_fqn = ev_fqn if field == "event" else st_fqn
                if type_fqn and "." in val and not val.startswith("<") and not val.startswith(type_fqn + "."):
                    if field == "sourceState" and val not in graph and fn_form(val) not in {fn_form(x) for x in graph}:
                        issues.append(f"BUG: triggers[{i}].sourceState {val!r} not a machine state")
                elif field == "event" and type_fqn and val.isupper() is False and "." not in val and not val.startswith("<"):
                    if val not in ("event", "payload", "customMessage", "eventProvider", "true", "false"):
                        issues.append(f"INFO: triggers[{i}].event dynamic {val!r}")

    # matchedTransitions
    for i, chain in enumerate(meta.get("callChains") or []):
        for j, mt in enumerate(chain.get("matchedTransitions") or []):
            me, ms, mtgt = mt.get("event"), mt.get("sourceState"), mt.get("targetState")
            ok = False
            for t in transitions:
                if fn_form(eid(t.get("event"))) != fn_form(me):
                    continue
                srcs = [sid(s) for s in t.get("sourceStates") or []]
                tgts = [sid(s) for s in t.get("targetStates") or []]
                if (not ms or any(fn_form(x) == fn_form(ms) for x in srcs)) and (
                    not mtgt or any(fn_form(x) == fn_form(mtgt) for x in tgts)
                ):
                    ok = True
                    break
            if not ok:
                issues.append(
                    f"BUG: callChains[{i}].matchedTransitions[{j}] ({ms!r},{me!r},{mtgt!r}) no transition"
                )

    # String machine: mixed NEW vs String.NEW
    if is_string or st_fqn == "String":
        all_ids = set(graph) | set(starts) | set(ends) | {sid(s) for s in states}
        for x in list(all_ids):
            if not x:
                continue
            bare = x.strip('"')
            if bare and "." not in bare and f"String.{bare}" in all_ids:
                issues.append(f"BUG: mixed bare {x!r} and String.{bare}")

    # stateTypeFqn missing on enum machine
    if is_pkg_enum and not st_fqn:
        issues.append("BUG: enum machine missing stateTypeFqn")

    return name, issues


def main():
    total = 0
    for path in sorted(GOLDEN.rglob("*.json")):
        data = json.loads(path.read_text())
        machine, issues = audit_file(path, data)
        rel = path.relative_to(GOLDEN)
        bugs = [i for i in issues if i.startswith("BUG")]
        warns = [i for i in issues if i.startswith("WARN")]
        noise = [i for i in issues if i.startswith("NOISE")]
        info = [i for i in issues if i.startswith("INFO")]
        if bugs or warns:
            print(f"\n{'='*70}\n{rel}\n  machine: {machine}")
            for i in bugs + warns + noise + info:
                print(f"  [{i.split(':')[0]}] {i.split(': ', 1)[-1]}")
            total += len(bugs) + len(warns)
    print(f"\nTotal BUG+WARN: {total}")


if __name__ == "__main__":
    main()
