from __future__ import annotations

import argparse
import json
from pathlib import Path
from typing import Any


FIELDS = ("left", "right", "top", "bottom")


def read_json(path: Path) -> dict[str, Any]:
    return json.loads(path.read_text(encoding="utf-8"))


def as_int(value: Any) -> int | None:
    try:
        return int(value)
    except (TypeError, ValueError):
        return None


def probe_ranges(payload: dict[str, Any]) -> list[dict[str, Any]]:
    probe = payload.get("probe") if isinstance(payload.get("probe"), dict) else payload
    ranges = probe.get("named_ranges") or probe.get("named_range_candidates") or []
    return [item for item in ranges if isinstance(item, dict)]


def candidate_indexes(raw_scalars: list[Any], expected_one_based: int) -> list[int]:
    result = []
    for index, value in enumerate(raw_scalars):
        if index < 2 or index > 5:
            continue
        parsed = as_int(value)
        if parsed is not None and parsed + 1 == expected_one_based:
            result.append(index)
    return result


def analyze_range(item: dict[str, Any]) -> dict[str, Any] | None:
    range_info = item.get("range") if isinstance(item.get("range"), dict) else {}
    one_based = range_info.get("one_based") if isinstance(range_info.get("one_based"), dict) else {}
    raw_scalars = item.get("raw_scalars") if isinstance(item.get("raw_scalars"), list) else []
    if not one_based or not raw_scalars:
        return None
    field_candidates: dict[str, list[int]] = {}
    for field in FIELDS:
        expected = as_int(one_based.get(field))
        if expected is None:
            continue
        field_candidates[field] = candidate_indexes(raw_scalars, expected)
    unique_values = len({one_based.get(field) for field in FIELDS if one_based.get(field) is not None})
    return {
        "name": item.get("name"),
        "kind": item.get("kind"),
        "one_based": {field: one_based.get(field) for field in FIELDS if field in one_based},
        "raw_scalars": raw_scalars,
        "field_candidates": field_candidates,
        "distinct_coordinate_values": unique_values,
    }


def aggregate_rules(samples: list[dict[str, Any]]) -> list[dict[str, Any]]:
    rules = []
    for field in FIELDS:
        sample_candidates = [set(sample.get("field_candidates", {}).get(field) or []) for sample in samples if sample.get("field_candidates", {}).get(field)]
        if not sample_candidates:
            continue
        intersection = set.intersection(*sample_candidates) if sample_candidates else set()
        all_distinct = all(int(sample.get("distinct_coordinate_values") or 0) >= 4 for sample in samples)
        confidence = "high" if len(intersection) == 1 and all_distinct else "medium" if intersection else "low"
        rules.append(
            {
                "target": f"moxel.named_range.{field}",
                "expression": "one_based = int(raw_scalar) + 1",
                "raw_scalar_indexes": sorted(intersection) if intersection else sorted(set.union(*sample_candidates)),
                "confidence": confidence,
                "evidence": {
                    "samples": len(sample_candidates),
                    "distinct_rectangular_samples": sum(1 for sample in samples if int(sample.get("distinct_coordinate_values") or 0) >= 4),
                },
            }
        )
    return rules


def analyze(probes: list[dict[str, Any]], target_name: str | None = None) -> dict[str, Any]:
    samples = []
    for payload in probes:
        for item in probe_ranges(payload):
            if target_name and str(item.get("name") or "") != target_name:
                continue
            sample = analyze_range(item)
            if sample:
                samples.append(sample)
    return {
        "schema": "codex_1c_moxel_named_range_rule_analysis.v1",
        "target_name": target_name,
        "status": "ok",
        "samples": samples,
        "rules": aggregate_rules(samples),
        "counts": {
            "samples": len(samples),
            "rules": 0,
        },
    }


def render_markdown(payload: dict[str, Any]) -> str:
    payload["counts"]["rules"] = len(payload.get("rules") or [])
    lines = ["# 1C MOXCEL Named Range Rule Analysis", ""]
    lines.append(f"- Samples: `{payload.get('counts', {}).get('samples')}`")
    lines.append(f"- Rules: `{payload.get('counts', {}).get('rules')}`")
    lines.append("")
    lines.append("| Target | Confidence | Raw indexes | Samples |")
    lines.append("| --- | --- | --- | --- |")
    for rule in payload.get("rules") or []:
        evidence = rule.get("evidence") or {}
        lines.append(
            f"| `{rule.get('target')}` | `{rule.get('confidence')}` | "
            f"`{', '.join(map(str, rule.get('raw_scalar_indexes') or []))}` | `{evidence.get('samples')}` |"
        )
    lines.append("")
    return "\n".join(lines)


def main() -> int:
    parser = argparse.ArgumentParser(description="Infer MOXCEL named range coordinate scalar indexes from probe snapshots.")
    parser.add_argument("--probe", action="append", required=True, help="Probe snapshot JSON. Repeatable.")
    parser.add_argument("--target-name", help="Optional named range to analyze.")
    parser.add_argument("--output-json", default="reports/1c-template-baselines/moxel-named-range-rules.json")
    parser.add_argument("--output-markdown", default="reports/1c-template-baselines/moxel-named-range-rules.md")
    args = parser.parse_args()

    payload = analyze([read_json(Path(path)) for path in args.probe], args.target_name)
    payload["counts"]["rules"] = len(payload.get("rules") or [])
    json_path = Path(args.output_json)
    md_path = Path(args.output_markdown)
    json_path.parent.mkdir(parents=True, exist_ok=True)
    md_path.parent.mkdir(parents=True, exist_ok=True)
    json_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
    md_path.write_text(render_markdown(payload), encoding="utf-8")
    print(json.dumps({"status": "ok", "json": str(json_path), "markdown": str(md_path), "counts": payload["counts"]}, ensure_ascii=False, indent=2))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
