"""Structured conflict detection and reporting for three-way plan merges."""

from __future__ import annotations

from enum import StrEnum
from typing import Any

from pydantic import BaseModel, ConfigDict, Field

from cleveragents.domain.models.base import DomainBaseModel


class ConflictType(StrEnum):
    """Categorises the nature of a merge conflict between three versions."""

    MODIFY_MODIFY = "MODIFY_MODIFY"
    MODIFY_DELETE = "MODIFY_DELETE"
    DELETE_MODIFY = "DELETE_MODIFY"
    DELETE_DELETE = "DELETE_DELETE"
    ADD_ADD = "ADD_ADD"


class ConflictContext(BaseModel):
    """Holds the three competing versions of a single field during merge."""

    model_config = ConfigDict(frozen=True)
    ancestor_value: str | None = None
    parent_value: str | None = None
    subplan_value: str | None = None


class ConflictReport(DomainBaseModel):
    """Structured report of all conflicts found during a three-way merge."""

    key: str
    conflict_type: ConflictType
    context: ConflictContext
    resolution_strategy: str | None = None

    @property
    def resolved_value(self) -> str | None:
        ctx = self.context
        if self.conflict_type == ConflictType.MODIFY_DELETE:
            return ctx.parent_value
        if self.conflict_type == ConflictType.DELETE_MODIFY:
            return ctx.subplan_value
        if (
            self.conflict_type == ConflictType.ADD_ADD
            and ctx.parent_value is not None
            and ctx.parent_value == ctx.subplan_value
        ):
            return ctx.parent_value
        return None


class MergeResult(DomainBaseModel):
    """The output of a complete three-way merge operation."""

    merged_config: dict[str, Any] = Field(default_factory=dict)
    conflicts: list[ConflictReport] = Field(default_factory=list)
    auto_resolved: dict[str, str | None] = Field(default_factory=dict)


class ThreeWayMergeConflictDetector:
    """Detects and resolves conflicts during a three-way plan merge."""

    def __init__(
        self, ancestor: dict[str, Any], parent: dict[str, Any], subplan: dict[str, Any]
    ) -> None:
        self.ancestor = ancestor or {}
        self.parent = parent or {}
        self.subplan = subplan or {}  # -- Public API --

    def detect(self) -> MergeResult:
        """Run the three-way merge and return conflicts plus merged result."""
        merged: dict[str, Any] = {}
        conflicts: list[ConflictReport] = []
        auto_resolved: dict[str, str | None] = {}

        for key in sorted(set(self.parent.keys()) | set(self.subplan.keys())):
            ancestor_has_key = key in self.ancestor
            parent_has_key = key in self.parent
            subplan_has_key = key in self.subplan

            ancestor_val = self.ancestor.get(key) if ancestor_has_key else None
            parent_val = self.parent.get(key) if parent_has_key else None
            subplan_val = self.subplan.get(key) if subplan_has_key else None

            ancestor_s = str(ancestor_val) if ancestor_val is not None else None
            parent_s = str(parent_val) if parent_val is not None else None
            subplan_s = str(subplan_val) if subplan_val is not None else None

            if parent_s == subplan_s and parent_s == ancestor_s:
                merged[key] = parent_s
                continue

            conflict = self._analyse_field(
                key,
                ancestor_s,
                parent_s,
                subplan_s,
                ancestor_has_key=ancestor_has_key,
                parent_has_key=parent_has_key,
                subplan_has_key=subplan_has_key,
            )

            rv = conflict.resolved_value
            if rv is not None:
                merged[key] = rv
                auto_resolved[key] = rv
            elif conflict.conflict_type == ConflictType.DELETE_DELETE:
                pass  # Both deleted it -- absent from output
            else:
                conflicts.append(conflict)

        return MergeResult(
            merged_config=merged,
            conflicts=conflicts,
            auto_resolved=auto_resolved,
        )

    def detect_diff_text(self) -> str:
        """Human-readable summary of all conflicts."""
        lines: list[str] = []
        result = self.detect()

        if not result.conflicts:
            return "No conflicts detected -- perfect merge."

        n_conflicts = len(result.conflicts)
        n_resolved = len(result.auto_resolved)
        lines.append(
            f"Three-way merge: {n_conflicts} conflict(s), {n_resolved} auto-resolved."
        )
        lines.append("")

        for cr in result.conflicts:
            ctx = cr.context
            lines.append(f"  Key: {cr.key}")
            lines.append(f"  Type: {cr.conflict_type}")
            an = "  Ancestor: " + (
                ctx.ancestor_value if ctx.ancestor_value is not None else "(absent)"
            )
            pa = "  Parent:   " + (
                ctx.parent_value if ctx.parent_value is not None else "(absent)"
            )
            su = "  Subplan:  " + (
                ctx.subplan_value if ctx.subplan_value is not None else "(absent)"
            )
            lines.extend([an, pa, su, ""])

        for k, v in result.auto_resolved.items():
            lines.append(f"  * Auto-resolved {k}: {v}")

        if result.merged_config:
            lines.append("Merged config:")
            for k, v in sorted(result.merged_config.items()):
                lines.append(f"    {k}: {v}")
        return "\n".join(lines)

    def describe_all(self) -> list[dict[str, Any]]:
        """Serializable summary of every field's merge outcome."""
        result = self.detect()
        summary: list[dict[str, Any]] = []
        all_keys = sorted(
            set(result.merged_config.keys()) | {cr.key for cr in result.conflicts}
        )

        for key in all_keys:
            entry: dict[str, Any] = {"key": key}
            cr = next((c for c in result.conflicts if c.key == key), None)
            if cr is not None:
                entry.update(
                    {
                        "status": "CONFLICT",
                        "conflict_type": str(cr.conflict_type),
                        "ancestor": cr.context.ancestor_value,
                        "parent": cr.context.parent_value,
                        "subplan": cr.context.subplan_value,
                    }
                )
            elif key in result.auto_resolved:
                entry.update(
                    {
                        "status": "AUTO_RESOLVED",
                        "resolved_to": result.auto_resolved[key],
                    }
                )
            else:
                entry.update(
                    {"status": "MERGED_OK", "value": result.merged_config.get(key)}
                )
            summary.append(entry)
        return summary

    def _analyse_field(
        self,
        key: str,
        ancestor_val: str | None,
        parent_val: str | None,
        subplan_val: str | None,
        *,
        ancestor_has_key: bool,
        parent_has_key: bool,
        subplan_has_key: bool,
    ) -> ConflictReport:
        """Classify a single field into a conflict type."""
        p_changed = (ancestor_has_key != parent_has_key) or (
            ancestor_has_key and parent_has_key and str(ancestor_val) != str(parent_val)
        )
        s_changed = (ancestor_has_key != subplan_has_key) or (
            ancestor_has_key
            and subplan_has_key
            and str(ancestor_val) != str(subplan_val)
        )

        if not p_changed and not s_changed:
            if ancestor_has_key and not parent_has_key and not subplan_has_key:
                ct = ConflictType.DELETE_DELETE
            else:
                ct = ConflictType.ADD_ADD

            return ConflictReport(
                key=key,
                conflict_type=ct,
                context=ConflictContext(
                    ancestor_value=ancestor_val,
                    parent_value=parent_val,
                    subplan_value=subplan_val,
                ),
            )

        # Both sides changed independently from ancestor.
        if p_changed and s_changed:
            if not ancestor_has_key:
                ct = ConflictType.ADD_ADD
            elif parent_val is None:
                ct = ConflictType.DELETE_MODIFY
            elif subplan_val is None:
                ct = ConflictType.MODIFY_DELETE
            elif str(parent_val) == str(subplan_val):
                ct = ConflictType.ADD_ADD
            else:
                ct = ConflictType.MODIFY_MODIFY
        elif p_changed and not s_changed:
            if ancestor_val is not None and parent_val is None:
                ct = ConflictType.DELETE_DELETE
            else:
                ct = ConflictType.MODIFY_DELETE
        else:
            if ancestor_val is not None and subplan_val is None:
                ct = ConflictType.DELETE_DELETE
            else:
                ct = ConflictType.DELETE_MODIFY

        return ConflictReport(
            key=key,
            conflict_type=ct,
            context=ConflictContext(
                ancestor_value=ancestor_val,
                parent_value=parent_val,
                subplan_value=subplan_val,
            ),
        )
