"""DTCG layer merger with provenance tracking.

Merges multiple DTCG TokenFile objects in layer precedence order
(core → fork → org → group → personal), tracking which layer
contributed each token value.
"""

from __future__ import annotations

import copy
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Sequence

from tokenmoulds.dtcg.types import Token, TokenFile, TokenGroup


@dataclass
class DTCGLayer:
    """A single DTCG token file representing one layer."""

    name: str  # core, fork, org, group, personal
    token_file: TokenFile
    source_path: Optional[str] = None


@dataclass
class DTCGProvenance:
    """Tracks the source layer for a DTCG token value."""

    layer: str
    source_path: Optional[str] = None
    overridden_by: Optional[List[str]] = None


@dataclass
class DTCGOverride:
    """Records a single value override during layer merging."""

    path: str
    from_layer: str
    to_layer: str
    old_value: Any
    new_value: Any


@dataclass
class DTCGConflict:
    """Records when multiple layers define the same token path."""

    path: str
    layers: List[str]
    final_layer: str
    final_value: Any


@dataclass
class DTCGMergeAudit:
    """Audit trail for DTCG layer merge operation."""

    timestamp: datetime
    layers_used: List[str]
    layer_sources: Dict[str, Optional[str]]
    overrides: List[DTCGOverride] = field(default_factory=list)
    conflicts: List[DTCGConflict] = field(default_factory=list)
    errors: List[str] = field(default_factory=list)
    warnings: List[str] = field(default_factory=list)

    def to_dict(self) -> Dict[str, Any]:
        """Convert audit to dictionary for serialization."""
        return {
            "timestamp": self.timestamp.isoformat(),
            "layers_used": self.layers_used,
            "layer_sources": self.layer_sources,
            "overrides": [
                {
                    "path": o.path,
                    "from_layer": o.from_layer,
                    "to_layer": o.to_layer,
                    "old_value": _safe_serialize(o.old_value),
                    "new_value": _safe_serialize(o.new_value),
                }
                for o in self.overrides
            ],
            "conflicts": [
                {
                    "path": c.path,
                    "layers": c.layers,
                    "final_layer": c.final_layer,
                    "final_value": _safe_serialize(c.final_value),
                }
                for c in self.conflicts
            ],
            "errors": self.errors,
            "warnings": self.warnings,
            "summary": {
                "total_overrides": len(self.overrides),
                "total_conflicts": len(self.conflicts),
                "total_errors": len(self.errors),
                "total_warnings": len(self.warnings),
            },
        }


@dataclass
class DTCGMergeResult:
    """Result of DTCG layer merge operation."""

    merged: TokenFile
    provenance: Dict[str, DTCGProvenance]
    audit: DTCGMergeAudit


def _safe_serialize(value: Any) -> Any:
    """Safely serialize a value for JSON output."""
    if hasattr(value, "to_hex"):
        return value.to_hex()
    if hasattr(value, "__dict__"):
        return str(value)
    return value


class DTCGMerger:
    """Merges DTCG TokenFiles in layer precedence order.

    Tracks:
    - Which layer each token came from (provenance)
    - What tokens were overridden by higher layers
    - Conflicts where multiple layers define the same path
    """

    # Layer precedence order (lowest to highest)
    LAYER_ORDER = ("core", "fork", "org", "group", "personal")

    def __init__(self, *, detect_conflicts: bool = True) -> None:
        """Initialize merger.

        Args:
            detect_conflicts: If True, detect and warn about layer conflicts
        """
        self.detect_conflicts = detect_conflicts

    def merge(
        self,
        layers: Sequence[DTCGLayer],
    ) -> DTCGMergeResult:
        """Merge DTCG layers in precedence order.

        Args:
            layers: Sequence of DTCGLayer objects. Should be in precedence
                   order (lowest first: core, fork, org, group, personal).

        Returns:
            DTCGMergeResult with merged TokenFile, provenance, and audit trail.
        """
        audit = DTCGMergeAudit(
            timestamp=datetime.now(timezone.utc),
            layers_used=[layer.name for layer in layers],
            layer_sources={layer.name: layer.source_path for layer in layers},
        )

        provenance: Dict[str, DTCGProvenance] = {}
        merged = TokenFile()

        # Detect conflicts before merging
        if self.detect_conflicts:
            conflicts = self._detect_conflicts(layers)
            audit.conflicts = conflicts
            for conflict in conflicts:
                audit.warnings.append(
                    f"Token '{conflict.path}' defined in multiple layers: "
                    f"{conflict.layers} (using '{conflict.final_layer}')"
                )

        # Merge layers in order
        prev_layer = "base"
        for layer in layers:
            self._merge_layer_into(merged, layer, prev_layer, provenance, audit)
            prev_layer = layer.name

        return DTCGMergeResult(
            merged=merged,
            provenance=provenance,
            audit=audit,
        )

    def _merge_layer_into(
        self,
        target: TokenFile,
        layer: DTCGLayer,
        prev_layer: str,
        provenance: Dict[str, DTCGProvenance],
        audit: DTCGMergeAudit,
    ) -> None:
        """Deep merge a single layer into target TokenFile."""
        # Merge root-level tokens
        for token_name, token in layer.token_file.tokens.items():
            self._merge_token(
                target.tokens,
                token_name,
                token,
                token_name,
                prev_layer,
                layer.name,
                layer.source_path,
                provenance,
                audit,
            )

        # Merge groups
        for group_name, group in layer.token_file.groups.items():
            if group_name not in target.groups:
                # New group - deep copy and track provenance
                target.groups[group_name] = copy.deepcopy(group)
                self._track_group_provenance(
                    group, group_name, layer.name, layer.source_path, provenance
                )
            else:
                # Merge into existing group
                self._merge_group(
                    target.groups[group_name],
                    group,
                    group_name,
                    prev_layer,
                    layer.name,
                    layer.source_path,
                    provenance,
                    audit,
                )

    def _merge_group(
        self,
        target: TokenGroup,
        source: TokenGroup,
        path_prefix: str,
        prev_layer: str,
        layer_name: str,
        source_path: Optional[str],
        provenance: Dict[str, DTCGProvenance],
        audit: DTCGMergeAudit,
    ) -> None:
        """Recursively merge token groups."""
        # Merge tokens
        for token_name, token in source.tokens.items():
            token_path = f"{path_prefix}.{token_name}"
            self._merge_token(
                target.tokens,
                token_name,
                token,
                token_path,
                prev_layer,
                layer_name,
                source_path,
                provenance,
                audit,
            )

        # Merge nested groups recursively
        for group_name, group in source.groups.items():
            group_path = f"{path_prefix}.{group_name}"
            if group_name not in target.groups:
                target.groups[group_name] = copy.deepcopy(group)
                self._track_group_provenance(
                    group, group_path, layer_name, source_path, provenance
                )
            else:
                self._merge_group(
                    target.groups[group_name],
                    group,
                    group_path,
                    prev_layer,
                    layer_name,
                    source_path,
                    provenance,
                    audit,
                )

    def _merge_token(
        self,
        target_tokens: Dict[str, Token],
        token_name: str,
        token: Token,
        token_path: str,
        prev_layer: str,
        layer_name: str,
        source_path: Optional[str],
        provenance: Dict[str, DTCGProvenance],
        audit: DTCGMergeAudit,
    ) -> None:
        """Merge a single token, tracking overrides."""
        if token_name in target_tokens:
            # Override - record it
            old_token = target_tokens[token_name]
            old_prov = provenance.get(token_path)

            audit.overrides.append(
                DTCGOverride(
                    path=token_path,
                    from_layer=old_prov.layer if old_prov else prev_layer,
                    to_layer=layer_name,
                    old_value=old_token.value,
                    new_value=token.value,
                )
            )

            # Update provenance with override history
            overridden_by = []
            if old_prov:
                if old_prov.overridden_by:
                    overridden_by = old_prov.overridden_by + [old_prov.layer]
                else:
                    overridden_by = [old_prov.layer]

            provenance[token_path] = DTCGProvenance(
                layer=layer_name,
                source_path=source_path,
                overridden_by=overridden_by if overridden_by else None,
            )
        else:
            # New token
            provenance[token_path] = DTCGProvenance(
                layer=layer_name,
                source_path=source_path,
            )

        # Copy token (deep copy to avoid mutation)
        target_tokens[token_name] = copy.deepcopy(token)
        target_tokens[token_name].path = token_path

    def _track_group_provenance(
        self,
        group: TokenGroup,
        path_prefix: str,
        layer_name: str,
        source_path: Optional[str],
        provenance: Dict[str, DTCGProvenance],
    ) -> None:
        """Track provenance for all tokens in a new group."""
        for token_name, token in group.tokens.items():
            token_path = f"{path_prefix}.{token_name}"
            provenance[token_path] = DTCGProvenance(
                layer=layer_name,
                source_path=source_path,
            )
            token.path = token_path

        for group_name, nested_group in group.groups.items():
            self._track_group_provenance(
                nested_group,
                f"{path_prefix}.{group_name}",
                layer_name,
                source_path,
                provenance,
            )

    def _detect_conflicts(
        self,
        layers: Sequence[DTCGLayer],
    ) -> List[DTCGConflict]:
        """Detect tokens defined in multiple layers."""
        conflicts: List[DTCGConflict] = []
        path_to_layers: Dict[str, List[str]] = {}
        path_to_value: Dict[str, Any] = {}
        path_to_final_layer: Dict[str, str] = {}

        for layer in layers:
            # Collect all tokens including those in groups
            seen_paths: set = set()

            # Root-level tokens
            for token in layer.token_file.tokens.values():
                path = token.path or token.name
                if path in seen_paths:
                    continue
                seen_paths.add(path)
                if path not in path_to_layers:
                    path_to_layers[path] = []
                path_to_layers[path].append(layer.name)
                path_to_value[path] = token.value
                path_to_final_layer[path] = layer.name

            # Tokens in groups (recursively)
            for group_name, group in layer.token_file.groups.items():
                for token in group.all_tokens(group_name):
                    path = token.path
                    if path in seen_paths:
                        continue
                    seen_paths.add(path)
                    if path not in path_to_layers:
                        path_to_layers[path] = []
                    path_to_layers[path].append(layer.name)
                    path_to_value[path] = token.value
                    path_to_final_layer[path] = layer.name

        # Find conflicts (tokens in multiple layers)
        for path, layer_list in path_to_layers.items():
            if len(layer_list) > 1:
                conflicts.append(
                    DTCGConflict(
                        path=path,
                        layers=layer_list,
                        final_layer=path_to_final_layer[path],
                        final_value=path_to_value[path],
                    )
                )

        return conflicts
