"""
Schema Validator

Validates connector credentials and source config against JSON schemas.
- schema.json: Connector-specific credentials validation
- target_schema.json: Source target destination and sync frequency validation
"""

import importlib
import importlib.resources
import json
import re
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple


class ValidationError(Exception):
    """Raised when schema validation fails."""

    def __init__(self, errors: List[str]):
        self.errors = errors
        super().__init__(f"Validation failed: {'; '.join(errors)}")


def load_schema(connector_name: str, base_path: str) -> Optional[Dict[str, Any]]:
    """
    Load JSON schema for a connector.

    Tries installed schema package first, falls back to file.

    Args:
        connector_name: Name of the connector (e.g., 'notion')
        base_path: Base path to lakehouse_files

    Returns:
        Schema dict or None if not found
    """
    # Try package-based loading
    try:
        schema_module = importlib.import_module(f"vd_dlt_{connector_name}_schema")
        if hasattr(schema_module, "get_schema"):
            return schema_module.get_schema()
    except ImportError:
        pass

    # Fall back to file-based loading
    schema_file = Path(base_path) / "connectors" / connector_name / "schema.json"
    if not schema_file.exists():
        return None

    with open(schema_file, "r") as f:
        return json.load(f)


def load_target_schema(base_path: str) -> Optional[Dict[str, Any]]:
    """
    Load target schema for source configuration validation.

    Tries bundled package data first, falls back to file.

    Args:
        base_path: Base path to lakehouse_files

    Returns:
        Schema dict or None if not found
    """
    # Try loading from package data
    try:
        ref = importlib.resources.files("vd_dlt") / "schemas" / "target_schema.json"
        with importlib.resources.as_file(ref) as schema_file:
            with open(schema_file, "r") as f:
                return json.load(f)
    except Exception:
        pass

    # Fall back to file-based loading
    schema_file = Path(base_path) / "core" / "target_schema.json"
    if not schema_file.exists():
        return None

    with open(schema_file, "r") as f:
        return json.load(f)


def _resolve_ref(schema: Dict[str, Any], ref: str) -> Dict[str, Any]:
    """Resolve a $ref pointer within the schema."""
    if not ref.startswith("#/"):
        return {}

    parts = ref[2:].split("/")
    current = schema
    for part in parts:
        if isinstance(current, dict) and part in current:
            current = current[part]
        else:
            return {}
    return current if isinstance(current, dict) else {}


def _validate_value(
    value: Any,
    prop_schema: Dict[str, Any],
    field_name: str,
    root_schema: Dict[str, Any],
) -> List[str]:
    """Validate a single value against its schema."""
    errors = []

    # Handle $ref
    if "$ref" in prop_schema:
        resolved = _resolve_ref(root_schema, prop_schema["$ref"])
        return _validate_value(value, resolved, field_name, root_schema)

    # Handle oneOf
    if "oneOf" in prop_schema:
        one_of_valid = False
        for sub_schema in prop_schema["oneOf"]:
            sub_errors = _validate_value(value, sub_schema, field_name, root_schema)
            if not sub_errors:
                one_of_valid = True
                break
        if not one_of_valid:
            errors.append(f"'{field_name}' does not match any allowed schema")
        return errors

    prop_type = prop_schema.get("type")

    # Type validation
    if prop_type == "string":
        if not isinstance(value, str):
            errors.append(f"'{field_name}' must be a string, got {type(value).__name__}")
            return errors

        # minLength
        min_len = prop_schema.get("minLength")
        if min_len is not None and len(value) < min_len:
            errors.append(f"'{field_name}' must be at least {min_len} characters")

        # pattern
        pattern = prop_schema.get("pattern")
        if pattern and not re.match(pattern, value):
            errors.append(f"'{field_name}' does not match required pattern: {pattern}")

        # enum
        enum_values = prop_schema.get("enum")
        if enum_values and value not in enum_values:
            errors.append(f"'{field_name}' must be one of: {', '.join(enum_values)}")

        # format
        fmt = prop_schema.get("format")
        if fmt == "uri" and not value.startswith(("http://", "https://")):
            errors.append(f"'{field_name}' must be a valid URI")

    elif prop_type == "integer":
        if not isinstance(value, int) or isinstance(value, bool):
            errors.append(f"'{field_name}' must be an integer")

    elif prop_type == "number":
        if not isinstance(value, (int, float)) or isinstance(value, bool):
            errors.append(f"'{field_name}' must be a number")

    elif prop_type == "boolean":
        if not isinstance(value, bool):
            errors.append(f"'{field_name}' must be a boolean")

    elif prop_type == "array":
        if not isinstance(value, list):
            errors.append(f"'{field_name}' must be an array")
            return errors

        # minItems
        min_items = prop_schema.get("minItems")
        if min_items is not None and len(value) < min_items:
            errors.append(f"'{field_name}' must have at least {min_items} items")

        # Validate items
        items_schema = prop_schema.get("items")
        if items_schema:
            for i, item in enumerate(value):
                item_errors = _validate_value(
                    item, items_schema, f"{field_name}[{i}]", root_schema
                )
                errors.extend(item_errors)

    elif prop_type == "object":
        if not isinstance(value, dict):
            errors.append(f"'{field_name}' must be an object")
            return errors

        # Validate nested object
        obj_errors = _validate_object(value, prop_schema, root_schema, field_name)
        errors.extend(obj_errors)

    return errors


def _validate_object(
    data: Dict[str, Any],
    schema: Dict[str, Any],
    root_schema: Dict[str, Any],
    prefix: str = "",
) -> List[str]:
    """Validate an object against its schema."""
    errors = []

    # Handle $ref at object level
    if "$ref" in schema:
        resolved = _resolve_ref(root_schema, schema["$ref"])
        return _validate_object(data, resolved, root_schema, prefix)

    # Check required fields
    required = schema.get("required", [])
    for field in required:
        if field not in data:
            field_path = f"{prefix}.{field}" if prefix else field
            errors.append(f"Missing required field: '{field_path}'")
        elif data[field] is None or data[field] == "":
            field_path = f"{prefix}.{field}" if prefix else field
            errors.append(f"Required field '{field_path}' cannot be empty")

    # Validate properties
    properties = schema.get("properties", {})
    for field, value in data.items():
        if field not in properties:
            continue  # Allow additional properties by default

        field_path = f"{prefix}.{field}" if prefix else field
        prop_schema = properties[field]
        field_errors = _validate_value(value, prop_schema, field_path, root_schema)
        errors.extend(field_errors)

    # Check additionalProperties
    if schema.get("additionalProperties") is False:
        extra_fields = set(data.keys()) - set(properties.keys())
        for field in extra_fields:
            field_path = f"{prefix}.{field}" if prefix else field
            errors.append(f"Unknown field: '{field_path}'")

    return errors


def validate_credentials(
    credentials: Dict[str, Any],
    schema: Dict[str, Any],
) -> Tuple[bool, List[str]]:
    """
    Validate credentials against the credentials definition in schema.

    Args:
        credentials: Credentials dict to validate
        schema: Full connector schema (with definitions)

    Returns:
        Tuple of (is_valid, error_messages)
    """
    # Get credentials schema from definitions
    cred_schema = schema.get("definitions", {}).get("credentials", {})
    if not cred_schema:
        # Fallback: try to use schema directly if it looks like credentials
        if "properties" in schema and "access_token" in schema.get("properties", {}):
            cred_schema = schema
        else:
            return True, []  # No credentials schema defined

    errors = _validate_object(credentials, cred_schema, schema)
    return len(errors) == 0, errors


def validate_connector_credentials(
    connector_name: str,
    credentials: Dict[str, Any],
    base_path: str,
) -> Tuple[bool, List[str]]:
    """
    Validate credentials for a specific connector.

    Args:
        connector_name: Name of the connector
        credentials: Credentials dict to validate
        base_path: Base path to lakehouse_files

    Returns:
        Tuple of (is_valid, error_messages)
    """
    schema = load_schema(connector_name, base_path)

    if schema is None:
        return True, []  # No schema found - skip validation

    return validate_credentials(credentials, schema)


def validate_target_config(
    source_config: Dict[str, Any],
    base_path: str,
) -> Tuple[bool, List[str]]:
    """
    Validate target configuration (sync_freq, target) against target_schema.json.

    Args:
        source_config: Source configuration dict
        base_path: Base path to lakehouse_files

    Returns:
        Tuple of (is_valid, error_messages)
    """
    schema = load_target_schema(base_path)

    if schema is None:
        return True, []  # No schema found - skip validation

    errors = []

    # Validate sync_freq
    sync_freq = source_config.get("sync_freq")
    if sync_freq is not None:
        sync_freq_schema = schema.get("definitions", {}).get("sync_freq", {})
        sync_freq_errors = _validate_value(sync_freq, sync_freq_schema, "sync_freq", schema)
        errors.extend(sync_freq_errors)

    # Validate target
    target = source_config.get("target")
    if target is not None:
        target_schema = schema.get("definitions", {}).get("target", {})
        target_errors = _validate_object(target, target_schema, schema, "target")
        errors.extend(target_errors)

    return len(errors) == 0, errors


def validate_all(
    connector_name: str,
    credentials: Dict[str, Any],
    source_config: Dict[str, Any],
    base_path: str,
) -> Tuple[bool, List[str]]:
    """
    Validate credentials and source config against schemas.

    Validates:
    - Credentials against connector schema.json
    - Target config (sync_freq, target) against target_schema.json

    Args:
        connector_name: Name of the connector
        credentials: Credentials dict
        source_config: Source configuration dict
        base_path: Base path to lakehouse_files

    Returns:
        Tuple of (is_valid, all_error_messages)
    """
    all_errors = []

    # Validate credentials against connector schema
    cred_valid, cred_errors = validate_connector_credentials(
        connector_name, credentials, base_path
    )
    all_errors.extend(cred_errors)

    # Validate target config (sync_freq, target)
    target_valid, target_errors = validate_target_config(source_config, base_path)
    all_errors.extend(target_errors)

    return len(all_errors) == 0, all_errors


def require_valid_credentials(
    connector_name: str,
    credentials: Dict[str, Any],
    base_path: str,
) -> None:
    """
    Validate credentials and raise ValidationError if invalid.

    Args:
        connector_name: Name of the connector
        credentials: Credentials dict
        base_path: Base path to lakehouse_files

    Raises:
        ValidationError: If validation fails
    """
    is_valid, errors = validate_connector_credentials(connector_name, credentials, base_path)

    if not is_valid:
        raise ValidationError(errors)
