"""Layer schema validation for token files.

Validates token layer files against JSON Schema before merging,
ensuring type safety and structural correctness.
"""

from __future__ import annotations

import json
import logging
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Union

from tokenmoulds.dtcg.errors import LayerValidationError

logger = logging.getLogger(__name__)

_SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$")

# Try to import jsonschema for validation
try:
    from jsonschema import Draft202012Validator as _Draft202012Validator

    HAS_JSONSCHEMA = True
except ImportError:
    _Draft202012Validator = None
    HAS_JSONSCHEMA = False
    logger.warning("jsonschema not installed - layer validation will be limited")


# Default schema path relative to repo root
DEFAULT_SCHEMA_PATH = (
    Path(__file__).parent.parent.parent.parent / "schemas" / "token-layer.schema.json"
)


@dataclass
class ValidationIssue:
    """A single validation issue found in a layer."""

    path: str
    message: str
    severity: str = "error"  # error, warning, info
    value: Any = None
    schema_path: Optional[str] = None

    def to_dict(self) -> Dict[str, Any]:
        return {
            "path": self.path,
            "message": self.message,
            "severity": self.severity,
            "value": self.value,
            "schema_path": self.schema_path,
        }


@dataclass
class LayerValidationResult:
    """Result of validating a token layer."""

    layer_name: str
    is_valid: bool
    issues: List[ValidationIssue] = field(default_factory=list)
    schema_version: Optional[str] = None

    @property
    def errors(self) -> List[ValidationIssue]:
        return [i for i in self.issues if i.severity == "error"]

    @property
    def warnings(self) -> List[ValidationIssue]:
        return [i for i in self.issues if i.severity == "warning"]

    def to_dict(self) -> Dict[str, Any]:
        return {
            "layer_name": self.layer_name,
            "is_valid": self.is_valid,
            "issues": [i.to_dict() for i in self.issues],
            "schema_version": self.schema_version,
            "summary": {
                "total_issues": len(self.issues),
                "errors": len(self.errors),
                "warnings": len(self.warnings),
            },
        }


class LayerValidator:
    """Validates token layers against JSON Schema.

    Provides schema-based validation for token layer files to ensure:
    - Type correctness (strings, numbers, booleans)
    - Required fields are present
    - Values are within valid ranges
    - Color formats are valid
    - Formula syntax is correct

    Usage:
        validator = LayerValidator()
        result = validator.validate(layer_payload, layer_name="org")
        if not result.is_valid:
            for error in result.errors:
                print(f"{error.path}: {error.message}")
    """

    def __init__(
        self,
        schema_path: Optional[Union[str, Path]] = None,
        *,
        strict: bool = False,
        custom_rules: Optional[List[Callable[..., Any]]] = None,
    ):
        """Initialize layer validator.

        Args:
            schema_path: Path to JSON Schema file (uses default if not provided)
            strict: If True, treat warnings as errors
            custom_rules: Additional validation functions
        """
        self.strict = strict
        self.custom_rules = custom_rules or []
        self._schema = None
        self._validator = None

        # Load schema
        schema_path = Path(schema_path) if schema_path else DEFAULT_SCHEMA_PATH
        self._load_schema(schema_path)

    def _load_schema(self, schema_path: Path) -> None:
        """Load JSON Schema from file."""
        if not schema_path.exists():
            logger.warning(f"Schema file not found: {schema_path}")
            return

        try:
            with open(schema_path, "r", encoding="utf-8") as f:
                self._schema = json.load(f)

            if HAS_JSONSCHEMA and _Draft202012Validator is not None:
                self._validator = _Draft202012Validator(self._schema)
                logger.debug(f"Loaded schema from {schema_path}")
            else:
                logger.warning("jsonschema not available - using basic validation only")

        except json.JSONDecodeError as e:
            logger.error(f"Failed to parse schema: {e}")
        except Exception as e:
            logger.error(f"Failed to load schema: {e}")

    def add_rule(self, rule: Callable[..., Any]) -> None:
        """Add a custom validation rule.

        Args:
            rule: A callable that takes (payload, layer_name) and returns
                  a list of ValidationIssue objects
        """
        self.custom_rules.append(rule)

    @property
    def has_schema(self) -> bool:
        """Check if schema is loaded and available."""
        return self._schema is not None

    @property
    def has_full_validation(self) -> bool:
        """Check if full JSON Schema validation is available."""
        return HAS_JSONSCHEMA and self._validator is not None

    def validate(
        self,
        payload: Mapping[str, Any],
        layer_name: str = "unknown",
    ) -> LayerValidationResult:
        """Validate a token layer payload.

        Args:
            payload: Token layer data to validate
            layer_name: Name of the layer (for error messages)

        Returns:
            LayerValidationResult with validation status and issues
        """
        issues: List[ValidationIssue] = []

        # Run JSON Schema validation if available
        if self.has_full_validation:
            schema_issues = self._validate_schema(payload, layer_name)
            issues.extend(schema_issues)

        # Run basic structural validation
        structural_issues = self._validate_structure(payload, layer_name)
        issues.extend(structural_issues)

        # Run custom validation rules
        for rule in self.custom_rules:
            try:
                custom_issues = rule(payload, layer_name)
                if custom_issues:
                    issues.extend(custom_issues)
            except Exception as e:
                logger.warning(f"Custom validation rule failed: {e}")

        # Determine validity
        errors = [i for i in issues if i.severity == "error"]
        is_valid = len(errors) == 0

        if self.strict:
            warnings = [i for i in issues if i.severity == "warning"]
            is_valid = is_valid and len(warnings) == 0

        return LayerValidationResult(
            layer_name=layer_name,
            is_valid=is_valid,
            issues=issues,
            schema_version=self._schema.get("$id") if self._schema else None,
        )

    def _validate_schema(
        self,
        payload: Mapping[str, Any],
        layer_name: str,
    ) -> List[ValidationIssue]:
        """Validate against JSON Schema."""
        issues = []

        if not self._validator:
            return issues

        try:
            errors = list(self._validator.iter_errors(dict(payload)))
            for error in errors:
                path = ".".join(str(p) for p in error.absolute_path) or "(root)"
                issues.append(
                    ValidationIssue(
                        path=path,
                        message=error.message,
                        severity="error",
                        value=error.instance,
                        schema_path=".".join(str(p) for p in error.schema_path),
                    )
                )
        except Exception as e:
            logger.warning(f"Schema validation error: {e}")
            issues.append(
                ValidationIssue(
                    path="(schema)",
                    message=f"Schema validation failed: {e}",
                    severity="warning",
                )
            )

        return issues

    # Keys whose values should be validated as hex colors.
    _COLOR_KEYS = frozenset(
        {
            "primary",
            "secondary",
            "accent",
            "neutral",
            "background",
            "foreground",
        }
    )

    # Numeric keys with (min, max) bounds.
    _RANGE_CHECKS: Dict[str, tuple] = {
        "brand_tone": (0.0, 1.0),
        "content_density": (0.0, 1.0),
        "base_grid_pt": (1, 100),
        "base_grid_emu": (1, 914400 * 10),  # Up to 10 inches
    }

    def _validate_structure(
        self,
        payload: Mapping[str, Any],
        layer_name: str,
    ) -> List[ValidationIssue]:
        """Basic structural validation in a single recursive walk."""
        issues: List[ValidationIssue] = []

        def _check_color(value: str, path: str) -> None:
            if value.startswith("${"):
                return
            if value.startswith("#"):
                if len(value) not in (7, 9):
                    issues.append(
                        ValidationIssue(
                            path=path,
                            message=f"Invalid hex color format: {value}",
                            severity="error",
                            value=value,
                        )
                    )
                elif not all(c in "0123456789ABCDEFabcdef" for c in value[1:]):
                    issues.append(
                        ValidationIssue(
                            path=path,
                            message=f"Invalid hex color characters: {value}",
                            severity="error",
                            value=value,
                        )
                    )

        def _check_formula(value: str, path: str) -> None:
            if "${" not in value:
                return
            depth = 0
            i = 0
            while i < len(value):
                if value[i : i + 2] == "${":
                    depth += 1
                    i += 2
                elif value[i] == "}" and depth > 0:
                    depth -= 1
                    i += 1
                else:
                    i += 1
            if depth != 0:
                issues.append(
                    ValidationIssue(
                        path=path,
                        message=f"Unclosed formula expression ({depth} unclosed): {value}",
                        severity="error",
                        value=value,
                    )
                )

        def walk(data: Any, prefix: str) -> None:
            if isinstance(data, Mapping):
                for key, value in data.items():
                    current_path = f"{prefix}.{key}" if prefix else key
                    if isinstance(value, str):
                        _check_formula(value, current_path)
                        if key.lower() in self._COLOR_KEYS or "color" in key.lower():
                            _check_color(value, current_path)
                    elif isinstance(value, (int, float)) and key in self._RANGE_CHECKS:
                        min_val, max_val = self._RANGE_CHECKS[key]
                        if not (min_val <= value <= max_val):
                            issues.append(
                                ValidationIssue(
                                    path=current_path,
                                    message=f"Value {value} out of range [{min_val}, {max_val}]",
                                    severity="warning",
                                    value=value,
                                )
                            )
                    walk(value, current_path)
            elif isinstance(data, list):
                for idx, value in enumerate(data):
                    item_path = f"{prefix}[{idx}]"
                    if isinstance(value, str):
                        _check_formula(value, item_path)
                    walk(value, item_path)

        walk(payload, "")
        issues.extend(self._validate_layer_metadata(payload, layer_name))
        return issues

    def _validate_layer_metadata(
        self,
        payload: Mapping[str, Any],
        layer_name: str,
    ) -> List[ValidationIssue]:
        """Validate layer metadata consistency."""
        issues = []

        metadata = payload.get("metadata", {})
        if not isinstance(metadata, Mapping):
            return issues

        # Check layer type matches
        declared_layer = metadata.get("layer")
        if declared_layer and declared_layer != layer_name:
            issues.append(
                ValidationIssue(
                    path="metadata.layer",
                    message=f"Declared layer '{declared_layer}' doesn't match actual layer '{layer_name}'",
                    severity="warning",
                    value=declared_layer,
                )
            )

        # Check version format
        version = metadata.get("version")
        if version and not self._is_valid_semver(version):
            issues.append(
                ValidationIssue(
                    path="metadata.version",
                    message=f"Invalid semantic version format: {version}",
                    severity="warning",
                    value=version,
                )
            )

        return issues

    @staticmethod
    def _is_valid_semver(version: str) -> bool:
        """Check if version string is valid semver."""
        return bool(_SEMVER_RE.match(version))


def validate_layers(
    layers: Union[Sequence[tuple], Mapping[str, Any]],
    *,
    schema_path: Optional[Path] = None,
    strict: bool = False,
    raise_on_error: bool = False,
    validator: Optional[LayerValidator] = None,
) -> Dict[str, LayerValidationResult]:
    """Validate multiple token layers.

    Args:
        layers: Either a sequence of (name, payload) tuples or a dict
                mapping layer names to payloads
        schema_path: Custom schema path (ignored if validator provided)
        strict: Treat warnings as errors (ignored if validator provided)
        raise_on_error: Raise LayerValidationError on first error
        validator: Optional pre-configured LayerValidator instance

    Returns:
        Dict mapping layer names to validation results
    """
    if validator is None:
        validator = LayerValidator(schema_path=schema_path, strict=strict)
    results = {}

    # Handle both dict and sequence of tuples
    if isinstance(layers, Mapping):
        layer_items = layers.items()
    else:
        layer_items = layers

    for name, payload in layer_items:
        result = validator.validate(payload, layer_name=name)
        results[name] = result

        if raise_on_error and not result.is_valid:
            raise LayerValidationError(
                layer_name=name,
                errors=[e.to_dict() for e in result.errors],
            )

    return results
