"""Dynamics 365 snapshot comparison - manifest-based diff.

Compares two snapshot directories by their manifest.json files.
Produces: files added/removed/changed (by sha256), category summaries.
Semantic diffs (field-level, form-level) are a follow-up.
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List
import json


@dataclass
class DynamicsSnapshotDiff:
    meta: Dict[str, Any]
    files_added: List[str]
    files_removed: List[str]
    files_changed: List[Dict[str, Any]]
    category_summary_a: Dict[str, int]
    category_summary_b: Dict[str, int]
    category_delta: Dict[str, Dict[str, int]]

    def to_dict(self) -> Dict[str, Any]:
        total_a = sum(self.category_summary_a.values())
        counts = {
            "added": len(self.files_added),
            "removed": len(self.files_removed),
            "changed": len(self.files_changed),
            "unchanged": max(0, total_a - (len(self.files_removed) + len(self.files_changed))),
        }
        return {
            "meta": self.meta,
            "files": {
                "added": self.files_added,
                "removed": self.files_removed,
                "changed": self.files_changed,
                "counts": counts,
            },
            "categories": self.category_delta,
            "category_summary_a": self.category_summary_a,
            "category_summary_b": self.category_summary_b,
        }


def compare_snapshots(a_dir: Path, b_dir: Path) -> DynamicsSnapshotDiff:
    """
    Compare two snapshot directories by manifest.json.
    1. Load manifest.json from both dirs
    2. Compare meta (environment match, solution version, toolchain)
    3. Diff file entries by relpath -> sha256
    4. Compute category-level deltas
    """
    a_manifest, a_meta = _load_manifest(a_dir)
    b_manifest, b_meta = _load_manifest(b_dir)

    files_added = sorted([path for path in b_manifest if path not in a_manifest])
    files_removed = sorted([path for path in a_manifest if path not in b_manifest])

    files_changed: List[Dict[str, Any]] = []
    for relpath in sorted(set(a_manifest.keys()) & set(b_manifest.keys())):
        before = a_manifest[relpath]
        after = b_manifest[relpath]
        if before.get("sha256") != after.get("sha256"):
            files_changed.append(
                {
                    "relpath": relpath,
                    "before_hash": before.get("sha256"),
                    "after_hash": after.get("sha256"),
                    "type_tag": after.get("type_tag") or before.get("type_tag") or "other",
                }
            )

    category_summary_a = _category_summary(a_manifest)
    category_summary_b = _category_summary(b_manifest)
    category_delta = _category_delta(files_added, files_removed, files_changed, a_manifest, b_manifest)

    meta = {
        "environment_match": _meta_value(a_meta, "environment_url") == _meta_value(b_meta, "environment_url"),
        "environment_url_a": _meta_value(a_meta, "environment_url"),
        "environment_url_b": _meta_value(b_meta, "environment_url"),
        "solution_version_a": _meta_value(a_meta, "solution_version"),
        "solution_version_b": _meta_value(b_meta, "solution_version"),
        "solution_name": _meta_value(a_meta, "solution_name") or _meta_value(b_meta, "solution_name"),
    }

    return DynamicsSnapshotDiff(
        meta=meta,
        files_added=files_added,
        files_removed=files_removed,
        files_changed=files_changed,
        category_summary_a=category_summary_a,
        category_summary_b=category_summary_b,
        category_delta=category_delta,
    )


def format_summary(diff: DynamicsSnapshotDiff) -> str:
    """Human-readable summary."""
    lines: List[str] = []
    solution = diff.meta.get("solution_name") or "Solution"
    lines.append(f"Solution: {solution}")
    version_a = diff.meta.get("solution_version_a") or "?"
    version_b = diff.meta.get("solution_version_b") or "?"
    lines.append(f"Version: A={version_a} B={version_b}")
    lines.append(
        f"Files:   +{len(diff.files_added)} -{len(diff.files_removed)} ~{len(diff.files_changed)}"
    )

    for tag in sorted(diff.category_delta.keys()):
        delta = diff.category_delta[tag]
        lines.append(
            f"{tag:<10} +{delta.get('added', 0)} -{delta.get('removed', 0)} ~{delta.get('changed', 0)}"
        )

    return "\n".join(lines)


# Internal

def _load_manifest(dir: Path) -> tuple[Dict[str, Dict[str, Any]], Dict[str, Any]]:
    """Load manifest.json, return {relpath: entry} and meta.json payload."""
    manifest_path = dir / "manifest.json"
    meta_path = dir / "meta.json"
    manifest_raw: Dict[str, Any] = {}
    meta_raw: Dict[str, Any] = {}
    if manifest_path.exists():
        manifest_raw = json.loads(manifest_path.read_text(encoding="utf-8"))
    if meta_path.exists():
        meta_raw = json.loads(meta_path.read_text(encoding="utf-8"))
    entries = manifest_raw.get("entries") or []
    manifest: Dict[str, Dict[str, Any]] = {}
    for entry in entries:
        relpath = entry.get("relpath")
        if relpath:
            manifest[str(relpath)] = entry
    return manifest, meta_raw


def _category_summary(manifest: Dict[str, Dict[str, Any]]) -> Dict[str, int]:
    summary: Dict[str, int] = {}
    for entry in manifest.values():
        tag = entry.get("type_tag") or "other"
        summary[tag] = summary.get(tag, 0) + 1
    return summary


def _category_delta(
    added: List[str],
    removed: List[str],
    changed: List[Dict[str, Any]],
    a_manifest: Dict[str, Dict[str, Any]],
    b_manifest: Dict[str, Dict[str, Any]],
) -> Dict[str, Dict[str, int]]:
    delta: Dict[str, Dict[str, int]] = {}

    def _bump(tag: str, key: str) -> None:
        bucket = delta.setdefault(tag, {"added": 0, "removed": 0, "changed": 0})
        bucket[key] += 1

    for rel in added:
        tag = (b_manifest.get(rel) or {}).get("type_tag") or "other"
        _bump(tag, "added")
    for rel in removed:
        tag = (a_manifest.get(rel) or {}).get("type_tag") or "other"
        _bump(tag, "removed")
    for item in changed:
        tag = item.get("type_tag") or "other"
        _bump(tag, "changed")

    return delta


def _meta_value(meta: Dict[str, Any], key: str) -> str:
    if not meta:
        return ""
    if key in meta and meta[key]:
        return str(meta[key])
    for variant in (key.lower(), key.upper()):
        if variant in meta and meta[variant]:
            return str(meta[variant])
    return ""
