from __future__ import annotations

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


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


def diff_named_range(change: dict[str, Any] | None) -> dict[str, Any]:
    change = change or {}
    one_based = change.get("one_based") if isinstance(change.get("one_based"), dict) else {}
    raw_scalars = change.get("raw_scalars") if isinstance(change.get("raw_scalars"), dict) else {}
    before_coords = one_based.get("before") if isinstance(one_based.get("before"), dict) else {}
    after_coords = one_based.get("after") if isinstance(one_based.get("after"), dict) else {}
    before_raw = raw_scalars.get("before") if isinstance(raw_scalars.get("before"), list) else []
    after_raw = raw_scalars.get("after") if isinstance(raw_scalars.get("after"), list) else []
    changed_fields: list[int] = []
    for index, (before, after) in enumerate(zip(before_raw, after_raw)):
        if before != after:
            changed_fields.append(index)
    return {
        "before_coords": before_coords,
        "after_coords": after_coords,
        "before_raw": before_raw,
        "after_raw": after_raw,
        "changed_scalar_indexes": changed_fields,
    }


def classify_transition(transition: dict[str, Any]) -> dict[str, Any]:
    named = diff_named_range(transition.get("named_range_changes") if isinstance(transition.get("named_range_changes"), dict) else {})
    before_coords = named["before_coords"]
    after_coords = named["after_coords"]
    text_changes = transition.get("text_match_changes") if isinstance(transition.get("text_match_changes"), dict) else {}
    before_next = (text_changes.get("next_moxel_record") or {}).get("before")
    after_next = (text_changes.get("next_moxel_record") or {}).get("after")
    count_changes = transition.get("count_changes") if isinstance(transition.get("count_changes"), dict) else {}

    top_before = before_coords.get("row_start")
    top_after = after_coords.get("row_start")
    left_before = before_coords.get("column_start")
    left_after = after_coords.get("column_start")

    labels: list[str] = []
    explanation: list[str] = []

    if top_before is not None and top_after is not None and left_before is not None and left_after is not None:
        if top_before != top_after and left_before == left_after:
            labels.append("row_move")
            explanation.append("Named range moved vertically while column stayed stable.")
        if left_before != left_after and top_before == top_after:
            labels.append("column_move")
            explanation.append("Named range moved horizontally while row stayed stable.")
        if left_before == left_after and top_before == top_after and before_coords:
            labels.append("same_named_range_coordinates")
            explanation.append("Named range coordinates stayed stable.")

    if isinstance(before_next, dict) and isinstance(after_next, dict):
        if before_next.get("type") != after_next.get("type"):
            labels.append("next_record_type_change")
            explanation.append("Text-cell next_moxel_record changed node type.")
        if before_next.get("head") != after_next.get("head"):
            labels.append("next_record_head_change")
            explanation.append("Text-cell next_moxel_record changed list head.")
        if before_next.get("scalar_prefix") != after_next.get("scalar_prefix") and before_next.get("head") == after_next.get("head"):
            labels.append("next_record_payload_change")
            explanation.append("Text-cell next_moxel_record kept the same head but changed payload.")
    elif before_next != after_next:
        labels.append("next_record_presence_change")
        explanation.append("Text-cell next_moxel_record appeared/disappeared or changed from scalar to structured form.")

    if "cell_style_candidates" in count_changes:
        labels.append("candidate_count_change")
        explanation.append("The number of visible inline text/style candidates changed.")

    target_signal_change_keys = {
        "cell_id",
        "tree_position",
        "next_moxel_record",
        "immediate_preceding_values",
        "last_7_preceding_values",
    }
    target_signal_changed = any(key in text_changes for key in target_signal_change_keys)
    named_coordinates_changed = bool(before_coords or after_coords)
    bytes_changed = transition.get("from_bytes") != transition.get("to_bytes")
    if bytes_changed and not named_coordinates_changed and not target_signal_changed and not count_changes:
        labels.append("tracked_signature_unchanged")
        explanation.append("Template bytes changed, but tracked coordinates and text-cell signature stayed stable.")

    if not labels:
        labels.append("uncategorized")
        explanation.append("No simple heuristic label matched this transition.")

    return {
        "from_file": transition.get("from_file"),
        "to_file": transition.get("to_file"),
        "labels": labels,
        "explanation": explanation,
        "named_range_diff": named,
        "next_moxel_record_before": before_next,
        "next_moxel_record_after": after_next,
        "count_changes": count_changes,
    }


def summarize_patterns(classified: list[dict[str, Any]]) -> dict[str, Any]:
    label_counts: dict[str, int] = {}
    for item in classified:
        for label in item.get("labels") or []:
            label_counts[label] = label_counts.get(label, 0) + 1
    return {
        "label_counts": [{"label": label, "count": count} for label, count in sorted(label_counts.items(), key=lambda pair: (-pair[1], pair[0]))]
    }


def recommend_next_steps(classified: list[dict[str, Any]]) -> list[str]:
    labels = {label for item in classified for label in (item.get("labels") or [])}
    steps: list[str] = []
    if "row_move" in labels and "column_move" in labels:
        steps.append("Coordinates for named cell/range are already proven; prioritize property-only changes on the same cell.")
    if "next_record_head_change" in labels or "next_record_type_change" in labels:
        steps.append("Change one presentation/property setting on `Ячейка 7 - 2` without moving it: `ВертикальноеПоложение`, then `ГоризонтальноеПоложение`, then `Защита`, then `Гиперссылка`.")
    if "candidate_count_change" in labels:
        steps.append("Avoid adding/removing neighbor cells during property experiments; that changes the candidate count and adds noise.")
    if "tracked_signature_unchanged" in labels:
        steps.append("When a save changes template bytes but leaves the tracked signature untouched, the edited property is either stored elsewhere or belongs to another cell record; capture one more isolated property change on the same cell before widening the search.")
    if not steps:
        steps.append("Continue with one-property-per-save experiments on the same tracked cell.")
    return steps


def main() -> int:
    parser = argparse.ArgumentParser(description="Infer heuristic property candidates from a 1C template history matrix.")
    parser.add_argument("matrix_json", help="Path to JSON generated by analyze_1c_template_history_matrix.py")
    parser.add_argument("--output", help="Optional JSON output path.")
    args = parser.parse_args()

    matrix = read_json(Path(args.matrix_json))
    transitions = matrix.get("transitions") or []
    classified = [classify_transition(item) for item in transitions if isinstance(item, dict)]
    payload = {
        "schema": "codex_1c_template_property_candidates.v1",
        "source": args.matrix_json,
        "classified_transitions": classified,
        "pattern_summary": summarize_patterns(classified),
        "recommended_next_steps": recommend_next_steps(classified),
    }
    rendered = json.dumps(payload, ensure_ascii=False, indent=2)
    if args.output:
        Path(args.output).write_text(rendered, encoding="utf-8")
    print(rendered)
    return 0


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