from __future__ import annotations

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


ROOT = Path(__file__).resolve().parents[1]

VOLATILE_KEYS = {
    "captured_at",
    "adapter_url",
    "modified",
    "bytes",
    "file_name",
    "template_file",
    "label",
    "diff",
    "cell_id",
}


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


def resolve_manifest_path(value: str, base_dir: Path) -> Path:
    path = Path(value)
    if path.is_absolute():
        return path
    candidates = [
        base_dir / path,
        ROOT / path,
        Path.cwd() / path,
        path,
    ]
    for candidate in candidates:
        resolved = candidate.resolve()
        if resolved.exists():
            return resolved
    return (base_dir / path).resolve()


def unwrap_structure(payload: dict[str, Any]) -> dict[str, Any]:
    if isinstance(payload.get("probe"), dict):
        return payload["probe"]
    if isinstance(payload.get("structure"), dict):
        return payload["structure"]
    return payload


def compact_next_record(value: Any) -> Any:
    if not isinstance(value, dict):
        return value
    result = {}
    for key in ("type", "value", "head", "scalar_prefix", "list_length", "tree_position"):
        if key in value:
            result[key] = value[key]
    return result


def normalize_item(item: Any) -> Any:
    if not isinstance(item, dict):
        return item
    result: dict[str, Any] = {}
    for key, value in item.items():
        if key in VOLATILE_KEYS:
            continue
        if key == "next_moxel_record":
            result[key] = compact_next_record(value)
        elif key == "style_evidence" and isinstance(value, dict):
            result[key] = {
                style_key: style_value
                for style_key, style_value in value.items()
                if style_key in {"immediate_preceding_values", "last_7_preceding_values"}
            }
        elif isinstance(value, dict):
            result[key] = normalize_item(value)
        elif isinstance(value, list):
            result[key] = [normalize_item(child) for child in value]
        else:
            result[key] = value
    return result


def stable_key(item: dict[str, Any], fallback_index: int) -> str:
    for key in ("text", "name"):
        if item.get(key) not in {None, ""}:
            return f"{key}:{item.get(key)}"
    if item.get("tree_position"):
        return f"tree:{item.get('tree_position')}"
    if item.get("one_based"):
        return f"cell:{json.dumps(item.get('one_based'), ensure_ascii=False, sort_keys=True)}"
    return f"index:{fallback_index}"


def normalize_section_list(items: Any) -> dict[str, Any]:
    if not isinstance(items, list):
        return {}
    result: dict[str, Any] = {}
    for index, item in enumerate(items):
        if not isinstance(item, dict):
            result[f"index:{index}"] = normalize_item(item)
            continue
        key = stable_key(item, index)
        if key in result:
            key = f"{key}#{index}"
        result[key] = normalize_item(item)
    return result


def normalized_structure(payload: dict[str, Any], *, target_text: str | None = None, target_name: str | None = None) -> dict[str, Any]:
    structure = unwrap_structure(payload)
    result: dict[str, Any] = {
        "counts": normalize_item(structure.get("counts") or {}),
        "dimensions": normalize_item(structure.get("dimensions") or {}),
        "cells": normalize_section_list(structure.get("cells") or []),
        "cell_style_candidates": normalize_section_list(structure.get("cell_style_candidates") or structure.get("cell_styles") or []),
        "named_range_candidates": normalize_section_list(structure.get("named_range_candidates") or structure.get("named_ranges") or []),
        "named_areas": normalize_section_list(structure.get("named_areas") or []),
        "column_widths": normalize_section_list(structure.get("column_widths") or []),
        "row_heights": normalize_section_list(structure.get("row_heights") or []),
        "merged_ranges": normalize_section_list(structure.get("merged_ranges") or []),
        "merged_range_candidates": normalize_section_list(structure.get("merged_range_candidates") or []),
    }
    if target_text:
        result["target_cell_styles"] = {
            key: value
            for key, value in result["cell_style_candidates"].items()
            if isinstance(value, dict) and str(value.get("text") or "") == target_text
        }
        result["target_cells"] = {
            key: value
            for key, value in result["cells"].items()
            if isinstance(value, dict) and str(value.get("text") or "") == target_text
        }
    if target_name:
        result["target_named_ranges"] = {
            key: value
            for key, value in result["named_range_candidates"].items()
            if isinstance(value, dict) and str(value.get("name") or "") == target_name
        }
    return result


def diff_values(before: Any, after: Any, path: str = "$") -> list[dict[str, Any]]:
    if before == after:
        return []
    if isinstance(before, dict) and isinstance(after, dict):
        changes: list[dict[str, Any]] = []
        for key in sorted(set(before) | set(after)):
            changes.extend(diff_values(before.get(key), after.get(key), f"{path}.{key}"))
        return changes
    if isinstance(before, list) and isinstance(after, list):
        changes = []
        for index in range(max(len(before), len(after))):
            old = before[index] if index < len(before) else None
            new = after[index] if index < len(after) else None
            changes.extend(diff_values(old, new, f"{path}[{index}]"))
        return changes
    return [{"path": path, "before": before, "after": after}]


def score_change(change: dict[str, Any], target_text: str | None, target_name: str | None) -> int:
    path = str(change.get("path") or "")
    score = 0
    if "target_" in path:
        score += 40
    if target_text and target_text in path:
        score += 30
    if target_name and target_name in path:
        score += 30
    if any(part in path for part in ("next_moxel_record", "style_evidence", "raw_scalars", "column_widths", "row_heights", "merged")):
        score += 15
    if ".counts." in path:
        score -= 20
    if ".tree_position" in path:
        score -= 10
    if path.endswith(".cell_id"):
        score -= 20
    if change.get("before") is None or change.get("after") is None:
        score -= 5
    return score


def analyze_experiment(experiment: dict[str, Any], base_dir: Path) -> dict[str, Any]:
    before_path = resolve_manifest_path(str(experiment["before"]), base_dir)
    after_path = resolve_manifest_path(str(experiment["after"]), base_dir)
    target_text = experiment.get("target_text")
    target_name = experiment.get("target_name")
    before = normalized_structure(read_json(before_path), target_text=target_text, target_name=target_name)
    after = normalized_structure(read_json(after_path), target_text=target_text, target_name=target_name)
    changes = diff_values(before, after)
    scored = sorted(
        (
            {
                **change,
                "score": score_change(change, str(target_text) if target_text else None, str(target_name) if target_name else None),
            }
            for change in changes
        ),
        key=lambda item: (-int(item.get("score") or 0), str(item.get("path") or "")),
    )
    min_positive = [item for item in scored if int(item.get("score") or 0) > 0]
    candidates = min_positive[: int(experiment.get("max_candidates") or 20)]
    confidence = "none"
    if len(candidates) == 1 and candidates[0]["score"] >= 40:
        confidence = "high"
    elif candidates and candidates[0]["score"] >= 40:
        confidence = "medium"
    elif candidates:
        confidence = "low"
    return {
        "property": experiment.get("property"),
        "operation": experiment.get("operation"),
        "target_text": target_text,
        "target_name": target_name,
        "before": str(before_path),
        "after": str(after_path),
        "confidence": confidence,
        "candidate_paths": candidates,
        "counts": {"changes": len(changes), "candidate_paths": len(candidates)},
    }


def default_probe_plan() -> list[dict[str, Any]]:
    return [
        {"property": "ГоризонтальноеПоложение", "values": ["Лево", "Центр", "Право"], "target": "cell"},
        {"property": "ВертикальноеПоложение", "values": ["Верх", "Центр", "Низ"], "target": "cell"},
        {"property": "ЦветТекста", "values": ["Черный", "Красный", "Синий"], "target": "cell"},
        {"property": "ЦветФона", "values": ["Нет", "Желтый", "Серый"], "target": "cell"},
        {"property": "Шрифт.Имя", "values": ["Arial", "Courier New"], "target": "cell"},
        {"property": "Шрифт.Размер", "values": [8, 10, 14], "target": "cell"},
        {"property": "ГраницаЛево", "values": ["Нет", "Тонкая", "Толстая"], "target": "cell"},
        {"property": "ГраницаВерх", "values": ["Нет", "Тонкая", "Толстая"], "target": "cell"},
        {"property": "Защита", "values": [True, False], "target": "cell"},
        {"property": "Гиперссылка", "values": ["", "https://example.invalid/1c-moxel-probe"], "target": "cell"},
        {"property": "Переносить", "values": [True, False], "target": "cell"},
        {"property": "ШиринаКолонки", "values": [8, 12, 20], "target": "column"},
        {"property": "ВысотаСтроки", "values": [12, 18, 24], "target": "row"},
        {"property": "Объединение", "values": ["none", "R8C4:R8C5"], "target": "range"},
    ]


def render_markdown(payload: dict[str, Any]) -> str:
    lines: list[str] = ["# 1C MOXCEL property experiments", ""]
    lines.append(f"- Experiments: `{len(payload.get('experiments') or [])}`")
    lines.append("")
    if payload.get("probe_plan"):
        lines.append("## Probe Plan")
        lines.append("")
        lines.append("| Property | Target | Values |")
        lines.append("| --- | --- | --- |")
        for item in payload["probe_plan"]:
            lines.append(f"| `{item.get('property')}` | `{item.get('target')}` | `{json.dumps(item.get('values'), ensure_ascii=False)}` |")
        lines.append("")
    if payload.get("experiments"):
        lines.append("## Results")
        lines.append("")
        lines.append("| Property | Confidence | Changes | Top path |")
        lines.append("| --- | --- | --- | --- |")
        for item in payload["experiments"]:
            top = (item.get("candidate_paths") or [{}])[0]
            lines.append(
                f"| `{item.get('property')}` | `{item.get('confidence')}` | "
                f"`{(item.get('counts') or {}).get('changes')}` | `{top.get('path') or ''}` |"
            )
        lines.append("")
    return "\n".join(lines)


def main() -> int:
    parser = argparse.ArgumentParser(description="Analyze controlled 1C MOXCEL one-property experiments.")
    parser.add_argument("--manifest", help="Experiment manifest JSON.")
    parser.add_argument("--output-json", default="reports/1c-template-baselines/Primer3_moxel_property_experiments.json")
    parser.add_argument("--output-markdown", default="reports/1c-template-baselines/Primer3_moxel_property_experiments.md")
    parser.add_argument("--emit-default-plan", action="store_true", help="Include the default next probe plan.")
    args = parser.parse_args()

    manifest_path = Path(args.manifest).resolve() if args.manifest else None
    manifest = read_json(manifest_path) if manifest_path else {"experiments": []}
    base_dir = manifest_path.parent if manifest_path else Path.cwd()
    experiments = [
        analyze_experiment(experiment, base_dir)
        for experiment in manifest.get("experiments") or []
        if isinstance(experiment, dict) and experiment.get("before") and experiment.get("after")
    ]
    payload = {
        "schema": "codex_1c_moxel_property_experiments.v1",
        "manifest": str(manifest_path) if manifest_path else None,
        "experiments": experiments,
        "probe_plan": default_probe_plan() if args.emit_default_plan or not experiments else [],
    }
    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), "experiments": len(experiments)}, ensure_ascii=False, indent=2))
    return 0


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