#!/usr/bin/env python3
"""Compare two 1C saved-state object reports in 1C object terms."""

from __future__ import annotations

import argparse
import hashlib
import importlib.util
import json
import sys
from pathlib import Path
from typing import Any


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


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


def write_json(path: Path, data: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def object_map(report: dict[str, Any]) -> dict[str, dict[str, Any]]:
    items = ((report.get("agent_summary") or {}).get("object_changes") or [])
    result: dict[str, dict[str, Any]] = {}
    for item in items:
        if not isinstance(item, dict):
            continue
        full_name = str(item.get("full_name") or "")
        if full_name:
            result[full_name] = item
    return result


def system_map(report: dict[str, Any]) -> dict[str, dict[str, Any]]:
    items = ((report.get("agent_summary") or {}).get("system_changes") or [])
    result: dict[str, dict[str, Any]] = {}
    for item in items:
        if not isinstance(item, dict):
            continue
        key = f"{item.get('layer')}::{item.get('extension')}::{item.get('name')}"
        result[key] = item
    return result


def stable_fingerprint(value: Any) -> str:
    encoded = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
    return hashlib.sha256(encoded).hexdigest()


def comparable_object(item: dict[str, Any]) -> dict[str, Any]:
    return {
        "full_name": item.get("full_name"),
        "layer": item.get("layer"),
        "extension": item.get("extension"),
        "kind": item.get("kind"),
        "kind_ru": item.get("kind_ru"),
        "name": item.get("name"),
        "synonym": item.get("synonym"),
        "parts_count": item.get("parts_count"),
        "text_diff_parts": item.get("text_diff_parts"),
        "active_missing_parts": item.get("active_missing_parts"),
        "added_terms": item.get("added_terms") or [],
        "removed_terms": item.get("removed_terms") or [],
        "parts": [
            {
                "file_name": part.get("file_name"),
                "payload_role": part.get("payload_role"),
                "active_exists": part.get("active_exists"),
                "summary": part.get("summary"),
                "delta_chars": part.get("delta_chars"),
            }
            for part in item.get("parts") or []
            if isinstance(part, dict)
        ],
    }


def terms_delta(before: list[Any], after: list[Any]) -> dict[str, list[Any]]:
    before_set = {str(value) for value in before}
    after_set = {str(value) for value in after}
    return {
        "added": [value for value in after if str(value) not in before_set],
        "removed": [value for value in before if str(value) not in after_set],
    }


def parts_delta(before: list[dict[str, Any]], after: list[dict[str, Any]]) -> dict[str, Any]:
    before_by_name = {str(part.get("file_name")): part for part in before if part.get("file_name")}
    after_by_name = {str(part.get("file_name")): part for part in after if part.get("file_name")}
    before_names = set(before_by_name)
    after_names = set(after_by_name)
    changed = []
    for name in sorted(before_names & after_names):
        before_part = before_by_name[name]
        after_part = after_by_name[name]
        if stable_fingerprint(before_part) != stable_fingerprint(after_part):
            changed.append({
                "file_name": name,
                "before": before_part,
                "after": after_part,
            })
    return {
        "added": [after_by_name[name] for name in sorted(after_names - before_names)],
        "removed": [before_by_name[name] for name in sorted(before_names - after_names)],
        "changed": changed,
    }


def object_delta(before: dict[str, Any], after: dict[str, Any]) -> dict[str, Any]:
    before_cmp = comparable_object(before)
    after_cmp = comparable_object(after)
    return {
        "full_name": after.get("full_name") or before.get("full_name"),
        "before_fingerprint": stable_fingerprint(before_cmp),
        "after_fingerprint": stable_fingerprint(after_cmp),
        "before": before_cmp,
        "after": after_cmp,
        "term_delta": {
            "added_terms": terms_delta(before_cmp.get("added_terms") or [], after_cmp.get("added_terms") or []),
            "removed_terms": terms_delta(before_cmp.get("removed_terms") or [], after_cmp.get("removed_terms") or []),
        },
        "part_delta": parts_delta(before_cmp.get("parts") or [], after_cmp.get("parts") or []),
    }


def compact_object(item: dict[str, Any]) -> dict[str, Any]:
    comparable = comparable_object(item)
    comparable["fingerprint"] = stable_fingerprint(comparable)
    return comparable


def compare_reports(before_path: Path, after_path: Path) -> dict[str, Any]:
    before_path = before_path.resolve()
    after_path = after_path.resolve()
    before = load_json(before_path)
    after = load_json(after_path)
    before_objects = object_map(before)
    after_objects = object_map(after)
    before_names = set(before_objects)
    after_names = set(after_objects)

    changed = []
    unchanged = []
    for name in sorted(before_names & after_names):
        before_fp = stable_fingerprint(comparable_object(before_objects[name]))
        after_fp = stable_fingerprint(comparable_object(after_objects[name]))
        if before_fp == after_fp:
            unchanged.append(name)
        else:
            changed.append(object_delta(before_objects[name], after_objects[name]))

    before_system = system_map(before)
    after_system = system_map(after)
    system_added = sorted(set(after_system) - set(before_system))
    system_removed = sorted(set(before_system) - set(after_system))
    system_changed = [
        key
        for key in sorted(set(before_system) & set(after_system))
        if stable_fingerprint(before_system[key]) != stable_fingerprint(after_system[key])
    ]

    return {
        "schema": "onec_saved_state_object_report_delta.v1",
        "before_report": str(before_path),
        "after_report": str(after_path),
        "database": after.get("database") or before.get("database"),
        "objects": {
            "added": [compact_object(after_objects[name]) for name in sorted(after_names - before_names)],
            "removed": [compact_object(before_objects[name]) for name in sorted(before_names - after_names)],
            "changed": changed,
            "unchanged": unchanged,
        },
        "system_changes": {
            "added": [after_system[key] for key in system_added],
            "removed": [before_system[key] for key in system_removed],
            "changed": [{"key": key, "before": before_system[key], "after": after_system[key]} for key in system_changed],
        },
        "counts": {
            "objects_added": len(after_names - before_names),
            "objects_removed": len(before_names - after_names),
            "objects_changed": len(changed),
            "objects_unchanged": len(unchanged),
            "system_added": len(system_added),
            "system_removed": len(system_removed),
            "system_changed": len(system_changed),
        },
        "safety": {
            "read_only": True,
            "sql_write_performed": False,
            "public_terms_are_1c_objects": True,
        },
    }


def render_markdown(delta: dict[str, Any]) -> str:
    module_path = REPO_ROOT / "scripts" / "render_1c_saved_state_object_report_delta_markdown.py"
    spec = importlib.util.spec_from_file_location("saved_state_delta_markdown", module_path)
    if spec is None or spec.loader is None:
        raise RuntimeError(f"Cannot load Markdown renderer: {module_path}")
    module = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = module
    spec.loader.exec_module(module)
    return module.render(delta)


def check_delta(delta_path: Path, check_output: Path) -> None:
    module_path = REPO_ROOT / "scripts" / "check_1c_saved_state_object_report_delta.py"
    spec = importlib.util.spec_from_file_location("saved_state_delta_check", module_path)
    if spec is None or spec.loader is None:
        raise RuntimeError(f"Cannot load delta checker: {module_path}")
    module = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = module
    spec.loader.exec_module(module)
    result = module.check_delta(delta_path)
    module.write_json(check_output, result)
    if not result.get("passed"):
        raise RuntimeError(f"Saved-state delta check failed: {check_output}")


def main() -> int:
    parser = argparse.ArgumentParser(description="Compare two 1C saved-state object reports.")
    parser.add_argument("--before", type=Path, required=True)
    parser.add_argument("--after", type=Path, required=True)
    parser.add_argument("--output", type=Path)
    parser.add_argument("--markdown-output", type=Path)
    parser.add_argument("--skip-markdown", action="store_true")
    parser.add_argument("--check-output", type=Path)
    parser.add_argument("--skip-check", action="store_true")
    args = parser.parse_args()

    result = compare_reports(args.before, args.after)
    markdown_output = args.markdown_output
    if markdown_output is None and args.output and not args.skip_markdown:
        markdown_output = args.output.with_suffix(".md")
    if markdown_output and not args.skip_markdown:
        markdown_output = markdown_output.resolve()
        result["markdown"] = str(markdown_output)
    check_output = args.check_output
    if check_output is None and args.output and not args.skip_check:
        check_output = args.output.with_name(f"{args.output.stem}-check.json")
    if check_output and not args.skip_check:
        check_output = check_output.resolve()
        result["check"] = str(check_output)
    if args.output:
        write_json(args.output, result)
    if markdown_output and not args.skip_markdown:
        markdown_output.parent.mkdir(parents=True, exist_ok=True)
        markdown_output.write_text(render_markdown(result), encoding="utf-8")
        if args.output:
            write_json(args.output, result)
    if check_output and not args.skip_check:
        if not args.output:
            raise SystemExit("Use --output when delta check output is enabled.")
        check_delta(args.output, check_output)
    print(json.dumps({"output": str(args.output) if args.output else None, "schema": result["schema"], "counts": result["counts"]}, ensure_ascii=False))
    return 0


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