"""
Field options loader and validators.

Loads valid field values from field_options.yaml (and split files) and provides
runtime validation functions for Pydantic schemas.

This is the ONLY place that reads field options - all validation
should go through these utilities.

Supports two loading modes:
1. Legacy: Single field_options.yaml file
2. Split: Multiple files in config/field_options/ directory (merged)

Usage:
    from lightwave.schema.pydantic.core.validators.field_options import (
        FieldOptions,
        validate_field_option,
    )

    # Get all valid values for a field
    statuses = FieldOptions.get_values("page_statuses")
    # ['draft', 'published', 'archived']

    # Get default value
    default = FieldOptions.get_default("page_statuses")
    # 'draft'

    # Validate a value
    validate_field_option("page_statuses", "published")  # OK
    validate_field_option("page_statuses", "invalid")    # Raises ValueError

    # Get validation report
    report = FieldOptions.get_validation_report()
    print(report.source_files)  # List of files loaded
    print(report.field_count)   # Number of fields
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from functools import lru_cache
from pathlib import Path
from typing import Any

import yaml

from lightwave.schema.core.paths import DEFINITIONS_DIR, get_sst_path

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class FieldOptionsReport:
    """Report on field options loading and validation."""

    source_files: tuple[str, ...]
    field_count: int
    load_mode: str  # "legacy" or "split"
    fields_by_source: dict[str, list[str]] = field(default_factory=dict)
    validation_errors: list[str] = field(default_factory=list)

    @property
    def is_valid(self) -> bool:
        return len(self.validation_errors) == 0


class FieldOptionsError(ValueError):
    """Raised when a field option is invalid."""

    def __init__(self, field_name: str, value: Any, valid_values: list[str]):
        self.field_name = field_name
        self.value = value
        self.valid_values = valid_values
        super().__init__(f"Invalid value '{value}' for field '{field_name}'. Valid options: {valid_values}")


class FieldOptions:
    """
    Loader and accessor for field options from SST YAML.

    All valid string values for choice fields are defined in
    field_options.yaml or split files in config/field_options/.
    This class provides access to those values for runtime validation.

    Loading Strategy:
    1. First, try to load from split files in config/field_options/
    2. If split files exist, merge them into a single dict
    3. If no split files, fall back to legacy field_options.yaml
    4. Cache the result for performance
    """

    # Registry key in __index.yaml
    SST_KEY = "field_options"
    # Alternate location for split files (relative to definitions/)
    SPLIT_DIR = "config/constraints"
    _cache: dict[str, Any] | None = None
    _report: FieldOptionsReport | None = None

    @classmethod
    def get_definitions_path(cls) -> Path:
        """Get path to definitions directory (from centralized resolver)."""
        return DEFINITIONS_DIR

    @classmethod
    def get_split_dir(cls) -> Path:
        """Get path to split field_options directory."""
        return DEFINITIONS_DIR / cls.SPLIT_DIR

    @classmethod
    def get_legacy_path(cls) -> Path:
        """Get path to field_options.yaml (from __index.yaml registry)."""
        return get_sst_path(cls.SST_KEY)

    @classmethod
    def _load_split_files(cls) -> tuple[dict[str, Any], dict[str, list[str]]]:
        """
        Load and merge all split field option files.

        Returns:
            Tuple of (merged_data, fields_by_source)
        """
        split_dir = cls.get_split_dir()
        merged: dict[str, Any] = {}
        fields_by_source: dict[str, list[str]] = {}

        if not split_dir.exists():
            return merged, fields_by_source

        for yaml_file in split_dir.glob("*.yaml"):
            if yaml_file.name.startswith("_"):
                continue  # Skip __index.yaml

            try:
                with open(yaml_file) as f:
                    data = yaml.safe_load(f) or {}

                # Track which fields came from which file
                source_name = yaml_file.name
                fields_by_source[source_name] = []

                for key, value in data.items():
                    if key.startswith("_"):
                        continue  # Skip _meta keys

                    if key in merged:
                        logger.warning(f"Duplicate field '{key}' in {source_name}, overwriting previous definition")

                    merged[key] = value
                    fields_by_source[source_name].append(key)

            except yaml.YAMLError as e:
                logger.error(f"Failed to parse {yaml_file}: {e}")

        return merged, fields_by_source

    @classmethod
    @lru_cache(maxsize=1)
    def load_all(cls) -> dict[str, Any]:
        """
        Load all field options from YAML.

        Strategy:
        1. Check for split files in config/field_options/
        2. If split files exist and have content, use them
        3. Otherwise, fall back to legacy field_options.yaml

        Returns:
            Dictionary of all field option definitions

        Raises:
            FileNotFoundError: If no field options found
        """
        cls.get_definitions_path()
        split_dir = cls.get_split_dir()
        legacy_path = cls.get_legacy_path()

        # Try split files first
        split_data, fields_by_source = cls._load_split_files()

        if split_data:
            # Use split files
            cls._report = FieldOptionsReport(
                source_files=tuple(f"{cls.SPLIT_DIR}/{f}" for f in fields_by_source.keys()),
                field_count=len(split_data),
                load_mode="split",
                fields_by_source=fields_by_source,
            )
            logger.debug(f"Loaded {len(split_data)} fields from split files")
            return split_data

        # Fall back to legacy file
        if not legacy_path.exists():
            raise FileNotFoundError(
                f"No field options found. Expected either:\n"
                f"  - Split files in: {split_dir}\n"
                f"  - Legacy file: {legacy_path}"
            )

        with open(legacy_path) as f:
            data = yaml.safe_load(f)

        # Remove _meta key
        result = {k: v for k, v in data.items() if not k.startswith("_")}

        source_file = str(legacy_path.relative_to(DEFINITIONS_DIR))
        cls._report = FieldOptionsReport(
            source_files=(source_file,),
            field_count=len(result),
            load_mode="legacy",
            fields_by_source={source_file: list(result.keys())},
        )
        logger.debug(f"Loaded {len(result)} fields from legacy file")
        return result

    @classmethod
    def get_validation_report(cls) -> FieldOptionsReport:
        """
        Get a report on the loaded field options.

        Returns:
            FieldOptionsReport with source files, counts, and any errors
        """
        # Ensure data is loaded
        cls.load_all()
        if cls._report is None:
            raise RuntimeError("Field options not loaded")
        return cls._report

    @classmethod
    def get_field_definition(cls, field_name: str) -> dict[str, Any]:
        """
        Get full definition for a field.

        Args:
            field_name: Name of the field (e.g., "page_statuses")

        Returns:
            Dictionary with description, default, and options

        Raises:
            KeyError: If field not found
        """
        data = cls.load_all()
        if field_name not in data:
            raise KeyError(
                f"Field '{field_name}' not found in field_options.yaml. Available fields: {list(data.keys())}"
            )
        return data[field_name]

    @classmethod
    def get_values(cls, field_name: str) -> list[str]:
        """
        Get list of valid values for a field.

        Args:
            field_name: Name of the field (e.g., "page_statuses")

        Returns:
            List of valid string values
        """
        definition = cls.get_field_definition(field_name)
        options = definition.get("options", [])
        return [opt["value"] for opt in options]

    @classmethod
    def get_default(cls, field_name: str) -> str | None:
        """
        Get default value for a field.

        Args:
            field_name: Name of the field

        Returns:
            Default value or None
        """
        definition = cls.get_field_definition(field_name)
        return definition.get("default")

    @classmethod
    def get_options_with_labels(cls, field_name: str) -> list[dict[str, str]]:
        """
        Get options with labels for UI dropdowns.

        Args:
            field_name: Name of the field

        Returns:
            List of dicts with 'value', 'label', and optional 'description'
        """
        definition = cls.get_field_definition(field_name)
        return definition.get("options", [])

    @classmethod
    def is_valid(cls, field_name: str, value: Any) -> bool:
        """
        Check if a value is valid for a field.

        Args:
            field_name: Name of the field
            value: Value to check

        Returns:
            True if valid, False otherwise
        """
        if value is None:
            # Check if null is a valid option
            valid_values = cls.get_values(field_name)
            return None in valid_values or "null" in valid_values
        return value in cls.get_values(field_name)

    @classmethod
    def validate(cls, field_name: str, value: Any) -> str:
        """
        Validate a value and return it if valid.

        Args:
            field_name: Name of the field
            value: Value to validate

        Returns:
            The validated value

        Raises:
            FieldOptionsError: If value is invalid
        """
        valid_values = cls.get_values(field_name)

        # Handle None values
        if value is None:
            if None in valid_values or cls.get_default(field_name) is None:
                return value
            raise FieldOptionsError(field_name, value, valid_values)

        if value not in valid_values:
            raise FieldOptionsError(field_name, value, valid_values)

        return value

    @classmethod
    def get_all_field_names(cls) -> list[str]:
        """Get list of all defined field names."""
        return list(cls.load_all().keys())

    @classmethod
    def clear_cache(cls) -> None:
        """Clear the cached data (useful for testing)."""
        cls.load_all.cache_clear()


def validate_field_option(field_name: str, value: Any) -> str:
    """
    Convenience function to validate a field option value.

    Args:
        field_name: Name of the field (e.g., "page_statuses")
        value: Value to validate

    Returns:
        The validated value

    Raises:
        FieldOptionsError: If value is invalid
    """
    return FieldOptions.validate(field_name, value)


def get_field_values(field_name: str) -> list[str]:
    """
    Convenience function to get valid values for a field.

    Args:
        field_name: Name of the field

    Returns:
        List of valid values
    """
    return FieldOptions.get_values(field_name)


def get_field_default(field_name: str) -> str | None:
    """
    Convenience function to get default value for a field.

    Args:
        field_name: Name of the field

    Returns:
        Default value or None
    """
    return FieldOptions.get_default(field_name)
