"""
Contract Validator Class - Runtime validation from contract artifacts.

This module provides a Validator class that can be instantiated with contract
artifacts (schema, coercion rules, validation rules) and performs validation
at runtime without code generation.
"""

from __future__ import annotations

import time
from pathlib import Path
from typing import TYPE_CHECKING, Any, Type

import yaml
from pydantic import BaseModel

from pycharter.contract_parser import ContractMetadata, parse_contract_file
from pycharter.metadata_store import MetadataStoreClient
from pycharter.pydantic_generator import from_dict
from pycharter.runtime_validator.utils import merge_rules_into_schema
from pycharter.runtime_validator.validator_core import (
    ValidationResult,
    validate,
    validate_batch,
)
from pycharter.utils.value_injector import resolve_values

if TYPE_CHECKING:
    from pycharter.quality.tracking import MetricsCollector


class Validator:
    """
    Generic Validator that performs validation from contract artifacts.

    This class can be instantiated with contract files (schema.yaml, coercion_rules.yaml,
    validation_rules.yaml) or a contract directory, and then used to validate data.

    Example:
        >>> from pycharter.runtime_validator import Validator
        >>>
        >>> # Initialize with contract directory
        >>> validator = Validator(
        ...     contract_dir="data/examples/fmp_stock_list"
        ... )
        >>>
        >>> # Validate single record
        >>> result = validator.validate({"symbol": "AAPL", "company_name": "Apple Inc."})
        >>> if result.is_valid:
        ...     print(f"Valid: {result.data}")
        >>>
        >>> # Validate batch
        >>> results = validator.validate_batch([record1, record2, record3])
        >>> valid_count = sum(1 for r in results if r.is_valid)
        >>>
        >>> # Using builder pattern
        >>> from pycharter.runtime_validator import ValidatorBuilder
        >>> validator = (
        ...     ValidatorBuilder()
        ...     .from_directory("data/contracts/user")
        ...     .strict()
        ...     .with_quality_checks()
        ...     .build()
        ... )
    """

    def __init__(
        self,
        contract_dir: str | None = None,
        contract_file: str | None = None,
        contract_dict: dict[str, Any] | None = None,
        contract_metadata: ContractMetadata | None = None,
        store: MetadataStoreClient | None = None,
        contract_name: str | None = None,
        contract_version: str | None = None,
    ):
        """
        Initialize the validator with contract artifacts.

        This is the primary and recommended way to perform validation in pycharter.
        The Validator class handles loading contracts from various sources and generates
        Pydantic models for validation.

        Args:
            contract_dir: Directory containing contract files (schema.yaml, coercion_rules.yaml, validation_rules.yaml)
            contract_file: Path to complete contract file (YAML/JSON)
            contract_dict: Contract as dictionary with 'schema', 'coercion_rules', 'validation_rules' keys
            contract_metadata: ContractMetadata object (from parse_contract)
            store: MetadataStoreClient instance (for loading from metadata store)
            contract_name: Data contract name (required when using store)
            contract_version: Data contract version (required when using store)

        Example:
            >>> # From contract directory
            >>> validator = Validator(contract_dir="data/contracts/user")
            >>> result = validator.validate({"name": "Alice", "age": 30})

            >>> # From metadata store
            >>> store = SQLiteMetadataStore("metadata.db")
            >>> validator = Validator(store=store, contract_name="user_schema", contract_version="1.0")
            >>> result = validator.validate({"name": "Alice", "age": 30})
        """
        self.schema = None
        self.coercion_rules = {}
        self.validation_rules = {}
        self.model: Type[BaseModel] | None = None
        self._schema_from_store = (
            False  # Flag to track if schema came from store (already merged)
        )

        # Builder configuration options
        self._strict_mode: bool = False
        self._include_quality: bool = False
        self._quality_thresholds: dict[str, float] | None = None
        self._metrics_collector: "MetricsCollector" | None = None
        self._schema_name: str | None = None
        self._schema_version: str = "1.0.0"

        # Load from various sources (order matters - store takes precedence)
        if store and contract_name and contract_version:
            self._load_from_store(store, contract_name, contract_version)
        elif contract_metadata:
            self._load_from_metadata(contract_metadata)
        elif contract_dict:
            self._load_from_dict(contract_dict)
        elif contract_file:
            self._load_from_file(Path(contract_file))
        elif contract_dir:
            self._load_from_directory(Path(contract_dir))
        else:
            raise ValueError(
                "Must provide one of: contract_dir, contract_file, contract_dict, "
                "contract_metadata, or (store + contract_name + contract_version)"
            )

        # Generate Pydantic model from complete schema
        self._generate_model()

    # =========================================================================
    # FACTORY METHODS (Clean API)
    # =========================================================================

    @classmethod
    def from_files(
        cls,
        schema: str,
        coercion_rules: str | None = None,
        validation_rules: str | None = None,
    ) -> "Validator":
        """
        Create validator from explicit file paths.

        Use any file names you want - no assumptions about naming conventions.

        Args:
            schema: Path to schema file (YAML or JSON)
            coercion_rules: Optional path to coercion rules file
            validation_rules: Optional path to validation rules file

        Returns:
            Configured Validator instance

        Example:
            validator = Validator.from_files(
                schema="my_user_schema.yaml",
                coercion_rules="type_conversions.yaml",
                validation_rules="business_rules.yaml"
            )
        """
        schema_path = Path(schema)
        if not schema_path.exists():
            raise FileNotFoundError(f"Schema file not found: {schema_path}")

        with open(schema_path) as f:
            schema_data = yaml.safe_load(f)

        coercion_data = {}
        if coercion_rules:
            coercion_path = Path(coercion_rules)
            if coercion_path.exists():
                with open(coercion_path) as f:
                    coercion_data = yaml.safe_load(f) or {}

        validation_data = {}
        if validation_rules:
            validation_path = Path(validation_rules)
            if validation_path.exists():
                with open(validation_path) as f:
                    validation_data = yaml.safe_load(f) or {}

        return cls(
            contract_dict={
                "schema": schema_data,
                "coercion_rules": coercion_data,
                "validation_rules": validation_data,
            }
        )

    @classmethod
    def from_dir(
        cls,
        directory: str,
    ) -> "Validator":
        """
        Create validator from a directory containing config files.

        Expects files with standard names:
        - schema.yaml (required)
        - coercion_rules.yaml (optional)
        - validation_rules.yaml (optional)

        Args:
            directory: Path to directory containing config files

        Returns:
            Configured Validator instance

        Example:
            validator = Validator.from_dir("contracts/users/")
        """
        return cls(contract_dir=directory)

    @classmethod
    def from_dict(
        cls,
        schema: dict[str, Any],
        coercion_rules: dict[str, Any] | None = None,
        validation_rules: dict[str, Any] | None = None,
    ) -> "Validator":
        """
        Create validator from dictionaries.

        Args:
            schema: Schema dictionary (JSON Schema format)
            coercion_rules: Optional coercion rules dictionary
            validation_rules: Optional validation rules dictionary

        Returns:
            Configured Validator instance

        Example:
            validator = Validator.from_dict(
                schema={
                    "type": "object",
                    "properties": {
                        "name": {"type": "string"},
                        "age": {"type": "integer"}
                    }
                },
                coercion_rules={"age": "int"},
                validation_rules={"age": {"min": 0, "max": 150}}
            )
        """
        return cls(
            contract_dict={
                "schema": schema,
                "coercion_rules": coercion_rules or {},
                "validation_rules": validation_rules or {},
            }
        )

    @classmethod
    def from_file(
        cls,
        path: str,
    ) -> "Validator":
        """
        Create validator from a single contract file.

        The file should contain all sections (schema, coercion_rules, validation_rules).

        Args:
            path: Path to contract file (YAML or JSON)

        Returns:
            Configured Validator instance

        Example:
            validator = Validator.from_file("contracts/user_contract.yaml")
        """
        return cls(contract_file=path)

    # =========================================================================
    # INTERNAL LOADING METHODS
    # =========================================================================

    def _load_from_store(
        self, store: MetadataStoreClient, contract_name: str, contract_version: str
    ) -> None:
        """
        Load contract from metadata store by contract name and version.

        Args:
            store: MetadataStoreClient instance
            contract_name: Data contract name
            contract_version: Data contract version

        Raises:
            ValueError: If schema not found in store
        """
        raw_schema = store.get_schema(contract_name, contract_version)
        if not raw_schema:
            raise ValueError(
                f"Contract {contract_name!r} @ {contract_version!r} not found in store"
            )
        coercion_rules = store.get_coercion_rules(contract_name, contract_version)
        validation_rules = store.get_validation_rules(contract_name, contract_version)

        # Store raw components - merge will happen in _merge_rules_into_schema()
        self.schema = raw_schema
        self.coercion_rules = (
            self._extract_rules(coercion_rules) if coercion_rules else {}
        )
        self.validation_rules = (
            self._extract_rules(validation_rules) if validation_rules else {}
        )
        self._schema_from_store = False  # Let internal merge happen

    def _load_from_metadata(self, metadata: ContractMetadata) -> None:
        """
        Load contract from ContractMetadata object.

        Args:
            metadata: ContractMetadata object containing schema and rules
        """
        self.schema = metadata.schema
        self.coercion_rules = metadata.coercion_rules or {}
        self.validation_rules = metadata.validation_rules or {}

    def _load_from_dict(self, contract: dict[str, Any]) -> None:
        """
        Load contract from dictionary.

        Args:
            contract: Dictionary with 'schema', 'coercion_rules', 'validation_rules' keys

        Raises:
            ValueError: If 'schema' key is missing
        """
        self.schema = contract.get("schema")
        if not self.schema:
            raise ValueError("Contract dictionary must contain 'schema' key")

        self.coercion_rules = self._extract_rules(contract.get("coercion_rules", {}))
        self.validation_rules = self._extract_rules(
            contract.get("validation_rules", {})
        )

    @staticmethod
    def _extract_rules(rules_data: Any) -> dict[str, Any]:
        """
        Extract rules from various formats.

        Handles:
        - Direct rules dict: {"field1": "rule1", "field2": "rule2"}
        - Wrapped rules dict: {"rules": {"field1": "rule1"}}
        - Metadata dict with rules: {"version": "1.0", "rules": {...}}

        Args:
            rules_data: Rules data in various formats

        Returns:
            Dictionary of field_name -> rule mappings
        """
        if not isinstance(rules_data, dict):
            return {}

        # If "rules" key exists, extract it
        if "rules" in rules_data:
            return rules_data["rules"]

        # If it looks like a metadata dict (has version/description/title), return empty
        # Otherwise, assume it's a direct rules dict
        metadata_keys = {"version", "description", "title"}
        if metadata_keys.intersection(rules_data.keys()):
            return {}

        return rules_data

    def _load_from_file(self, file_path: Path) -> None:
        """
        Load contract from file.

        Args:
            file_path: Path to contract file (YAML/JSON)

        Raises:
            FileNotFoundError: If file doesn't exist
            ValueError: If file cannot be parsed
        """
        contract_metadata = parse_contract_file(str(file_path))
        self._load_from_metadata(contract_metadata)

    def _load_from_directory(self, contract_dir: Path):
        """
        Load contract components from directory.

        Expected files:
        - schema.yaml (required)
        - coercion_rules.yaml (optional)
        - validation_rules.yaml (optional)

        Args:
            contract_dir: Path to contract directory

        Raises:
            ValueError: If directory or schema file doesn't exist
        """
        if not contract_dir.exists():
            raise ValueError(f"Contract directory not found: {contract_dir}")

        # Load schema (required)
        schema_path = contract_dir / "schema.yaml"
        if not schema_path.exists():
            raise ValueError(f"Schema file not found: {schema_path}")

        self.schema = self._load_yaml(schema_path)

        # Load coercion rules (optional)
        coercion_path = contract_dir / "coercion_rules.yaml"
        if coercion_path.exists():
            coercion_data = self._load_yaml(coercion_path)
            self.coercion_rules = self._extract_rules(coercion_data)

        # Load validation rules (optional)
        validation_path = contract_dir / "validation_rules.yaml"
        if validation_path.exists():
            validation_data = self._load_yaml(validation_path)
            self.validation_rules = self._extract_rules(validation_data)

    def _load_yaml(self, file_path: Path) -> dict[str, Any]:
        """
        Load and parse YAML file with variable substitution.

        Args:
            file_path: Path to YAML file

        Returns:
            Parsed YAML data as dictionary
        """
        with open(file_path, "r", encoding="utf-8") as f:
            data = yaml.safe_load(f) or {}

        # Resolve variable substitutions if data exists
        if data:
            data = resolve_values(data, source_file=str(file_path))

        return data

    def _merge_rules_into_schema(self) -> dict[str, Any]:
        """
        Merge coercion and validation rules into schema.

        Returns:
            Complete schema with rules merged

        Raises:
            ValueError: If schema not loaded
        """
        if not self.schema:
            raise ValueError("Schema not loaded. Check contract loading.")

        # If schema came from store, it's already merged
        if self._schema_from_store:
            return self.schema

        return merge_rules_into_schema(
            self.schema, self.coercion_rules, self.validation_rules
        )

    def _generate_model(self) -> None:
        """
        Generate Pydantic model from complete schema.

        Raises:
            ValueError: If schema cannot be converted to model
        """
        complete_schema = self._merge_rules_into_schema()
        model_name = complete_schema.get("title", "DynamicModel")
        self.model = from_dict(complete_schema, model_name)

        # Store schema name and version for metrics tracking
        self._schema_name = model_name
        if "version" in complete_schema:
            self._schema_version = complete_schema["version"]

    def validate(
        self,
        data: dict[str, Any],
        strict: bool | None = None,
        include_quality: bool | None = None,
    ) -> ValidationResult:
        """
        Validate a single data record against the contract.

        Args:
            data: Data dictionary to validate
            strict: If True, raise exceptions on validation errors.
                   Defaults to builder setting or False.
            include_quality: If True, include quality metrics in result.
                            Defaults to builder setting or False.

        Returns:
            ValidationResult object

        Example:
            >>> validator = Validator(contract_dir="data/examples/fmp_stock_list")
            >>> result = validator.validate({"symbol": "AAPL", "company_name": "Apple Inc."})
            >>> if result.is_valid:
            ...     print(f"Valid: {result.data.symbol}")
        """
        if not self.model:
            raise ValueError("Model not initialized. Check contract loading.")

        # Use builder settings as defaults
        use_strict = strict if strict is not None else self._strict_mode
        use_quality = (
            include_quality if include_quality is not None else self._include_quality
        )

        # Track timing for metrics
        start_time = time.perf_counter()
        result = validate(self.model, data, strict=use_strict)
        duration_ms = (time.perf_counter() - start_time) * 1000

        # Add quality metrics if requested
        if use_quality:
            result.quality = self._compute_quality_metrics([data], [result])

        # Record metrics if collector is configured
        if self._metrics_collector:
            self._record_metrics(result, duration_ms)

        return result

    def validate_batch(
        self,
        data_list: list[dict[str, Any]],
        strict: bool | None = None,
        include_quality: bool | None = None,
    ) -> list[ValidationResult]:
        """
        Validate a batch of data records against the contract.

        Args:
            data_list: List of data dictionaries to validate
            strict: If True, stop on first validation error.
                   Defaults to builder setting or False.
            include_quality: If True, include quality metrics in results.
                            Defaults to builder setting or False.

        Returns:
            List of ValidationResult objects

        Example:
            >>> validator = Validator(contract_dir="data/examples/fmp_stock_list")
            >>> results = validator.validate_batch([record1, record2, record3])
            >>> valid_count = sum(1 for r in results if r.is_valid)
            >>> invalid_count = sum(1 for r in results if not r.is_valid)
        """
        if not self.model:
            raise ValueError("Model not initialized. Check contract loading.")

        # Use builder settings as defaults
        use_strict = strict if strict is not None else self._strict_mode
        use_quality = (
            include_quality if include_quality is not None else self._include_quality
        )

        # Track timing for metrics
        start_time = time.perf_counter()
        results = validate_batch(self.model, data_list, strict=use_strict)
        duration_ms = (time.perf_counter() - start_time) * 1000

        # Add quality metrics if requested
        if use_quality:
            quality = self._compute_quality_metrics(data_list, results)
            # Add to last result for batch summary
            if results:
                results[-1].quality = quality

        # Record metrics if collector is configured
        if self._metrics_collector:
            self._record_batch_metrics(results, duration_ms)

        return results

    def validate_for_state(
        self,
        data: dict[str, Any],
        state: str,
        state_rules: dict[str, Any] | None = None,
    ) -> ValidationResult:
        """Validate data with state-specific rule overrides.

        Applies state-specific ``required`` / ``optional`` overrides on top of
        the base schema.  If the given *state* is not found in *state_rules*
        (or *state_rules* is ``None``), falls back to standard ``validate()``.

        Args:
            data: Data dictionary to validate.
            state: Current state value (e.g. ``"DRAFT"``, ``"FILLED"``).
            state_rules: Mapping from state names to override dicts.  Each
                override may contain ``"optional"`` (list of field names
                removed from ``required``) and/or ``"required"`` (list of
                field names added to ``required``).

                Example::

                    {
                        "DRAFT": {"optional": ["filled_qty", "fill_price"]},
                        "FILLED": {"required": ["filled_qty", "fill_price"]},
                    }

        Returns:
            ValidationResult (identical format to ``validate()``).

        Example:
            >>> rules = {"DRAFT": {"optional": ["filled_qty"]}}
            >>> result = validator.validate_for_state(order, "DRAFT", state_rules=rules)
        """
        # Resolve state_rules: explicit param > instance attr > None
        rules = (
            state_rules
            if state_rules is not None
            else getattr(self, "_state_rules", None)
        )

        if not rules or state not in rules:
            return self.validate(data)

        if not self.schema:
            raise ValueError("Schema not loaded. Check contract loading.")

        import copy

        overrides = rules[state]
        schema_copy = copy.deepcopy(self._merge_rules_into_schema())

        # Apply overrides to the "required" list in the schema
        current_required = list(schema_copy.get("required", []))

        for field_name in overrides.get("optional", []):
            if field_name in current_required:
                current_required.remove(field_name)

        for field_name in overrides.get("required", []):
            if field_name not in current_required:
                current_required.append(field_name)

        schema_copy["required"] = current_required

        # Generate a temporary model from the modified schema
        model_name = schema_copy.get("title", "DynamicModel") + f"_{state}"
        temp_model = from_dict(schema_copy, model_name)

        return validate(temp_model, data, strict=self._strict_mode)

    def _compute_quality_metrics(
        self,
        data_list: list[dict[str, Any]],
        results: list[ValidationResult],
    ) -> "QualityMetrics":
        """
        Compute quality metrics for validated data.

        Args:
            data_list: Original data records
            results: Validation results

        Returns:
            QualityMetrics object
        """
        from pycharter.runtime_validator.validator_core import QualityMetrics

        if not data_list:
            return QualityMetrics()

        # Count valid/invalid
        valid_count = sum(1 for r in results if r.is_valid)
        error_count = len(results) - valid_count

        # Compute field completeness
        field_completeness: dict[str, float | None] = {}
        all_fields: set = set()

        for data in data_list:
            all_fields.update(data.keys())

        for field_name in all_fields:
            non_null_count = sum(
                1 for data in data_list if data.get(field_name) is not None
            )
            field_completeness[field_name] = non_null_count / len(data_list)

        # Compute overall completeness
        if field_completeness:
            completeness = sum(field_completeness.values()) / len(field_completeness)
        else:
            completeness = 1.0

        return QualityMetrics(
            completeness=completeness,
            field_completeness=field_completeness,
            record_count=len(data_list),
            valid_count=valid_count,
            error_count=error_count,
        )

    def get_model(self) -> Type[BaseModel]:
        """
        Get the generated Pydantic model.

        Returns:
            Pydantic model class

        Example:
            >>> validator = Validator(contract_dir="data/examples/fmp_stock_list")
            >>> Model = validator.get_model()
            >>> # Use model directly
            >>> instance = Model(symbol="AAPL", company_name="Apple Inc.")
        """
        if not self.model:
            raise ValueError("Model not initialized. Check contract loading.")

        return self.model

    def get_schema(self) -> dict[str, Any]:
        """
        Get the complete schema (with rules merged).

        Returns:
            Complete schema dictionary
        """
        return self._merge_rules_into_schema()

    def _record_metrics(self, result: ValidationResult, duration_ms: float) -> None:
        """
        Record metrics for a single validation.

        Args:
            result: Validation result
            duration_ms: Validation duration in milliseconds
        """
        if not self._metrics_collector:
            return

        self._metrics_collector.record(
            result,
            schema_name=self._schema_name or "unknown",
            version=self._schema_version,
            duration_ms=duration_ms,
        )

    def _record_batch_metrics(
        self, results: list[ValidationResult], duration_ms: float
    ) -> None:
        """
        Record metrics for a batch validation.

        Args:
            results: List of validation results
            duration_ms: Total validation duration in milliseconds
        """
        if not self._metrics_collector:
            return

        self._metrics_collector.record_batch(
            results,
            schema_name=self._schema_name or "unknown",
            version=self._schema_version,
            duration_ms=duration_ms,
        )


# Convenience function for easy import
def create_validator(
    contract_dir: str | None = None,
    **kwargs,
) -> Validator:
    """
    Create a Validator instance.

    Args:
        contract_dir: Directory containing contract files
        **kwargs: Additional arguments passed to Validator

    Returns:
        Validator instance

    Example:
        >>> from pycharter.runtime_validator import create_validator
        >>> validator = create_validator("data/examples/fmp_stock_list")
        >>> result = validator.validate({"symbol": "AAPL"})
    """
    return Validator(contract_dir=contract_dir, **kwargs)
