#!/usr/bin/env python3
"""Compare CUAS target sections through the ImageJ MCP HTTP endpoint."""

import argparse
import json
import math
import sys
from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple

import requests


DEFAULT_URL = "http://127.0.0.1:48888/mcp/cuas/targets"

PARAMS: List[Tuple[str, int]] = [
    ("RSLT_X", 0),
    ("RSLT_Y", 1),
    ("RSLT_VX", 16),
    ("RSLT_VY", 17),
    ("RSLT_VSTR", 18),
    ("RSLT_BX", 20),
    ("RSLT_BY", 21),
    ("RSLT_AX", 22),
    ("RSLT_AY", 23),
    ("RSLT_MISMATCH_BEFORE", 24),
    ("RSLT_MISMATCH_AFTER", 25),
    ("RSLT_MISMATCH_DIRS", 26),
    ("RSLT_MATCH_LENGTH", 27),
    ("RSLT_BEFORE_LENGTH", 28),
    ("RSLT_AFTER_LENGTH", 29),
    ("RSLT_SEQ_TRAVEL", 30),
    ("RSLT_MSCORE", 31),
    ("RSLT_QMATCH", 36),
    ("RSLT_QMATCH_LEN", 37),
    ("RSLT_QTRAVEL", 38),
    ("RSLT_QSCORE", 39),
    ("RSLT_SLOW", 41),
    ("RSLT_FAIL", 43),
    ("RSLT_GLOBAL", 48),
]

PARAM_BY_INDEX = {index: name for name, index in PARAMS}

CORE_FIELDS = [
    "RSLT_MATCH_LENGTH",
    "RSLT_BEFORE_LENGTH",
    "RSLT_AFTER_LENGTH",
    "RSLT_MISMATCH_BEFORE",
    "RSLT_MISMATCH_AFTER",
    "RSLT_SEQ_TRAVEL",
    "RSLT_QMATCH",
    "RSLT_QMATCH_LEN",
    "RSLT_QTRAVEL",
    "RSLT_QSCORE",
    "RSLT_GLOBAL",
]


TargetKey = Tuple[int, int, int, int]
TargetRecord = Dict[str, Any]


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description=(
            "Query BEFORE and AFTER CUAS target TIFFs through /mcp/cuas/targets "
            "and print per-target matching length changes."
        )
    )
    parser.add_argument("--file-before", required=True, help="Path to TARGETS_SINGLE-FIRST TIFF")
    parser.add_argument("--file-after", required=True, help="Path to MATCHING_LENGTHS2 TIFF")
    parser.add_argument("--tx", type=int, required=True, help="Tile X coordinate")
    parser.add_argument("--ty", type=int, required=True, help="Tile Y coordinate")
    parser.add_argument("--seq0", type=int, required=True, help="First sequence index")
    parser.add_argument("--seq1", type=int, required=True, help="Last sequence index")
    parser.add_argument("--tw", type=int, default=1, help="Tile-section width; default: 1")
    parser.add_argument("--th", type=int, default=1, help="Tile-section height; default: 1")
    parser.add_argument("--url", default=DEFAULT_URL, help=f"MCP endpoint URL; default: {DEFAULT_URL}")
    parser.add_argument("--timeout", type=float, default=60.0, help="HTTP timeout in seconds")
    parser.add_argument(
        "--json",
        action="store_true",
        help="Print normalized JSON instead of the text report",
    )
    return parser.parse_args()


def query_targets(
    url: str,
    target_path: str,
    args: argparse.Namespace,
) -> Mapping[str, Any]:
    payload = {
        "target_path": target_path,
        "queries": [
            {
                "mode": "section",
                "tx": args.tx,
                "ty": args.ty,
                "tw": args.tw,
                "th": args.th,
                "nseq0": args.seq0,
                "nseq1": args.seq1,
                "skip_empty": False,
                "params": [index for _, index in PARAMS],
            }
        ],
    }
    response = requests.post(url, json=payload, timeout=args.timeout)
    response.raise_for_status()
    data = response.json()
    if not data.get("ok", False):
        raise RuntimeError(f"MCP request failed for {target_path}: {data}")
    return data


def normalize_response(data: Mapping[str, Any]) -> Dict[TargetKey, TargetRecord]:
    results = data.get("results") or []
    if not results:
        return {}
    section = results[0]
    if "error" in section:
        raise RuntimeError(str(section["error"]))

    records: Dict[TargetKey, TargetRecord] = {}
    for seq in section.get("sequences", []):
        nseq = int(seq["nseq"])
        for tile in seq.get("tiles", []):
            tx = int(tile["tx"])
            ty = int(tile["ty"])
            for ntarg, target in enumerate(tile.get("targets", [])):
                named = rename_params(target)
                named.update({"nseq": nseq, "tx": tx, "ty": ty, "ntarg": ntarg})
                records[(nseq, tx, ty, ntarg)] = named
    return records


def rename_params(target: Mapping[str, Any]) -> TargetRecord:
    renamed: TargetRecord = {}
    for key, value in target.items():
        try:
            numeric_key = int(key)
        except (TypeError, ValueError):
            renamed[key] = value
            continue
        renamed[PARAM_BY_INDEX.get(numeric_key, key)] = value
    return renamed


def fmt_value(value: Any) -> str:
    if value is None:
        return "-"
    if isinstance(value, float):
        if math.isnan(value):
            return "NaN"
        if math.isinf(value):
            return "Inf" if value > 0 else "-Inf"
        return f"{value:.6g}"
    return str(value)


def fmt_delta(before: Any, after: Any) -> str:
    if before is None or after is None:
        return ""
    if isinstance(before, (int, float)) and isinstance(after, (int, float)):
        delta = after - before
        if delta == 0:
            return ""
        return f" ({delta:+.6g})"
    if before != after:
        return " (changed)"
    return ""


def iter_changed_fields(before: Optional[TargetRecord], after: Optional[TargetRecord]) -> Iterable[str]:
    for field in CORE_FIELDS:
        bval = before.get(field) if before else None
        aval = after.get(field) if after else None
        if bval != aval:
            yield f"{field}: {fmt_value(bval)} -> {fmt_value(aval)}{fmt_delta(bval, aval)}"


def print_report(
    before: Mapping[TargetKey, TargetRecord],
    after: Mapping[TargetKey, TargetRecord],
    args: argparse.Namespace,
) -> None:
    keys = sorted(set(before) | set(after))
    print(f"Endpoint: {args.url}")
    print(f"BEFORE:   {args.file_before}")
    print(f"AFTER:    {args.file_after}")
    print(f"Section:  tx={args.tx}, ty={args.ty}, tw={args.tw}, th={args.th}, seq={args.seq0}..{args.seq1}")
    print(f"Targets:  before={len(before)}, after={len(after)}, union={len(keys)}")
    print()

    if not keys:
        print("No targets returned for this section.")
        return

    for key in keys:
        nseq, tx, ty, ntarg = key
        btarget = before.get(key)
        atarget = after.get(key)
        status = "changed"
        if btarget is None:
            status = "after-only"
        elif atarget is None:
            status = "before-only"
        elif not list(iter_changed_fields(btarget, atarget)):
            status = "unchanged"

        print(f"nseq={nseq} tile=({tx},{ty}) ntarg={ntarg} [{status}]")
        print("  kinematics:")
        for field in ("RSLT_BX", "RSLT_BY", "RSLT_AX", "RSLT_AY", "RSLT_VX", "RSLT_VY", "RSLT_VSTR", "RSLT_SLOW", "RSLT_FAIL"):
            bval = btarget.get(field) if btarget else None
            aval = atarget.get(field) if atarget else None
            print(f"    {field}: {fmt_value(bval)} -> {fmt_value(aval)}{fmt_delta(bval, aval)}")

        changed = list(iter_changed_fields(btarget, atarget))
        if changed:
            print("  matching:")
            for line in changed:
                print(f"    {line}")
        else:
            print("  matching: no core-field changes")
        print()


def jsonable_records(records: Mapping[TargetKey, TargetRecord]) -> Dict[str, TargetRecord]:
    return {
        f"{nseq}:{tx}:{ty}:{ntarg}": record
        for (nseq, tx, ty, ntarg), record in sorted(records.items())
    }


def main() -> int:
    args = parse_args()
    try:
        before_raw = query_targets(args.url, args.file_before, args)
        after_raw = query_targets(args.url, args.file_after, args)
        before = normalize_response(before_raw)
        after = normalize_response(after_raw)
    except requests.RequestException as exc:
        print(f"HTTP error querying MCP endpoint: {exc}", file=sys.stderr)
        return 2
    except (ValueError, RuntimeError, KeyError, TypeError) as exc:
        print(f"Error processing MCP response: {exc}", file=sys.stderr)
        return 3

    if args.json:
        print(
            json.dumps(
                {"before": jsonable_records(before), "after": jsonable_records(after)},
                indent=2,
                sort_keys=True,
                default=str,
            )
        )
    else:
        print_report(before, after, args)
    return 0


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