"""DTCG Layered Resolution Adapter.

Merges 5-layer DTCG token hierarchy (core → fork → org → group → personal),
resolves {token.path} references, and converts to internal payload format
for Document IR and emitters.

This adapter implements ADR 017: DTCG Layered Resolution Adapter.

Three-phase pipeline:
1. Merge: Combine layers in precedence order with provenance tracking
2. Resolve: Expand {token.path} references against merged view
3. Convert: Transform to internal payload for build_document_ir()
"""

from __future__ import annotations

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

from tokenmoulds.dtcg.converter import convert_from_dtcg
from tokenmoulds.dtcg.merger import (
    DTCGLayer,
    DTCGMergeAudit,
    DTCGMerger,
    DTCGProvenance,
)
from tokenmoulds.dtcg.parser import parse_tokens
from tokenmoulds.dtcg.reference_resolver import (
    DTCGReferenceResolver,
    ReferenceResolution,
)
from tokenmoulds.dtcg.types import TokenFile


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


@dataclass
class LayeredManifest:
    """Manifest pointing to DTCG layer files.

    This is a non-DTCG format for bundling layer references.

    Example manifest JSON:
    {
        "version": "1.0",
        "brand": "acme",
        "layers": {
            "core": "layers/core.tokens.json",
            "fork": "layers/fork.tokens.json",
            "org": "layers/org.tokens.json"
        }
    }
    """

    version: str
    brand: str
    base_path: Path
    layers: Dict[str, str]  # layer_name -> relative_path

    @classmethod
    def from_file(cls, path: Path) -> LayeredManifest:
        """Load manifest from JSON file.

        Args:
            path: Path to manifest JSON file

        Returns:
            LayeredManifest instance

        Raises:
            FileNotFoundError: If manifest file doesn't exist
            json.JSONDecodeError: If manifest is invalid JSON
            KeyError: If required fields are missing
        """
        with open(path) as f:
            data = json.load(f)

        return cls(
            version=data.get("version", "1.0"),
            brand=data.get("brand", "unknown"),
            base_path=path.parent,
            layers=data["layers"],
        )

    def resolve_layer_paths(self) -> Dict[str, Path]:
        """Resolve relative layer paths to absolute paths.

        Returns:
            Dict mapping layer name to absolute file path
        """
        return {
            name: self.base_path / rel_path for name, rel_path in self.layers.items()
        }


@dataclass
class AdapterResult:
    """Result of layered DTCG resolution.

    Contains both the internal payload for emitters and sidecar
    audit/provenance data for debugging and drift detection.
    """

    # Internal payload for build_document_ir()
    payload: Dict[str, Any]

    # Sidecar data (not embedded in payload)
    merged_dtcg: TokenFile  # Fully merged & resolved DTCG
    provenance: Dict[str, DTCGProvenance]  # Token path → layer source
    merge_audit: DTCGMergeAudit  # Layer merge audit trail
    resolutions: List[ReferenceResolution]  # Reference resolutions

    # Metadata
    layers_used: List[str]
    derived_token_count: int
    input_token_count: int
    messages: List[str] = field(default_factory=list)

    def to_dict(self) -> Dict[str, Any]:
        """Convert result to dictionary for serialization."""
        return {
            "payload": self.payload,
            "layers_used": self.layers_used,
            "input_token_count": self.input_token_count,
            "derived_token_count": self.derived_token_count,
            "provenance": {
                path: {
                    "layer": prov.layer,
                    "source_path": prov.source_path,
                    "overridden_by": prov.overridden_by,
                }
                for path, prov in self.provenance.items()
            },
            "merge_audit": self.merge_audit.to_dict(),
            "resolutions": [
                {
                    "path": r.path,
                    "reference": r.reference,
                    "target_path": r.target_path,
                    "depth": r.depth,
                }
                for r in self.resolutions
            ],
            "messages": self.messages,
        }


class DTCGLayeredAdapter:
    """Adapts layered DTCG files to internal payload format.

    Implements a three-phase pipeline:
    1. Merge: Combine layers in precedence order (core → personal)
    2. Resolve: Expand {token.path} references
    3. Convert: Transform to internal payload

    Usage:
        adapter = DTCGLayeredAdapter()

        # From directory (auto-discover layers)
        result = adapter.adapt_from_directory(Path("data/layers"))

        # From manifest file
        result = adapter.adapt_from_manifest(Path("data/manifest.json"))

        # From explicit layer files
        layers = [
            DTCGLayer("core", parse_tokens(Path("core.tokens.json"))),
            DTCGLayer("org", parse_tokens(Path("org.tokens.json"))),
        ]
        result = adapter.adapt_layers(layers)

        # Use result
        document_ir = build_document_ir(result.payload, org_id, locale)
    """

    def __init__(
        self,
        *,
        detect_conflicts: bool = True,
        strict_references: bool = False,
        max_reference_depth: int = 10,
    ) -> None:
        """Initialize adapter.

        Args:
            detect_conflicts: If True, detect and warn about layer conflicts
            strict_references: If True, raise on unresolved references
            max_reference_depth: Maximum reference chain depth
        """
        self.detect_conflicts = detect_conflicts
        self.strict_references = strict_references
        self.max_reference_depth = max_reference_depth

    def adapt_from_manifest(self, manifest_path: Path) -> AdapterResult:
        """Adapt layers from manifest file.

        Args:
            manifest_path: Path to manifest JSON file

        Returns:
            AdapterResult with internal payload and sidecar data
        """
        manifest = LayeredManifest.from_file(manifest_path)
        layer_paths = manifest.resolve_layer_paths()
        layers = self._load_layers_from_paths(layer_paths)
        return self.adapt_layers(layers)

    def adapt_from_directory(
        self,
        directory: Path,
        *,
        layer_names: Optional[Sequence[str]] = None,
    ) -> AdapterResult:
        """Auto-discover and adapt layers from directory.

        Looks for files matching pattern: {layer}.tokens.json

        Args:
            directory: Directory containing layer files
            layer_names: Optional list of layer names to load
                        (defaults to LAYER_ORDER)

        Returns:
            AdapterResult with internal payload and sidecar data
        """
        layers = self._discover_layers(directory, layer_names)
        return self.adapt_layers(layers)

    def adapt_layers(self, layers: Sequence[DTCGLayer]) -> AdapterResult:
        """Main entry point: merge, resolve, convert.

        Args:
            layers: Sequence of DTCGLayer objects in precedence order

        Returns:
            AdapterResult with internal payload and sidecar data
        """
        messages: List[str] = []

        # Phase 1: Merge layers
        merger = DTCGMerger(detect_conflicts=self.detect_conflicts)
        merge_result = merger.merge(layers)

        messages.extend(merge_result.audit.warnings)
        messages.extend(merge_result.audit.errors)

        # Phase 2: Resolve DTCG references
        resolver = DTCGReferenceResolver(
            max_depth=self.max_reference_depth,
            strict=self.strict_references,
        )
        resolved_file, resolutions, resolve_messages = resolver.resolve(
            merge_result.merged
        )

        messages.extend(resolve_messages)

        # Phase 3: Convert to internal payload
        payload = convert_from_dtcg(resolved_file)

        # Add metadata to payload
        payload["_metadata"] = {
            "source": "dtcg_layered_adapter",
            "layers": [layer.name for layer in layers],
        }

        # Count tokens
        input_count = self._count_inputs(resolved_file)
        derived_count = self._count_derived(resolved_file)

        return AdapterResult(
            payload=payload,
            merged_dtcg=resolved_file,
            provenance=merge_result.provenance,
            merge_audit=merge_result.audit,
            resolutions=resolutions,
            layers_used=[layer.name for layer in layers],
            derived_token_count=derived_count,
            input_token_count=input_count,
            messages=messages,
        )

    def _load_layers_from_paths(
        self,
        layer_paths: Dict[str, Path],
    ) -> List[DTCGLayer]:
        """Load DTCG layer files from paths.

        Args:
            layer_paths: Dict mapping layer name to file path

        Returns:
            List of DTCGLayer objects in precedence order
        """
        layers: List[DTCGLayer] = []

        # Load in precedence order
        for layer_name in LAYER_ORDER:
            if layer_name in layer_paths:
                path = layer_paths[layer_name]
                if path.exists():
                    token_file = parse_tokens(path)
                    layers.append(
                        DTCGLayer(
                            name=layer_name,
                            token_file=token_file,
                            source_path=str(path),
                        )
                    )

        return layers

    def _discover_layers(
        self,
        directory: Path,
        layer_names: Optional[Sequence[str]] = None,
    ) -> List[DTCGLayer]:
        """Auto-discover layer files in directory.

        Looks for: {layer}.tokens.json

        Args:
            directory: Directory to search
            layer_names: Layer names to look for (defaults to LAYER_ORDER)

        Returns:
            List of DTCGLayer objects in precedence order
        """
        names = layer_names if layer_names else LAYER_ORDER
        layers: List[DTCGLayer] = []

        for layer_name in names:
            # Try different file patterns
            patterns = [
                f"{layer_name}.tokens.json",
                f"{layer_name}.json",
            ]

            for pattern in patterns:
                path = directory / pattern
                if path.exists():
                    token_file = parse_tokens(path)
                    layers.append(
                        DTCGLayer(
                            name=layer_name,
                            token_file=token_file,
                            source_path=str(path),
                        )
                    )
                    break

        return layers

    def _count_inputs(self, token_file: TokenFile) -> int:
        """Count tokens in the inputs group.

        Per ADR 017, tokens in 'inputs' group are input tokens.
        """
        inputs_group = token_file.groups.get("inputs")
        if inputs_group is None:
            return 0

        return len(inputs_group.all_tokens("inputs"))

    def _count_derived(self, token_file: TokenFile) -> int:
        """Count tokens outside the inputs group.

        Per ADR 017, tokens outside 'inputs' are derived.
        """
        total = len(token_file.all_tokens())
        input_count = self._count_inputs(token_file)
        return total - input_count


def adapt_dtcg_layers(
    layers_dir: Optional[Path] = None,
    manifest_path: Optional[Path] = None,
    layer_paths: Optional[Dict[str, Path]] = None,
    *,
    detect_conflicts: bool = True,
    strict_references: bool = False,
) -> AdapterResult:
    """Convenience function to adapt DTCG layers.

    Provide one of:
    - layers_dir: Auto-discover layers in directory
    - manifest_path: Load layers from manifest file
    - layer_paths: Explicit dict of layer name → file path

    Args:
        layers_dir: Directory containing layer files
        manifest_path: Path to manifest JSON
        layer_paths: Dict mapping layer name to file path
        detect_conflicts: Detect and warn about layer conflicts
        strict_references: Raise on unresolved references

    Returns:
        AdapterResult with internal payload and sidecar data

    Raises:
        ValueError: If no input source provided
    """
    adapter = DTCGLayeredAdapter(
        detect_conflicts=detect_conflicts,
        strict_references=strict_references,
    )

    if manifest_path is not None:
        return adapter.adapt_from_manifest(manifest_path)
    elif layers_dir is not None:
        return adapter.adapt_from_directory(layers_dir)
    elif layer_paths is not None:
        layers = adapter._load_layers_from_paths(layer_paths)
        return adapter.adapt_layers(layers)
    else:
        raise ValueError(
            "Must provide one of: layers_dir, manifest_path, or layer_paths"
        )
