"""Read-only meet evidence parsing; no I/O occurs on import.

parse_rounds returns rounds, family_gates, and string warnings. Round fields:
step, subnet_names (unique, encounter order), up_count (all accepted UP records),
start_lo_ms/start_hi_ms, timing, line_numbers (1-based), structured, run_id,
timing_basis, duplicate_subnets, and events (raw records plus individual bounds).
An UP-only round's bounds describe its observed UP envelope, NOT the initiation
of a bind/round. Only NET_MEET_ROUND stamps measure actual round initiation.

Legacy brackets are approximate line-order evidence, even with equal anchors.
Missing bounds stay None; reversed anchors invalidate both bounds. Family gates
remain independent evidence; paired_up_lines uses explicit structured bind IDs,
never proximity. Structured and legacy views are kept separate, not added up.

align_rounds returns copies of candidate rounds with association, lag_lo_ms,
lag_hi_ms (peak minus original bounds, NOT clipped), eligible_lag_lo_ms and
eligible_lag_hi_ms (intersection with 0..15000), and before_interval. Unknown
rounds remain visible with association='unknown'; they are not temporal matches.
'definite' means definite temporal placement only, not causal mechanism evidence.
"""

from datetime import datetime
import json
import math
import re


_WALL = re.compile(r"(?<![\w])wall_ms=(-?\d+)(?![\w.])")
_ISO = re.compile(
    r"(?<!\d)\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}"
    r"(?:\.\d+)?(?:[Zz]|[+-]\d{2}:?\d{2})(?![\d:])"
)
_UP = re.compile(r"\bPAIR_MEET_UP:([^\s:]+)\s+step=(\d+)(?![\w.])")
_GATE = re.compile(r"\bNET_FAMILY_GATE:")
_MARKER = "NET_DIAG_V1:"
_EVENTS = {"NET_MEET_ROUND", "NET_FAMILY_GATE", "PAIR_MEET_UP"}


def _integer(value):
    return type(value) is int


def _number(value):
    return type(value) in (int, float) and math.isfinite(value)


def _wall_stamp(line):
    match = _WALL.search(line)
    if match:
        return int(match.group(1))
    for match in _ISO.finditer(line):
        try:
            stamp = datetime.fromisoformat(match.group().replace("z", "+00:00").replace("Z", "+00:00"))
            return stamp.timestamp() * 1000
        except (ValueError, OverflowError, OSError):
            continue
    return None


def _bounds(index, anchors, previous, following, warnings):
    own = anchors[index]
    if own is not None:
        return {"start_lo_ms": own, "start_hi_ms": own, "timing": "exact_stamp",
                "anchor_lines": [index + 1]}
    before, after = previous[index], following[index]
    lo = anchors[before] if before is not None else None
    hi = anchors[after] if after is not None else None
    refs = [i + 1 for i in (before, after) if i is not None]
    if lo is not None and hi is not None and lo > hi:
        warnings.append(f"line {index + 1}: non-monotonic surrounding anchors at lines {refs}; timing unknown")
        return {"start_lo_ms": None, "start_hi_ms": None, "timing": "unknown_nonmonotonic",
                "anchor_lines": refs, "preceding_wall_ms": lo, "following_wall_ms": hi}
    if lo is None or hi is None:
        warnings.append(f"line {index + 1}: missing {'both' if lo is None and hi is None else 'one'} timing anchor(s)")
    timing = "unknown" if lo is None and hi is None else "approximate_bracket"
    return {"start_lo_ms": lo, "start_hi_ms": hi, "timing": timing, "anchor_lines": refs}


def _new_round(step, structured, run_id):
    return {"step": step, "subnet_names": [], "up_count": 0,
            "start_lo_ms": None, "start_hi_ms": None, "timing": "unknown",
            "line_numbers": [], "structured": structured, "run_id": run_id,
            "duplicate_subnets": [], "events": []}


def parse_rounds(lines: list[str]) -> dict:
    """Parse legacy UP steps and v1 events, preserving uncertainty and raw evidence.

    Structured grouping is (run_id, step); legacy grouping is the literal step.
    Full coverage/clock validation belongs to the analyzer. This parser warns on
    sequence gaps and discards all copies of contradictory sequence records.
    """
    warnings, gates = [], []
    anchors = [None] * len(lines)
    records = {}
    sequence_records = {}
    invalid_sequences = set()
    run_sequences = {}
    for index, line in enumerate(lines):
        if _MARKER not in line:
            anchors[index] = _wall_stamp(line)
            continue
        try:
            record = json.loads(line.split(_MARKER, 1)[1].strip())
        except (ValueError, TypeError):
            warnings.append(f"line {index + 1}: malformed NET_DIAG_V1 JSON")
            continue
        if not isinstance(record, dict) or not (
            _integer(record.get("v")) and record["v"] == 1
            and isinstance(record.get("event"), str)
            and isinstance(record.get("run_id"), str) and record["run_id"]
            and all(_integer(record.get(k)) and record[k] >= 0 for k in ("seq", "broker_pid", "wall_ms", "mono_ms"))
        ):
            warnings.append(f"line {index + 1}: invalid NET_DIAG_V1 common fields")
            continue
        key = (record["run_id"], record["seq"])
        if key in sequence_records:
            old_index, old_record = sequence_records[key]
            if old_record != record:
                invalid_sequences.add(key)
                warnings.append(f"lines {old_index + 1},{index + 1}: contradictory structured sequence {key}")
            else:
                warnings.append(f"line {index + 1}: duplicate structured sequence {key} ignored")
            continue
        sequence_records[key] = (index, record)
        run_sequences.setdefault(record["run_id"], []).append(record["seq"])
        records[index] = record
    for run_id, sequences in run_sequences.items():
        for before, after in zip(sequences, sequences[1:]):
            if after != before + 1:
                warnings.append(f"run {run_id}: sequence gap/reordering {before}->{after}; coverage requires analyzer validation")
    for index, record in list(records.items()):
        if (record["run_id"], record["seq"]) in invalid_sequences:
            del records[index]
        else:
            anchors[index] = record["wall_ms"]

    previous, following = [None] * len(lines), [None] * len(lines)
    last = None
    for index in range(len(lines)):
        previous[index] = last
        if anchors[index] is not None:
            last = index
    last = None
    for index in range(len(lines) - 1, -1, -1):
        following[index] = last
        if anchors[index] is not None:
            last = index

    groups = {}
    bind_ups = {}
    for index, line in enumerate(lines):
        record = records.get(index)
        if _MARKER in line:
            if record is None or record["event"] not in _EVENTS:
                continue
            event = record["event"]
            step, subnet = record.get("step"), record.get("subnet")
            if not _integer(step) or step < 0:
                warnings.append(f"line {index + 1}: {event} missing/invalid step")
                continue
            if event == "NET_MEET_ROUND":
                names = record.get("subnets")
                if not isinstance(names, list) or not all(isinstance(n, str) and n for n in names):
                    warnings.append(f"line {index + 1}: NET_MEET_ROUND invalid subnets")
                    continue
            else:
                if not isinstance(subnet, str) or not subnet or not isinstance(record.get("op_id"), str) or not record["op_id"]:
                    warnings.append(f"line {index + 1}: {event} missing/invalid subnet or op_id")
                    continue
                names = [subnet]
            structured, run_id = True, record["run_id"]
        else:
            match = _UP.search(line)
            if match:
                event, subnet, step = "PAIR_MEET_UP", match.group(1), int(match.group(2))
                names = [subnet]
            elif _GATE.search(line):
                event, subnet, step, names = "NET_FAMILY_GATE", None, None, []
            else:
                if "PAIR_MEET_UP:" in line:
                    warnings.append(f"line {index + 1}: malformed legacy PAIR_MEET_UP")
                continue
            structured, run_id = False, None
        evidence = {"event": event, "line_numbers": [index + 1], "structured": structured,
                    "run_id": run_id, "step": step, "subnet": subnet if event != "NET_MEET_ROUND" else None,
                    "raw": line.rstrip("\r\n"), "record": record,
                    **_bounds(index, anchors, previous, following, warnings)}
        if event == "NET_FAMILY_GATE":
            evidence["op_id"] = record["op_id"] if structured else None
            evidence["paired_up_lines"] = []
            gates.append(evidence)
            continue
        key = (structured, run_id, step)
        group = groups.setdefault(key, _new_round(step, structured, run_id))
        group["line_numbers"].append(index + 1)
        group["events"].append(evidence)
        for name in names:
            if name not in group["subnet_names"]:
                group["subnet_names"].append(name)
        if event == "PAIR_MEET_UP":
            group["up_count"] += 1
            if structured:
                bind_key = (run_id, step, subnet, record["op_id"])
                bind_ups.setdefault(bind_key, []).append(index + 1)

    for group in groups.values():
        counts = {}
        for event in group["events"]:
            if event["event"] == "PAIR_MEET_UP":
                name = event["subnet"]
                counts[name] = counts.get(name, 0) + 1
        group["duplicate_subnets"] = [name for name, count in counts.items() if count > 1]
        if group["duplicate_subnets"]:
            warnings.append(f"round {group['run_id']} step {group['step']}: duplicate UP subnets {group['duplicate_subnets']}; UP count retains repeats")
        starts = [e for e in group["events"] if e["event"] == "NET_MEET_ROUND"]
        selected = starts or group["events"]
        group["timing_basis"] = "NET_MEET_ROUND" if starts else "observed_up_envelope_not_round_initiation"
        if len(starts) > 1:
            warnings.append(f"run {group['run_id']} step {group['step']}: multiple NET_MEET_ROUND stamps; retaining envelope")
        lows = [e["start_lo_ms"] for e in selected]
        highs = [e["start_hi_ms"] for e in selected]
        group["start_lo_ms"] = min(lows) if all(v is not None for v in lows) else None
        group["start_hi_ms"] = max(highs) if all(v is not None for v in highs) else None
        if any(e["timing"] == "unknown_nonmonotonic" for e in selected):
            group["start_lo_ms"] = group["start_hi_ms"] = None
            group["timing"] = "unknown_nonmonotonic"
        elif all(e["timing"] == "exact_stamp" for e in selected):
            group["timing"] = "exact_stamp" if group["start_lo_ms"] == group["start_hi_ms"] else "stamped_envelope"
        else:
            group["timing"] = "unknown" if all(v is None for v in lows + highs) else "approximate_bracket"
    for gate in gates:
        if gate["structured"]:
            gate["paired_up_lines"] = bind_ups.get((gate["run_id"], gate["step"], gate["subnet"], gate["op_id"]), [])
        gate["paired"] = bool(gate["paired_up_lines"])
    if any(not g["structured"] for g in groups.values()):
        warnings.append("Legacy UP envelopes are approximate round proxies, not bind/round initiation timestamps; steps are never converted to UTC.")
    if any(not g["paired"] for g in gates):
        warnings.append("Unpaired family gates retained independently; no subnet attribution is inferred from adjacency.")
    if any(g["structured"] for g in groups.values()) and any(not g["structured"] for g in groups.values()):
        warnings.append("Mixed structured/legacy round views retained separately; human mirror lines must not be counted as additional rounds.")
    return {"rounds": list(groups.values()), "family_gates": gates, "warnings": warnings}


def align_rounds(parsed: dict, interval_start_ms: float, peak_ms: float) -> list[dict]:
    """Select envelopes intersecting [peak-15000, peak], preserving original bounds.

    interval_start_ms classifies precedence to stale support independently of
    peak association. Missing/invalid timing is visible but never definite.
    Clock/epoch eligibility must be checked by the caller before using a match.
    """
    if not _number(interval_start_ms) or not _number(peak_ms) or interval_start_ms > peak_ms:
        raise ValueError("finite interval_start_ms <= peak_ms is required")
    result = []
    window_start = peak_ms - 15000
    for group in parsed["rounds"]:
        lo, hi = group["start_lo_ms"], group["start_hi_ms"]
        if (lo is not None and not _number(lo)) or (hi is not None and not _number(hi)):
            raise ValueError("round timing bounds must be finite numbers or None")
        if lo is not None and hi is not None and lo > hi:
            raise ValueError("round timing bounds are reversed")
        if (lo is not None and lo > peak_ms) or (hi is not None and hi < window_start):
            continue
        lag_lo = peak_ms - hi if hi is not None else None
        lag_hi = peak_ms - lo if lo is not None else None
        if lo is None and hi is None:
            association = "unknown"
            eligible_lo = eligible_hi = None
        else:
            eligible_lo = max(0, lag_lo) if lag_lo is not None else 0
            eligible_hi = min(15000, lag_hi) if lag_hi is not None else 15000
            inside = lo is not None and hi is not None and window_start <= lo <= hi <= peak_ms
            exact = group["timing"] in ("exact_stamp", "stamped_envelope")
            association = "definite" if inside and exact else "possible_approximate"
        if hi is not None and hi <= interval_start_ms:
            precedence = "definite" if group["timing"] in ("exact_stamp", "stamped_envelope") else "approximate"
        elif lo is not None and lo > interval_start_ms:
            precedence = "no"
        else:
            precedence = "unknown" if lo is None and hi is None else "possible"
        result.append({**group, "association": association, "lag_lo_ms": lag_lo, "lag_hi_ms": lag_hi,
                       "eligible_lag_lo_ms": eligible_lo, "eligible_lag_hi_ms": eligible_hi,
                       "before_interval": precedence, "interval_start_ms": interval_start_ms, "peak_ms": peak_ms})
    return result
