"""
Compiler for payload-guard.
Compiles shape descriptors into optimized filter functions.
"""

from __future__ import annotations
import re
from typing import Any, Optional
from copy import deepcopy

from .types import (
    FieldConfig,
    ShapeDescriptor,
    CompiledFilter,
    ValidationError,
    ErrorCodes,
)
from .sanitizer import (
    trim_string,
    coerce_primitive,
    validate_type,
    validate_email,
    validate_regex,
)
from .security import is_sensitive


class CompileOptions:
    """Compilation options."""

    def __init__(
        self,
        sensitive: Optional[list[str]] = None,
        dev: bool = False,
        strict: bool = False,
        redact: Optional[callable] = None,
        logger: Optional[callable] = None,
        max_array_length: Optional[int] = None,
        parent_path: str = "",
        errors: Optional[list[ValidationError]] = None,
        collect_errors: bool = False,
    ):
        self.sensitive = sensitive or []
        self.dev = dev
        self.strict = strict
        self.redact = redact
        self.logger = logger
        self.max_array_length = max_array_length
        self.parent_path = parent_path
        self._errors = errors
        self.collect_errors = collect_errors

    def copy(self, **kwargs) -> CompileOptions:
        """Create a copy with updated values."""
        opts = CompileOptions(
            sensitive=self.sensitive,
            dev=self.dev,
            strict=self.strict,
            redact=self.redact,
            logger=self.logger,
            max_array_length=self.max_array_length,
            parent_path=self.parent_path,
            errors=self._errors,
            collect_errors=self.collect_errors,
        )
        for key, value in kwargs.items():
            if hasattr(opts, key):
                setattr(opts, key, value)
        return opts


def _create_error(
    field: str,
    message: str,
    code: str,
    value: Any,
    error_message: Optional[Any] = None,
    error_codes: Optional[ErrorCodes] = None,
) -> ValidationError:
    """Create a validation error with custom message support."""
    # Use custom message if provided
    if callable(error_message):
        message = error_message(value, field)
    elif error_message:
        message = error_message

    # Use custom error code if provided
    code_obj = error_codes or ErrorCodes()
    final_code = getattr(code_obj, code, None) or code

    return ValidationError(field=field, message=message, code=final_code, value=value)


def _compile_field_config(config: FieldConfig, opts: CompileOptions) -> CompiledFilter:
    """Compile a field configuration into a filter function."""
    prim_type = config.type
    default_val = config.default
    field_path = opts.parent_path or "field"

    def filter_fn(value: Any) -> Any:
        val = default_val if value is None else coerce_primitive(prim_type, value)
        error: Optional[ValidationError] = None

        # Required check
        if config.required and val is None:
            error = _create_error(
                field_path,
                f"{field_path} is required",
                "required",
                val,
                config.error_message,
                config.error_codes,
            )
            if opts.collect_errors and opts._errors is not None:
                opts._errors.append(error)
            return default_val

        # Perform validation if value is not None
        if val is not None:
            # Type check in strict mode
            if opts.strict and not validate_type(val, prim_type):
                error = _create_error(
                    field_path,
                    f"{field_path} must be of type {prim_type}",
                    "type",
                    val,
                    config.error_message,
                    config.error_codes,
                )
                if opts.collect_errors and opts._errors is not None:
                    opts._errors.append(error)
                return default_val

            # String validations
            if prim_type == "string" and isinstance(val, str):
                val = trim_string(val)

                if config.min is not None and len(val) < config.min:
                    error = _create_error(
                        field_path,
                        f"{field_path} must be at least {config.min} characters",
                        "min",
                        val,
                        config.error_message,
                        config.error_codes,
                    )
                    if opts.collect_errors and opts._errors is not None:
                        opts._errors.append(error)
                    return default_val

                if config.max is not None and len(val) > config.max:
                    error = _create_error(
                        field_path,
                        f"{field_path} must be at most {config.max} characters",
                        "max",
                        val,
                        config.error_message,
                        config.error_codes,
                    )
                    if opts.collect_errors and opts._errors is not None:
                        opts._errors.append(error)
                    return default_val

                if config.regex and not validate_regex(val, config.regex):
                    error = _create_error(
                        field_path,
                        f"{field_path} does not match required pattern",
                        "pattern",
                        val,
                        config.error_message,
                        config.error_codes,
                    )
                    if opts.collect_errors and opts._errors is not None:
                        opts._errors.append(error)
                    return default_val

                if config.email and not validate_email(val):
                    error = _create_error(
                        field_path,
                        f"{field_path} must be a valid email address",
                        "email",
                        val,
                        config.error_message,
                        config.error_codes,
                    )
                    if opts.collect_errors and opts._errors is not None:
                        opts._errors.append(error)
                    return default_val

            # Number validations
            if prim_type == "number" and isinstance(val, (int, float)):
                if config.min is not None and val < config.min:
                    error = _create_error(
                        field_path,
                        f"{field_path} must be at least {config.min}",
                        "min",
                        val,
                        config.error_message,
                        config.error_codes,
                    )
                    if opts.collect_errors and opts._errors is not None:
                        opts._errors.append(error)
                    return default_val

                if config.max is not None and val > config.max:
                    error = _create_error(
                        field_path,
                        f"{field_path} must be at most {config.max}",
                        "max",
                        val,
                        config.error_message,
                        config.error_codes,
                    )
                    if opts.collect_errors and opts._errors is not None:
                        opts._errors.append(error)
                    return default_val

            # Custom validation
            if config.validate:
                try:
                    if not config.validate(val):
                        error = _create_error(
                            field_path,
                            f"{field_path} failed validation",
                            "custom",
                            val,
                            config.error_message,
                            config.error_codes,
                        )
                        if opts.collect_errors and opts._errors is not None:
                            opts._errors.append(error)
                        return default_val
                except Exception as e:
                    if opts.dev and opts.logger:
                        opts.logger(f"[payload-guard] validation error: {e}", "warn")
                    error = _create_error(
                        field_path,
                        f"{field_path} validation threw an error",
                        "custom",
                        val,
                        config.error_message,
                        config.error_codes,
                    )
                    if opts.collect_errors and opts._errors is not None:
                        opts._errors.append(error)
                    return default_val

            # Transformation
            if config.transform:
                try:
                    val = config.transform(val)
                except Exception as e:
                    if opts.dev and opts.logger:
                        opts.logger(f"[payload-guard] transform error: {e}", "warn")

        return val

    return filter_fn


def _is_field_config(value: Any) -> bool:
    """Check if value is a FieldConfig."""
    if isinstance(value, FieldConfig):
        return True
    if hasattr(value, "config") and hasattr(value, "__is_builder"):
        return True
    return False


def _get_field_config(value: Any) -> FieldConfig:
    """Get FieldConfig from value or builder."""
    if isinstance(value, FieldConfig):
        return value
    if hasattr(value, "config"):
        return value.config
    raise ValueError(f"Cannot get FieldConfig from {type(value)}")


def _is_shape_function(value: Any) -> bool:
    """Check if value is a compiled shape function."""
    return callable(value) and hasattr(value, "_is_compiled_shape")


def compile_shape(
    shape: ShapeDescriptor,
    opts: Optional[CompileOptions] = None,
) -> CompiledFilter:
    """
    Compile a shape descriptor into an optimized filter function.

    Args:
        shape: Shape descriptor to compile
        opts: Compilation options

    Returns:
        Compiled filter function
    """
    if opts is None:
        opts = CompileOptions()

    # If it's a shape function (from build_shape), use its internal compile
    if _is_shape_function(shape):
        return shape

    # Field config or Builder
    if _is_field_config(shape):
        config = _get_field_config(shape)

        # Check for nested shape
        if config.shape is not None:
            # Nested object shape - compile both field validator and nested shape
            field_validator = _compile_field_config(config, opts)
            nested_shape_fn = compile_shape(
                config.shape,
                opts.copy(parent_path=opts.parent_path),
            )

            def combined_filter(value: Any) -> Any:
                validated = field_validator(value)
                if validated is None:
                    return validated
                return nested_shape_fn(validated)

            combined_filter._is_compiled_shape = True  # type: ignore
            return combined_filter

        return _compile_field_config(config, opts)

    # If already a regular function, wrap it
    if callable(shape) and not isinstance(shape, (dict, str)):
        shape._is_compiled_shape = True  # type: ignore
        return shape

    # Primitive type string
    if isinstance(shape, str):
        prim_type = shape

        def primitive_filter(value: Any) -> Any:
            if value is None:
                return value
            coerced = coerce_primitive(prim_type, value)
            if opts.strict and not validate_type(coerced, prim_type):
                raise ValueError(f"Type mismatch: expected {prim_type}, got {type(value)}")
            return trim_string(coerced) if prim_type == "string" else coerced

        primitive_filter._is_compiled_shape = True  # type: ignore
        return primitive_filter

    # Dict shape (nested object)
    if isinstance(shape, dict):
        # Check if it's an array descriptor
        if shape.get("__is_array") is True:
            item_shape = shape.get("item")
            if item_shape is None:
                def empty_array_filter(value: Any) -> Any:
                    return [] if isinstance(value, list) else value
                empty_array_filter._is_compiled_shape = True  # type: ignore
                return empty_array_filter

            # Compile item filter
            item_filter = compile_shape(
                item_shape,
                opts.copy(parent_path=opts.parent_path + "[]"),
            )

            def array_filter(value: Any) -> Any:
                if not isinstance(value, list):
                    if opts.collect_errors and opts._errors is not None:
                        opts._errors.append(
                            ValidationError(
                                field=opts.parent_path or "array",
                                message=f"{opts.parent_path or 'array'} must be an array",
                                code="type",
                                value=value,
                            )
                        )
                    return []

                # Memory safety: limit array length
                arr = value
                if opts.max_array_length and len(arr) > opts.max_array_length:
                    if opts.dev and opts.logger:
                        opts.logger(
                            f"[payload-guard] {opts.parent_path or 'array'}: truncated from {len(arr)} to {opts.max_array_length} items",
                            "warn",
                        )
                    arr = arr[: opts.max_array_length]

                result = []
                for idx, item in enumerate(arr):
                    try:
                        filtered_item = item_filter(item)
                        if filtered_item is not None:
                            result.append(filtered_item)
                    except Exception as e:
                        if opts.dev and opts.logger:
                            opts.logger(f"[payload-guard] array item filter error: {e}", "warn")

                return result

            array_filter._is_compiled_shape = True  # type: ignore
            return array_filter

        field_filters: dict[str, CompiledFilter] = {}

        for key, field_shape in shape.items():
            # Prevent prototype pollution
            if key in ("__proto__", "constructor", "prototype"):
                if opts.dev and opts.logger:
                    opts.logger(f'[payload-guard] skipping unsafe shape key "{key}"', "warn")
                continue

            # Skip sensitive fields
            if is_sensitive(key, opts.sensitive):
                if opts.dev and opts.logger:
                    opts.logger(f'[payload-guard] field "{key}" is sensitive and will be excluded', "warn")
                continue

            field_path = f"{opts.parent_path}.{key}" if opts.parent_path else key

            # Check if it's a FieldConfig with nested shape
            if _is_field_config(field_shape):
                config = _get_field_config(field_shape)
                if config.shape is not None:
                    # Nested shape with field config
                    field_validator = _compile_field_config(
                        config, opts.copy(parent_path=field_path)
                    )
                    nested_shape_fn = compile_shape(
                        config.shape, opts.copy(parent_path=field_path)
                    )

                    def make_combined(fv: CompiledFilter, nsf: CompiledFilter) -> CompiledFilter:
                        def combined(value: Any) -> Any:
                            validated = fv(value)
                            if validated is None:
                                return validated
                            return nsf(validated)

                        combined._is_compiled_shape = True  # type: ignore
                        return combined

                    field_filters[key] = make_combined(field_validator, nested_shape_fn)
                else:
                    field_filters[key] = compile_shape(
                        field_shape, opts.copy(parent_path=field_path)
                    )
            else:
                field_filters[key] = compile_shape(
                    field_shape, opts.copy(parent_path=field_path)
                )

        def object_filter(value: Any) -> Any:
            if value is None or not isinstance(value, dict):
                return {}

            # Deep clone to avoid mutation
            source = deepcopy(value)
            result: dict[str, Any] = {}

            for key, filter_fn in field_filters.items():
                try:
                    # Check for field aliasing
                    config = None
                    original_shape = shape.get(key)
                    if _is_field_config(original_shape):
                        config = _get_field_config(original_shape)

                    source_key = config.from_field if config and config.from_field else key
                    source_val = source.get(source_key)

                    out_val = filter_fn(source_val)

                    # Apply redact hook if provided
                    if opts.redact:
                        try:
                            out_val = opts.redact(key, out_val)
                        except Exception as e:
                            if opts.dev and opts.logger:
                                opts.logger(f"[payload-guard] redact hook error: {e}", "warn")

                    result[key] = out_val
                except Exception as e:
                    if opts.dev and opts.logger:
                        opts.logger(f'[payload-guard] filter error for field "{key}": {e}', "warn")
                    # Graceful failure - skip this field

            return result

        object_filter._is_compiled_shape = True  # type: ignore
        return object_filter

    # Fallback - return value as-is
    def identity_filter(value: Any) -> Any:
        return value

    identity_filter._is_compiled_shape = True  # type: ignore
    return identity_filter
