#!/usr/bin/env python3
"""Write the Markdown audit report from the result that audit_crm.py produces.

Usage (from a private working folder; SKILL_DIR is this skill's folder):
  python3 -I "$SKILL_DIR/scripts/audit_crm.py" <inventory.json> ... --md audit-work/report.md
or, from a saved result:
  python3 -I "$SKILL_DIR/scripts/audit_crm.py" <inventory.json> ... --json > audit-work/result.json
  python3 -I "$SKILL_DIR/scripts/write_report.py" audit-work/result.json --out audit-work/report.md

The report follows assets/audit-report-template.md. Evidence comes from the inventory's evidence block,
matched by rule; a FAIL or a gate WARN without evidence is printed as unconfirmed. Text in angle brackets
that starts with "<agent" is left for the agent to finish from what the scripts cannot see, and the report
is not done until none remains.

Reads one local JSON file and writes only --out (or stdout). Exit code 0, or 2 on bad input.
"""
import argparse
import json
import os
import sys

STATUS_ORDER = {"FAIL": 3, "WARN": 2, "PASS": 1}
EVIDENCE_HINT = "unconfirmed; attach one item from references/qa-evidence.md"


def fmt_num(v):
    v = float(v)
    return "{:,.0f}".format(v) if v.is_integer() else "{:,.2f}".format(v)


def count_text(c):
    try:
        n, d = float(c.get("numerator")), float(c.get("denominator"))
    except (TypeError, ValueError, AttributeError):
        return None
    return "%s of %s (%s)" % (fmt_num(n), fmt_num(d), ("%.1f%%" % (100.0 * n / d)) if d else "zero denominator")


def yes_no(v, unknown):
    return {True: "yes", False: "no"}.get(v, unknown)


def evidence_lines(rule, by_rule):
    ev = by_rule.get(rule["id"]) or []
    if ev:
        return ["  - Evidence: " + "; ".join("%s, %s: %s (%s)" % (
            e.get("id") or "EV-?", e.get("kind") or "evidence", e.get("summary") or "no summary", e.get("ref") or "no file")
            for e in ev)]
    if rule["status"] == "FAIL" or rule.get("gate"):
        return ["  - Evidence: " + EVIDENCE_HINT]
    return []


def render(res):
    m, scope = res.get("meta") or {}, res.get("scope") or "full"
    ident = m.get("reading_identity") or {}
    by_rule = {}
    for e in res.get("evidence") or []:
        by_rule.setdefault(e.get("rule"), []).append(e)
    rules = res.get("rules") or []
    L = ["# CRM audit: %s, %s" % (m.get("instance_label") or "<agent: instance label>", m.get("crm") or "<agent: CRM>"), "",
         "## Baseline", "",
         "- CRM and instance: %s, %s, %s" % (m.get("crm") or "<agent: CRM>", m.get("instance_label") or "<agent: placeholder label>",
                                            m.get("environment") or "<agent: production or sandbox>"),
         "- Edition or plan: %s" % (m.get("edition") or "<agent: from the order form or billing page>"),
         "- Read time: %s; as of %s" % (m.get("read_at") or "<agent: read time>", res.get("as_of")),
         "- Routes: %s" % ("; ".join(m.get("routes") or []) or "<agent: admin exports, MCP read tools, CLI read commands>"),
         "- Reading identity: %s; can write: %s; write tools denied in the client: %s" % (
             ident.get("role") or "<agent: role>", yes_no(ident.get("can_write"), "<agent: yes or no>"),
             ", ".join(ident.get("denied_tools") or []) or ("none needed" if ident.get("can_write") is False else "<agent: list them>")),
         "- Scope: %s" % ("gates only, 13 rules plus both inventories" if scope == "gates" else "full, 56 rules"),
         "- Scripts: audit_crm.py on %s" % "; ".join(res.get("inputs") or []),
         "- Summary: %s" % res.get("summary_line"),
         "- martech-context files read: <agent: list them, or none>", "", "## Verdicts", ""]
    for k, lab in (("automation", "Automation readiness"), ("reporting", "Reporting readiness")):
        v = (res.get("verdicts") or {}).get(k) or {}
        L.append("- %s: %s (%s)" % (lab, v.get("verdict"), v.get("detail")))
    if scope != "gates":
        L += ["", "## Scorecard", "", "The score ranks where to look; the verdicts decide readiness.", ""]
        for d in res.get("domains") or []:
            L.append("- %s %s: %s, coverage %.0f%%%s" % (
                d["code"], d["name"], ("%d %s" % (d["score"], d["band"])) if d.get("show") else "insufficient evidence",
                100 * d["coverage"], " (partial)" if d.get("partial") else ""))
        L.append("- Overall: %s" % (res["overall"] if res.get("overall") is not None else "not computed (fewer than six domains have a score)"))
    for st in ("FAIL", "WARN", "PASS"):
        L += ["", "## " + st, ""]
        for rule in rules:
            if rule["status"] != st:
                continue
            tag = ", ".join(x for x in ("gate" if rule.get("gate") else "", "class " + str(rule.get("class_used"))) if x)
            L.append("- %s %s (%s)" % (rule["id"], rule["name"], tag))
            for f in sorted(rule.get("findings") or [], key=lambda x: -STATUS_ORDER.get(x["status"], 0)):
                L.append("  - %s: %s" % (f["status"], f["text"]))
            for extra in ["not checked: " + x for x in rule.get("not_checked") or []] + list(rule.get("notes") or []):
                L.append("  - Note: " + extra)
            if st != "PASS":
                L += evidence_lines(rule, by_rule)
            if st == "WARN":
                L.append("  - Decision needed from: <agent: owner role>")
        if L[-1] == "":
            L.append("- none")
    L += ["", "## NOT CHECKED and N/A", ""]
    for rule in rules:
        if rule["status"] == "NOT CHECKED":
            L.append("- %s NOT CHECKED %s: %s. Evidence that would close it: <agent: the export, capture or count to request>" % (
                rule["id"], rule["name"], "; ".join(rule.get("not_checked") or []) or "no evidence"))
        elif rule["status"] == "N/A":
            L.append("- %s N/A %s: %s" % (rule["id"], rule["name"], rule.get("na")))
    if L[-1] == "":
        L.append("- none")
    L += ["", "## Critical field writers", ""]
    for w in res.get("writer_inventory") or []:
        obs = "; ".join("%s (%s)" % (k, ", ".join(v)) for k, v in w["observed"].items()) or "none observed"
        fill = w.get("fill")
        L.append("- %s.%s (%s): permitted %s; observed %s; precedence %s; flip-flop records %s; fill %s" % (
            w["object"], w["field"], w.get("class") or "class not set", ", ".join(w["permitted"]) or "not registered", obs,
            w["precedence_rule"] or "none written", w["flip_flop_records"] if w["flip_flop_records"] is not None else "no history",
            ("%s, population %s" % (count_text(fill), fill.get("population") or "not named")) if fill and count_text(fill) else "not counted"))
    L += ["", "## Integration inventory", ""]
    for i in res.get("integration_inventory") or []:
        mk = i.get("match_key") or ", ".join("%s on %s" % kv for kv in (i.get("match_keys") or {}).items()) or "not recorded"
        L.append("- %s (%s): identity %s, owner %s, direction %s, objects %s, match key %s, creates %s, writes %s, blank behaviour %s, in stack.md: %s" % (
            i.get("name"), i.get("type") or "type not recorded", i.get("identity_type") or "not recorded", i.get("owner") or "NONE",
            i.get("direction") or "not recorded", ", ".join(i.get("objects") or []) or "not recorded", mk,
            ", ".join(i.get("creates_records") or []) or "nothing", ", ".join(i.get("fields_written") or []) or "nothing",
            i.get("blank_semantics") or "not recorded", yes_no(i.get("in_stack_md"), "not recorded")))
    L += ["", "## Counts and populations", ""]
    for k, v in sorted((res.get("counts") or {}).items()):
        v = v if isinstance(v, dict) else {}
        miss = [x for x in ("population", "query", "read_at") if not str(v.get(x) or "").strip()]
        L.append("- %s: %s%s%s" % (k, count_text(v) or "not a count object",
                                    (", population %s" % v["population"]) if v.get("population") else "",
                                    ("; missing " + ", ".join(miss)) if miss else ""))
    L += ["", "## Unclassified workflow actions", ""]
    L += ["- %r: %s. Open it and add its field writes, deletes and consent actions to the inventory overlay; until then "
          "the automation verdict reads Not assessable." % (u["automation"], ", ".join(u.get("labels") or u["action_types"]))
          for u in res.get("unclassified_actions") or []] or ["- none"]
    L += ["", "## Checked outside the inventory", ""]
    L += ["- %s (%s, %s): %s; %s" % (e.get("id"), e.get("rule"), e.get("kind"), e.get("summary"), e.get("ref"))
          for e in res.get("evidence") or []]
    L.append("- <agent: any further record traces, reconciliations, access views and settings captures, one line each with its "
             "evidence reference, or delete this line>")
    L += ["", "## Overrides", ""]
    L += ["- %s: %s -> %s (%s)" % (o["path"], o["default"], o["value"], o["reason"]) for o in res.get("overrides") or []] or ["- none"]
    L += ["", "## Script warnings", ""] + (["- " + w for w in res.get("warnings") or []] or ["- none"])
    L += ["", "## Proposed martech-context updates", "", "Nothing is written until the user approves.", "",
          "- <agent: stack.md, data-dictionary.md, decisions.md, metrics.md, one line each>",
          "", "## Next step", "", "- Change list: <agent: path>. Nothing in the CRM was changed by this audit.",
          "- Questions for owners: <agent: one line each, with the owner role>", ""]
    return "\n".join(L)


def main():
    p = argparse.ArgumentParser(description="Markdown report from an audit_crm.py --json result.")
    p.add_argument("result")
    p.add_argument("--out", help="write the report here (default: print it)")
    a = p.parse_args()
    try:
        with open(a.result, encoding="utf-8") as f:
            res = json.load(f)
        text = render(res)
    except (OSError, ValueError, KeyError, TypeError, AttributeError) as e:
        print("write_report: cannot use %s (%s: %s); pass the JSON that audit_crm.py --json prints" % (
            a.result, type(e).__name__, e), file=sys.stderr)
        sys.exit(2)
    if a.out:
        os.makedirs(os.path.dirname(os.path.abspath(a.out)), exist_ok=True)
        with open(a.out, "w", encoding="utf-8") as f:
            f.write(text)
    else:
        print(text)


if __name__ == "__main__":
    main()
