"""DTCG reference resolver for {token.path} syntax.

Resolves `{token.path}` references in a merged DTCG TokenFile,
expanding them to their actual values. This happens AFTER layer
merging to allow cross-layer references.
"""

from __future__ import annotations

import copy
from dataclasses import dataclass
from typing import Any, List, Optional, Tuple

from tokenmoulds.dtcg.types import (
    TokenFile,
    TokenGroup,
    TypographyValue,
)


@dataclass
class ReferenceResolution:
    """Records a single reference resolution."""

    path: str  # Path of the token containing the reference
    reference: str  # Original reference string: {color.primary}
    target_path: str  # Path being referenced: color.primary
    resolved_value: Any  # The resolved value
    depth: int  # Resolution depth (for chained references)


class CircularReferenceError(Exception):
    """Raised when circular references are detected."""

    def __init__(self, path: str, chain: List[str]) -> None:
        self.path = path
        self.chain = chain
        super().__init__(
            f"Circular reference detected at '{path}': {' -> '.join(chain)}"
        )


class UnresolvedReferenceError(Exception):
    """Raised when references cannot be resolved."""

    def __init__(self, unresolved: List[Tuple[str, str]]) -> None:
        self.unresolved = unresolved
        paths = [f"'{path}' -> {ref}" for path, ref in unresolved]
        super().__init__(f"Unresolved references after max depth: {', '.join(paths)}")


class DTCGReferenceResolver:
    """Resolves {token.path} references in DTCG TokenFile.

    Unlike internal formula evaluation (${...}), DTCG references are simple
    path lookups without expression evaluation. They use the `{token.path}`
    syntax as defined in the W3C DTCG spec.

    Example:
        {color.primary}  -> Looks up color.primary token
        {inputs.font.body}  -> Looks up inputs.font.body token
    """

    def __init__(self, *, max_depth: int = 10, strict: bool = False) -> None:
        """Initialize resolver.

        Args:
            max_depth: Maximum reference chain depth (for circular detection)
            strict: If True, raise on unresolved references; if False, warn
        """
        self.max_depth = max_depth
        self.strict = strict

    def resolve(
        self,
        token_file: TokenFile,
    ) -> Tuple[TokenFile, List[ReferenceResolution], List[str]]:
        """Resolve all {token.path} references in token file.

        Args:
            token_file: Merged TokenFile with unresolved references

        Returns:
            Tuple of:
            - Resolved TokenFile (deep copy with references expanded)
            - List of ReferenceResolution records
            - List of warning/error messages

        Raises:
            CircularReferenceError: If circular references detected
            UnresolvedReferenceError: If strict=True and references can't resolve
        """
        # Deep copy to avoid mutating input
        resolved = copy.deepcopy(token_file)

        resolutions: List[ReferenceResolution] = []
        messages: List[str] = []

        # Iteratively resolve until no references remain
        depth = 0
        while depth < self.max_depth:
            unresolved = self._find_references(resolved)
            if not unresolved:
                break

            progress = False
            still_unresolved: List[Tuple[str, str]] = []

            for ref_path, ref_value in unresolved:
                target_path = self._extract_reference_path(ref_value)
                if not target_path:
                    still_unresolved.append((ref_path, ref_value))
                    continue

                # Check for circular reference
                if target_path == ref_path:
                    raise CircularReferenceError(ref_path, [ref_path, target_path])

                resolved_value = self._lookup_value(resolved, target_path)

                if resolved_value is not None:
                    # Check if resolved value is also a reference (chain)
                    if self._is_reference(resolved_value):
                        # Leave for next pass
                        still_unresolved.append((ref_path, ref_value))
                        continue

                    self._set_token_value(resolved, ref_path, resolved_value)
                    progress = True

                    resolutions.append(
                        ReferenceResolution(
                            path=ref_path,
                            reference=ref_value,
                            target_path=target_path,
                            resolved_value=resolved_value,
                            depth=depth,
                        )
                    )
                else:
                    still_unresolved.append((ref_path, ref_value))

            if not progress and still_unresolved:
                # No progress made - likely unresolvable or circular
                if self.strict:
                    raise UnresolvedReferenceError(still_unresolved)
                else:
                    for path, ref in still_unresolved:
                        messages.append(
                            f"Warning: Could not resolve reference '{ref}' at '{path}'"
                        )
                    break

            depth += 1

        # Check for any remaining references
        final_unresolved = self._find_references(resolved)
        if final_unresolved:
            for path, ref in final_unresolved:
                messages.append(
                    f"Warning: Unresolved reference after {depth} passes: "
                    f"'{ref}' at '{path}'"
                )

        return resolved, resolutions, messages

    def _find_references(
        self,
        token_file: TokenFile,
    ) -> List[Tuple[str, str]]:
        """Find all {token.path} references in token file.

        Returns:
            List of (token_path, reference_string) tuples
        """
        references: List[Tuple[str, str]] = []

        # Check root-level tokens
        for token_name, token in token_file.tokens.items():
            if self._is_reference(token.value):
                references.append((token_name, token.value))

        # Check groups recursively
        for group_name, group in token_file.groups.items():
            self._find_references_in_group(group, group_name, references)

        return references

    def _find_references_in_group(
        self,
        group: TokenGroup,
        path_prefix: str,
        references: List[Tuple[str, str]],
    ) -> None:
        """Recursively find references in a token group."""
        for token_name, token in group.tokens.items():
            token_path = f"{path_prefix}.{token_name}"
            if self._is_reference(token.value):
                references.append((token_path, token.value))
            # Also check for references inside composite values
            elif isinstance(token.value, TypographyValue):
                self._find_references_in_typography(token.value, token_path, references)

        for group_name, nested_group in group.groups.items():
            self._find_references_in_group(
                nested_group, f"{path_prefix}.{group_name}", references
            )

    def _find_references_in_typography(
        self,
        typo: TypographyValue,
        path_prefix: str,
        references: List[Tuple[str, str]],
    ) -> None:
        """Find references inside TypographyValue composite."""
        # Check each field that could be a reference
        for field_name in (
            "fontFamily",
            "fontSize",
            "fontWeight",
            "letterSpacing",
            "lineHeight",
        ):
            value = getattr(typo, field_name, None)
            if value is not None and self._is_reference(value):
                references.append((f"{path_prefix}.{field_name}", value))

    def _is_reference(self, value: Any) -> bool:
        """Check if a value is a DTCG reference."""
        if isinstance(value, str):
            v = value.strip()
            return v.startswith("{") and v.endswith("}") and not v.startswith("${")
        return False

    def _extract_reference_path(self, ref: str) -> Optional[str]:
        """Extract token path from reference string.

        Args:
            ref: Reference string like "{color.primary}"

        Returns:
            Token path like "color.primary", or None if invalid
        """
        if not self._is_reference(ref):
            return None
        # Strip braces
        return ref.strip()[1:-1].strip()

    def _lookup_value(self, token_file: TokenFile, path: str) -> Any:
        """Look up a token value by path.

        Args:
            token_file: TokenFile to search
            path: Dot-separated path like "color.primary.base"

        Returns:
            Token value if found, None otherwise
        """
        token = token_file.get_token(path)
        if token is not None:
            return token.value
        return None

    def _set_token_value(
        self,
        token_file: TokenFile,
        path: str,
        value: Any,
    ) -> None:
        """Set a token value by path.

        Args:
            token_file: TokenFile to modify
            path: Dot-separated path
            value: New value to set
        """
        parts = path.split(".")

        if len(parts) == 1:
            # Root-level token
            if parts[0] in token_file.tokens:
                token_file.tokens[parts[0]].value = value
            return

        # Navigate to containing group
        current: Any = token_file
        for i, part in enumerate(parts[:-1]):
            if isinstance(current, TokenFile):
                if part in current.groups:
                    current = current.groups[part]
                else:
                    return  # Path not found
            elif isinstance(current, TokenGroup):
                if part in current.groups:
                    current = current.groups[part]
                elif part in current.tokens:
                    # This shouldn't happen for intermediate parts
                    return
                else:
                    return

        # Set the token value
        token_name = parts[-1]
        if isinstance(current, TokenGroup) and token_name in current.tokens:
            current.tokens[token_name].value = value


def resolve_references(
    token_file: TokenFile,
    *,
    max_depth: int = 10,
    strict: bool = False,
) -> Tuple[TokenFile, List[ReferenceResolution], List[str]]:
    """Convenience function to resolve DTCG references.

    Args:
        token_file: TokenFile with unresolved references
        max_depth: Maximum reference chain depth
        strict: If True, raise on unresolved; if False, warn

    Returns:
        Tuple of (resolved_file, resolutions, messages)
    """
    resolver = DTCGReferenceResolver(max_depth=max_depth, strict=strict)
    return resolver.resolve(token_file)
