"""RUN-062 - BCODR: Bidirectional Circular Overlap Decomposition and Recomposition (methodology).

BCODR v0.1 (2026-09-21), one of the 42 canonical documents, is a methodology: two families of circular kernels, one
growing outward from inner seeds and one contracting inward from outer constraints, are overlapped; overlap is kept as
information (multi-membership, not Voronoi); the circles' arrangement cuts the domain into atomic cells of constant
membership; cells are separated by geometry, topology, information and certificate role, recomposed under provenance-
preserving merge rules, quotiented into a graph and pruned to a surviving spine. It defines a symmetric-difference
measure D_mu and a Jaccard-type D_J for minimum distinguishability, multiplicity and balance fields, a default
information field, certificate weights, the CONFLICT CELL, a kernel-deformation gate, its relation to UESFCM and LESR,
a root-6217 parameter-space map as the next step, and an honest boundary (not a convergence theorem). Here its
mathematical statements are instantiated - the two differences on discs (exact lens areas), the arrangement's cells
(Euler's formula against a direct count) - and its links to UESFCM, the certificate grades and the Lebesgue line are
set against what this tree found.

Checks
  A0 anchors      the statements below occur in the document as written
  W1 differences  sec. 10-11: D_mu and D_J on discs - the triangle inequality, D_J in [0, 1], D_J scale-free and D_mu not
  W2 arrangement  sec. 18-21, 59, 79 B: the cells of N circles - Euler's count against a direct count of constant-membership
                  regions; at most N(N-1) + 2, exactly that when every pair crosses
  W3 links        sec. 16, 45: the certificate weights against the handoff's grades; the cycle against UESFCM's loop
  W4 lebesgue     sec. 47-50, 84: the root-6217 map's inputs against RUN-058 (the 195 roots), RUN-048/059 (no lower
                  branch) and RUN-060 (Paper 09C not archived)
  R1 registry     BCODR's judgments reviewed, first-used statuses defined
  P1 package      the VALIDATION record against the bytes, the archive and the text

Usage: python code/run062_bcodr.py [--drill] [--blind CID]
"""
import argparse
import copy
import hashlib
import inspect
import json
import math
import re

import numpy as np

import gate
import registry
from corpus import SOURCES, TREE, source_bytes, walk_sources

FOLDER = "main.zip!/BCODR/"
DOC = FOLDER + "BCODR_v0.1_Package.zip!/BCODR_Bidirectional_Circular_Overlap_Decomposition_and_Recomposition_v0.1.md"
VALIDATION = FOLDER + "BCODR_v0.1_Package.zip!/BCODR_v0.1_VALIDATION.json"
UESFCM = "main.zip!/UESFCM/UESFCM_v0.1_Package.zip!/UESFCM_Unbounded_Expansion_Self_Referential_Finite_Closure_Methodology_v0.1.md"
HANDOFF = "main.zip!/LESR_Skew_Field_Variational/LESR_Lebesgue_Midpoint_Handoff_Certificate_vs_CounterCertificate_v0.1.md"
LOG = TREE / "data" / "gate-logs" / "run062-bcodr.json"
LOGS = {n: TREE / "data" / "gate-logs" / f"{n}.json" for n in ("run048-uesfcm-cycles-12-14", "run058-lesr-paper08", "run059-lesr-paper09a",
                                                                  "run060-lesr-paper09b")}
MANIFEST = TREE / "data" / "SOURCE_MANIFEST.v1.json"
CACHE = TREE / "data" / "cache" / "run062-searches.json"     # data/cache is not committed


def load_inputs():
    blobs = {n: source_bytes(n) for n in SOURCES}
    want = (DOC, VALIDATION, UESFCM, HANDOFF)
    got = {leaf.id: d for leaf, d in walk_sources(blobs, keep=lambda l: l.id in want) if d is not None}
    rows = {r[0]: r for r in json.loads(MANIFEST.read_text(encoding="utf-8"))["leaves"] if r[0] in (DOC, VALIDATION)}
    return {"text": got[DOC].decode("utf-8"), "validation": json.loads(got[VALIDATION]), "uesfcm": got[UESFCM].decode("utf-8"),
            "handoff": got[HANDOFF].decode("utf-8"), "bytes": {DOC: got[DOC], VALIDATION: got[VALIDATION]}, "manifest_rows": rows,
            "logs": {n: json.loads(p.read_text(encoding="utf-8")) for n, p in LOGS.items()}}


# ---------------------------------------------------------------- the two differences, on discs

def lens(c1, r1, c2, r2):
    """the area of the intersection of two discs, exactly (the lens formula)"""
    d = math.dist(c1, c2)
    if d >= r1 + r2:
        return 0.0
    if d <= abs(r1 - r2):
        return math.pi * min(r1, r2) ** 2
    a = r1 * r1 * math.acos((d * d + r1 * r1 - r2 * r2) / (2 * d * r1)) + r2 * r2 * math.acos((d * d + r2 * r2 - r1 * r1) / (2 * d * r2))
    return a - 0.5 * math.sqrt((-d + r1 + r2) * (d + r1 - r2) * (d - r1 + r2) * (d + r1 + r2))


def differences(A, B):
    """sec. 10-11: D_mu = mu(A sym B) and D_J = D_mu / mu(A union B)"""
    a, b = math.pi * A[1] ** 2, math.pi * B[1] ** 2
    i = lens(A[0], A[1], B[0], B[1])
    dm = a + b - 2 * i
    return dm, dm / (a + b - i)


def metric_scan(n=20000, seed=62):
    """random disc triples: the largest triangle-inequality excess of D_mu and of D_J, D_J's range, and D_mu and D_J
    under a common scaling by 3"""
    rng = np.random.default_rng(seed)
    worst = [0.0, 0.0]
    lo, hi = 1.0, 0.0
    scale = 0.0
    for _ in range(n):
        D = [((float(x), float(y)), float(r)) for x, y, r in zip(rng.uniform(-1, 1, 3), rng.uniform(-1, 1, 3), rng.uniform(0.05, 1, 3))]
        ab, bc, ac = differences(D[0], D[1]), differences(D[1], D[2]), differences(D[0], D[2])
        for k in (0, 1):
            worst[k] = max(worst[k], ac[k] - ab[k] - bc[k])
        lo, hi = min(lo, ab[1], bc[1], ac[1]), max(hi, ab[1], bc[1], ac[1])
        S = [((3 * c[0], 3 * c[1]), 3 * r) for c, r in D[:2]]
        s3 = differences(*S)
        scale = max(scale, abs(s3[0] / ab[0] - 9) if ab[0] > 1e-12 else 0.0, abs(s3[1] - ab[1]))
    return {"triples": n, "excess_mu": worst[0], "excess_J": worst[1], "J_range": [lo, hi], "scaling": scale}


# ---------------------------------------------------------------- the arrangement's atomic cells

def euler_faces(D):
    """faces of the arrangement of circles in general position (no tangency, no three through a point): V = two per crossing
    pair, E = the vertices on each circle summed (a vertex-free circle adds no edge), C = the components of the crossing
    graph; F = E - V + 1 + C, the unbounded face included"""
    n = len(D)
    cross = [[i != j and abs(D[i][1] - D[j][1]) < math.dist(D[i][0], D[j][0]) < D[i][1] + D[j][1] for j in range(n)] for i in range(n)]
    V = sum(2 for i in range(n) for j in range(i + 1, n) if cross[i][j])
    E = sum(2 * sum(cross[i]) for i in range(n))
    seen, C = set(), 0
    for s in range(n):
        if s in seen:
            continue
        C += 1
        stack = [s]
        while stack:
            i = stack.pop()
            if i in seen:
                continue
            seen.add(i)
            stack += [j for j in range(n) if cross[i][j]]
    return E - V + 1 + C


def signatures(D, rng, n=200000):
    """the distinct membership signatures met by n uniform points in a box holding every circle: each one is at least one
    atomic cell, so there are never more than the cells (a grid count of the cells themselves is not used: near-tangent
    circles make slivers thinner than any grid, and pixels split them)"""
    xs = [c[0] - r for c, r in D] + [c[0] + r for c, r in D]
    ys = [c[1] - r for c, r in D] + [c[1] + r for c, r in D]
    P = np.column_stack([rng.uniform(min(xs) - 0.1, max(xs) + 0.1, n), rng.uniform(min(ys) - 0.1, max(ys) + 0.1, n)])
    mask = np.zeros(n, dtype=np.int64)
    for k, (c, r) in enumerate(D):
        mask |= (((P[:, 0] - c[0]) ** 2 + (P[:, 1] - c[1]) ** 2) <= r * r).astype(np.int64) << k
    return len(np.unique(mask))


def arrangement_scan(seed=62):
    """random families of 3, 4 and 5 circles (Euler's cells against the signatures met), pairwise-crossing families of 2 ...
    12 (Euler against N(N-1) + 2), and three pairwise-crossing circles (a Venn diagram: all 2^3 signatures)"""
    rng = np.random.default_rng(seed)
    rows = []
    for n in (3, 4, 5):
        for _ in range(12):
            D = [((float(x), float(y)), float(r)) for x, y, r in zip(rng.uniform(0, 1, n), rng.uniform(0, 1, n), rng.uniform(0.2, 0.45, n))]
            rows.append([n, euler_faces(D), signatures(D, rng)])
    full = []
    for n in range(2, 13):
        D = [((float(x), float(y)), 1.0) for x, y in zip(rng.uniform(0, 0.4, n), rng.uniform(0, 0.4, n))]
        full.append([n, euler_faces(D), n * (n - 1) + 2])
    venn = [((0.0, 0.0), 1.0), ((1.0, 0.0), 1.0), ((0.5, 0.85), 1.0)]
    return {"random": rows, "pairwise": full, "venn": [euler_faces(venn), signatures(venn, rng)]}


# ---------------------------------------------------------------- the searches

def _key(fn, extra=""):
    digest = hashlib.sha256(inspect.getsource(fn).encode())
    for helper in HELPERS[fn.__name__]:
        digest.update(inspect.getsource(helper).encode())
    digest.update(extra.encode())
    return digest.hexdigest()


HELPERS = {"metric_scan": (lens, differences), "arrangement_scan": (euler_faces, signatures)}


def searches():
    cached = json.loads(CACHE.read_text(encoding="utf-8")) if CACHE.exists() else {}
    out = {}
    for fn in (metric_scan, arrangement_scan):
        key = _key(fn)
        if cached.get(fn.__name__, {}).get("key") != key:
            cached[fn.__name__] = {"key": key, "value": json.loads(json.dumps(fn()))}
            CACHE.parent.mkdir(parents=True, exist_ok=True)
            CACHE.write_text(json.dumps(cached, indent=1) + "\n", encoding="utf-8")
        out[fn.__name__] = cached[fn.__name__]["value"]
    return out


# ---------------------------------------------------------------- checks

ANCHORS = {"A0": [r"\text{Generate}\rightarrow\text{Overlap}\rightarrow\text{Measure}\rightarrow\text{Cut}\rightarrow\text{Separate}\rightarrow"
                  r"\text{Recompose}\rightarrow\text{Close}", r"D_\mu(A,B)=\mu(A\triangleB),", r"D_J(A,B)=\frac{\mu(A\triangleB)}{\mu(A\cupB)}.",
                  r"b(x)=m_+(x)-m_-(x).", r"G_C\text{connected}.", r"\texttt{CONFLICTCELL}.", "0.1=searchevidence0.3=numericalreplay0.5=research"
                  "certificate0.7=independentreplay0.9=intervalcertificate1.0=formalkernel", r"O(N^2)", r"\text{Expand}=\text{circlegeneration}",
                  "195shape-activeparentroots；", r"\text{BCODR是closurearchitecture，不是universalconvergencetheorem。}"]}
DEFINITIONS = {
    "CONFLICT CELL": ("set-off", "sec. 38: an atomic cell with both strong proof support and strong counter support, which is not averaged but "
                                 "marked and given the next round's top priority"),
}
REVIEWS = [
    ("CONFLICT CELL", "", "n/a", "n/a", "a label defined in sec. 38; no judgment"),
]
CLAIMS = {
    "metrics_claim": {"triples": 20000, "triangle": [True, True], "J_in_unit": True, "scaling": True},
    # every sampled signature count within Euler's cells; N(N-1) + 2 exactly when every pair crosses (134 at N = 12); a
    # Venn diagram of three circles has 8 cells and all 8 signatures
    "arrangement_claim": {"random": [36, True, 22], "bound": True, "pairwise": True, "largest": [12, 134, 134], "venn": [8, 8]},
    "links_claim": {"weights": [0.1, 0.3, 0.5, 0.7, 0.9, 1.0],
                    "grades": ["SEARCH_EVIDENCE", "RESEARCH_CERTIFICATE", "INDEPENDENTLY_REPLAYED", "INTERVAL_CERTIFIED", "FORMAL_KERNEL"],
                    "uesfcm_stages": ["Expand", "LinkBack", "FiniteClose", "Recenter"]},
    "lebesgue_claim": {"ledger_195": [195, 0.146], "lower_branch": False, "paper09c_archived": False},
    "package_claim": {"digest": True, "manifest": True, "utf8": True, "delimiters": ["PASS", []], "code": "BCODR-v0.1",
                      "cycle": ["Generate", "Overlap", "Measure", "Cut", "Separate", "Recompose", "Close"],
                      "rules": ["connected overlap graph", "pairwise overlap"]},
}


def sig(x, n=2):
    return float(f"{x:.{n - 1}e}")


def w1_differences(w):
    m = w["search"]["metric_scan"]
    got = {"triples": m["triples"], "triangle": [m["excess_mu"] <= 1e-12, m["excess_J"] <= 1e-12], "J_in_unit": 0 <= m["J_range"][0] and m["J_range"][1] <= 1,
           "scaling": m["scaling"] < 1e-9}
    w["w1"] = got
    return [] if got == w["metrics_claim"] else [f"the differences: {got}"]


def w2_arrangement(w):
    a = w["search"]["arrangement_scan"]
    rnd = a["random"]
    got = {"random": [len(rnd), all(s <= e for _, e, s in rnd), max(e for _, e, _ in rnd)], "bound": all(e <= n * (n - 1) + 2 for n, e, _ in rnd),
           "pairwise": all(e == b for _, e, b in a["pairwise"]), "largest": a["pairwise"][-1], "venn": a["venn"]}
    w["w2"] = got
    return [] if got == w["arrangement_claim"] else [f"the arrangement: {got}"]


def w3_links(w):
    """sec. 16's weights in order, against the handoff's grades (sec. 17 there); sec. 45's four UESFCM stages in UESFCM's
    own text"""
    t, u, h = gate.squash(w["text"]), gate.squash(w["uesfcm"]), gate.squash(w["handoff"])
    weights = re.findall(r"(\d\.\d)=([a-z]+(?:[a-z]+)*)", t.split("0.1=searchevidence")[0][-10:] + "0.1=searchevidence" + t.split("0.1=searchevidence")[1][:120])
    grades = [g for g in ("SEARCH_EVIDENCE", "RESEARCH_CERTIFICATE", "INDEPENDENTLY_REPLAYED", "INTERVAL_CERTIFIED", "FORMAL_KERNEL") if g in h]
    got = {"weights": [float(x) for x, _ in weights], "grades": grades, "uesfcm_stages": [s for s in ("Expand", "LinkBack", "FiniteClose", "Recenter") if s in u]}
    w["w3"] = got
    return [] if got == w["links_claim"] else [f"the links: {got}"]


def w4_lebesgue(w):
    """sec. 84 maps the 195 shape-active roots, the local/tail split, the barrier, the positive-rotation candidate branch and
    A_ref's prune regions: the 195 leave out 14.6 % of root 6217's box (RUN-058); no lower positive-rotation branch was
    found (RUN-048, RUN-059); the Paper 09C it would not yet replace is not in the archive (RUN-060)"""
    L = w["logs"]
    got = {"ledger_195": [L["run058-lesr-paper08"]["w4"]["file_counts"][0], L["run058-lesr-paper08"]["w4"]["shares"]["outside_f3"]],
           "lower_branch": min(L["run048-uesfcm-cycles-12-14"]["w7"]["circle"][4]) <= 0 or min(L["run059-lesr-paper09a"]["w5"]["ratios"]) <= 0,
           "paper09c_archived": L["run060-lesr-paper09b"]["w7"]["next_archived"]}
    w["w4"] = got
    return [] if got == w["lebesgue_claim"] else [f"the Lebesgue link: {got}"]


def r1_registry(w):
    return registry.completeness(w["registry"], "BCODR", w["definitions"], w["reviews"])


def p1_package(w):
    v, B = w["validation"], w["bytes"]
    sha = lambda b: hashlib.sha256(b).hexdigest()
    body = B[DOC].decode("utf-8")
    got = {"digest": v["sha256"] == sha(B[DOC]), "manifest": all(w["manifest_rows"][k][2] == sha(b) for k, b in B.items()), "utf8": v["utf8"] is True,
           "delimiters": [v["math_delimiters"], sorted(set(re.findall(r"\\\(|\\\)|\\\[|\\\]", body)))], "code": v["code"],
           "cycle": v["core_cycle"], "rules": [v["default_overlap_rule"], v["strong_overlap_variant"]]}
    w["p1"] = got
    return [] if got == w["package_claim"] else [f"the package: {got}"]


def a0_anchors(w):
    text = gate.squash(w["text"])
    return [f"anchor not found: {a[:60]}" for a in ANCHORS["A0"] if a not in text]


CHECKS = {"A0": a0_anchors, "W1": w1_differences, "W2": w2_arrangement, "W3": w3_links, "W4": w4_lebesgue, "R1": r1_registry, "P1": p1_package}
DEPENDS = {"A0": {"text"}, "W1": {"search", "metrics_claim"}, "W2": {"search", "arrangement_claim"}, "W3": {"text", "uesfcm", "handoff", "links_claim"},
           "W4": {"logs", "lebesgue_claim"}, "R1": {"registry", "definitions", "reviews"}, "P1": {"validation", "bytes", "manifest_rows", "package_claim"}}


def drill_cases(base):
    def edit(obj, fn):
        x = copy.deepcopy(obj)
        fn(x)
        return x

    text, s, L = base["text"], base["search"], base["logs"]
    return [
        ("K0-unmodified", "control", {}, set()),
        ("K1-crlf", "control", {"text": text.replace("\n", "\r\n")}, set()),
        ("K2-validation-keys-sorted", "control", {"validation": json.loads(json.dumps(base["validation"], sort_keys=True))}, set()),
        ("M1-complexity", "mutation", {"text": text.replace("O(N^2)", "O(N^3)")}, {"A0"}),
        ("M2-jaccard-triangle", "mutation", {"search": edit(s, lambda x: x["metric_scan"].update(excess_J=0.01))}, {"W1"}),
        ("M3-pairwise-cells", "mutation", {"search": edit(s, lambda x: x["arrangement_scan"]["pairwise"][3].__setitem__(1, x["arrangement_scan"]["pairwise"][3][1] + 1))},
         {"W2"}),
        ("M4-signatures", "mutation", {"search": edit(s, lambda x: x["arrangement_scan"]["random"][0].__setitem__(2, 99))}, {"W2"}),
        ("M5-uesfcm-stage", "mutation", {"uesfcm": base["uesfcm"].replace("LinkBack", "Link-back")}, {"W3"}),
        ("M6-handoff-grade", "mutation", {"handoff": base["handoff"].replace("INTERVAL_CERTIFIED", "INTERVAL_DONE")}, {"W3"}),
        ("M7-ledger-share", "mutation", {"logs": edit(L, lambda x: x["run058-lesr-paper08"]["w4"]["shares"].update(outside_f3=0.0))}, {"W4"}),
        ("M8-09c-archived", "mutation", {"logs": edit(L, lambda x: x["run060-lesr-paper09b"]["w7"].update(next_archived=True))}, {"W4"}),
        ("M9-review-dropped", "mutation", {"reviews": []}, {"R1"}),
        ("M10-validation-digest", "mutation", {"validation": {**base["validation"], "sha256": "0" * 64}}, {"P1"}),
        ("M11-validation-cycle", "mutation", {"validation": {**base["validation"], "core_cycle": base["validation"]["core_cycle"][::-1]}}, {"P1"}),
    ]


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--drill", action="store_true")
    ap.add_argument("--blind", metavar="CID")
    args = ap.parse_args()
    inputs = load_inputs()
    base = {**inputs, "search": searches()}

    def build():
        return {**copy.deepcopy(base), "registry": registry.load(), "definitions": dict(DEFINITIONS), "reviews": list(REVIEWS), **copy.deepcopy(CLAIMS)}

    if args.blind:
        CHECKS[args.blind] = lambda w: []
        args.drill = True
    w = build()
    results = {}
    for c, fn in CHECKS.items():
        try:
            results[c] = fn(w)
        except KeyError as e:
            results[c] = [f"claim missing: {e}"]
    for c, f in results.items():
        print(f"{c} {'PASS' if not f else 'FAIL'}", *(f"\n     {x}" for x in f[:6]))
    for key in ("w1", "w2", "w3", "w4", "p1"):
        print(key, "=", json.dumps(w.get(key), default=str, ensure_ascii=False))
    if not args.drill:
        return
    cases = drill_cases(base)
    if args.blind:
        cases = [c for c in cases if args.blind in c[3]]
    d = gate.run_drill(build, CHECKS, DEPENDS, cases)
    summary = gate.summarize(d, CHECKS)
    print(gate.report_drill(summary))
    for x in d:
        if not x["ok"]:
            print("   DRILL FAIL", x)
    if args.blind:
        n = sum(1 for x in d if args.blind in x["expected"] and args.blind not in x["tripped"])
        print(f"blind {args.blind}: drill reported it blind in {n} cases (nothing written)")
        return
    log = {"run": "RUN-062", "subject": "BCODR: bidirectional circular overlap decomposition and recomposition", "document": DOC,
           "checks": {c: {"pass": not f, "failures": f} for c, f in results.items()},
           **{k: w.get(k) for k in ("w1", "w2", "w3", "w4", "p1")}, "drill": summary}
    LOG.write_text(json.dumps(log, ensure_ascii=False, indent=1, default=float) + "\n", encoding="utf-8")
    if not results["R1"]:
        registry.record("BCODR", "RUN-062", DEFINITIONS, REVIEWS)
        print(f"registry: {len(DEFINITIONS)} definitions, {len(REVIEWS)} reviews recorded for BCODR")


if __name__ == "__main__":
    main()
