"""Metric naming convention validation for MCA SDK.

This module enforces metric naming conventions to ensure consistency
across all model types (internal, generative, agentic, vendor).

Naming conventions:
- Internal models: emms_model_*_total, emms_model_*_seconds, emms_model_*_ratio
- GenAI models: emms_genai_*_total, emms_genai_*_seconds, emms_genai_*_ratio
- Agentic AI models: emms_agentic_*_total, emms_agentic_*_seconds, emms_agentic_*_ratio
- Vendor models: emms_vendor_*_total, emms_vendor_*_seconds, emms_vendor_*_ratio
"""

import re
from typing import Optional, Set

from ..utils.exceptions import ValidationError

# Valid metric name patterns by metric prefix
# Metric prefix is determined by get_metric_prefix(model_category, model_type)
METRIC_PATTERNS = {
    "model": [  # Traditional ML models (internal + regression/time-series/classification)
        re.compile(r"^emms_model_[a-z0-9_]+_total$"),  # Counters: emms_model_predictions_total
        re.compile(r"^emms_model_[a-z0-9_]+_seconds$"),  # Latency: emms_model_latency_seconds
        re.compile(r"^emms_model_[a-z0-9_]+_ratio$"),  # Ratios: emms_model_accuracy_ratio
        re.compile(
            r"^emms_model_[a-z0-9_]+_milliseconds$"
        ),  # Milliseconds: emms_model_inference_milliseconds
        re.compile(r"^emms_model_[a-z0-9_]+_count$"),  # Counts: emms_model_error_count
    ],
    "genai": [  # Generative AI/LLM models (internal + generative)
        re.compile(r"^emms_genai_[a-z0-9_]+_total$"),  # Counters: emms_genai_requests_total
        re.compile(r"^emms_genai_[a-z0-9_]+_seconds$"),  # Latency: emms_genai_latency_seconds
        re.compile(r"^emms_genai_[a-z0-9_]+_ratio$"),  # Ratios: emms_genai_success_ratio
        re.compile(r"^emms_genai_[a-z0-9_]+_count$"),  # Counts: emms_genai_token_count
        re.compile(r"^emms_genai_[a-z0-9_]+_dollars$"),  # Cost: emms_genai_cost_dollars
        re.compile(r"^emms_genai_[a-z0-9_]+_usd$"),  # Cost: emms_genai_cost_usd
        re.compile(r"^emms_genai_[a-z0-9_]+_prompt$"),  # Tokens: emms_genai_tokens_prompt
        re.compile(r"^emms_genai_[a-z0-9_]+_completion$"),  # Tokens: emms_genai_tokens_completion
    ],
    "agentic": [  # Agentic AI models (internal + agentic)
        re.compile(r"^emms_agentic_[a-z0-9_]+_total$"),  # Counters: emms_agentic_goals_total
        re.compile(r"^emms_agentic_[a-z0-9_]+_seconds$"),  # Latency: emms_agentic_reasoning_seconds
        re.compile(r"^emms_agentic_[a-z0-9_]+_ratio$"),  # Ratios: emms_agentic_success_ratio
        re.compile(r"^emms_agentic_[a-z0-9_]+_count$"),  # Counts: emms_agentic_tool_count
        re.compile(r"^emms_agentic_[a-z0-9_]+_dollars$"),  # Cost: emms_agentic_cost_dollars
    ],
    "vendor": [  # Vendor models (vendor + any type)
        re.compile(r"^emms_vendor_[a-z0-9_]+_total$"),  # Counters: emms_vendor_predictions_total
        re.compile(r"^emms_vendor_[a-z0-9_]+_seconds$"),  # Latency: emms_vendor_latency_seconds
        re.compile(r"^emms_vendor_[a-z0-9_]+_ratio$"),  # Ratios: emms_vendor_accuracy_ratio
        re.compile(r"^emms_vendor_[a-z0-9_]+_count$"),  # Counts: emms_vendor_error_count
    ],
}

# Valid metric units (suffixes)
VALID_UNITS: Set[str] = {
    "total",  # Counters (monotonically increasing)
    "seconds",  # Time duration in seconds
    "milliseconds",  # Time duration in milliseconds
    "ratio",  # Ratios and percentages (0.0-1.0)
    "count",  # Counts (may decrease)
    "dollars",  # Cost in dollars (GenAI/agentic)
    "usd",  # Cost in USD (GenAI: emms_genai_cost_usd)
    "prompt",  # Token counts by type (GenAI: emms_genai_tokens_prompt)
    "completion",  # Token counts by type (GenAI: emms_genai_tokens_completion)
}

# Valid metric prefixes (determined by routing)
VALID_PREFIXES = ["model", "genai", "agentic", "vendor"]


class MetricNamingConvention:
    """Validator for metric naming conventions.

    Enforces consistent metric naming across different model types to ensure
    metrics can be properly categorized and queried in monitoring systems.

    Examples:
        >>> validator = MetricNamingConvention(model_type="internal")
        >>> validator.validate("emms_model_predictions_total")  # OK
        >>> validator.validate("custom_metric")  # Raises ValidationError

        >>> validator = MetricNamingConvention(model_type="internal", strict=False)
        >>> validator.validate("custom_metric")  # OK (non-strict mode)
    """

    def __init__(
        self, model_category: str = "internal", model_type: str = "regression", strict: bool = True
    ):
        """Initialize metric naming validator.

        Uses the two-field taxonomy to determine the metric prefix and
        corresponding naming patterns.

        Args:
            model_category: High-level category ("internal" or "vendor")
            model_type: Specific type ("regression", "time-series", "classification", "generative", "agentic")
            strict: If True, enforce naming conventions. If False, allow any name.

        Raises:
            ValidationError: If taxonomy combination is invalid
        """
        from ..utils.routing import get_metric_prefix

        # Determine metric prefix from taxonomy
        try:
            self.expected_prefix = get_metric_prefix(model_category, model_type)
        except ValueError as e:
            raise ValidationError(f"Invalid taxonomy combination: {e}") from e

        self.model_category = model_category
        self.model_type = model_type
        self.strict = strict
        self.patterns = METRIC_PATTERNS.get(self.expected_prefix, [])

    def validate(self, metric_name: str, metric_data: Optional[dict] = None) -> None:
        """Validate a metric name against conventions and optionally validate structure.

        Args:
            metric_name: The metric name to validate
            metric_data: Optional metric data structure to validate (Protobuf schema)

        Raises:
            ValidationError: If metric name doesn't follow conventions (strict mode only)
        """
        # PROTOBUF SCHEMA VALIDATION (AC #5)
        # Validate metric structure if provided
        if metric_data is not None:
            self._validate_metric_structure(metric_data)

        # SECURITY: Input validation to prevent DoS/ReDoS attacks
        if not isinstance(metric_name, str):
            raise ValidationError(f"Metric name must be a string, got {type(metric_name)}")

        # SECURITY: Limit metric name length to prevent DoS
        MAX_METRIC_NAME_LENGTH = 256
        if len(metric_name) > MAX_METRIC_NAME_LENGTH:
            raise ValidationError(
                f"Metric name exceeds maximum length of {MAX_METRIC_NAME_LENGTH} characters. "
                f"Got {len(metric_name)} characters."
            )

        # SECURITY: Sanitize input - allow safe characters
        # In strict mode: alphanumeric, dots, underscores only
        # In non-strict mode: also allow hyphens (for legacy migration)
        # Always reject: control characters, quotes, brackets (injection risk)
        allowed_chars = "._-" if not self.strict else "._"
        dangerous_chars = set(
            "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
            "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
            "\"'\\/[]{}()<>;|&$`"
        )

        if metric_name:
            for c in metric_name:
                if not c.isalnum() and c not in allowed_chars:
                    if c in dangerous_chars or ord(c) < 32 or ord(c) > 126:
                        raise ValidationError(
                            f"Metric name contains dangerous character: {repr(c)}. "
                            "Control characters and special injection characters are not allowed."
                        )
                    elif self.strict:
                        raise ValidationError(
                            f"Metric name contains invalid character: {repr(c)}. "
                            "In strict mode, only alphanumeric, dots, and underscores are allowed. "
                            "Use strict_validation=False to allow hyphens and other characters."
                        )

        # Non-strict mode: allow any safe name (after security checks)
        if not self.strict:
            return

        # Check if name matches any pattern for this model type
        for pattern in self.patterns:
            if pattern.match(metric_name):
                return  # Valid!

        # If we get here, name doesn't match any pattern
        self._raise_validation_error(metric_name)

    def _validate_metric_structure(self, metric_data: dict) -> None:
        """Validate metric structure against OTLP Protobuf schema (AC #5).

        Validates basic structural requirements:
        - Metric data must be a dict
        - Required fields must be present
        - Field types must be correct

        Args:
            metric_data: Metric data structure to validate

        Raises:
            ValidationError: If structure is invalid
        """
        if not isinstance(metric_data, dict):
            raise ValidationError(f"Metric data must be a dict, got {type(metric_data)}")

        # Validate required fields (basic OTLP structure)
        # Note: Full OTLP validation is SDK's responsibility
        # We only validate what applications can control
        if "name" in metric_data:
            if not isinstance(metric_data["name"], str):
                raise ValidationError(
                    f"Metric 'name' field must be a string, got {type(metric_data['name'])}"
                )

        if "unit" in metric_data:
            if not isinstance(metric_data["unit"], str):
                raise ValidationError(
                    f"Metric 'unit' field must be a string, got {type(metric_data['unit'])}"
                )
            # Validate unit against VALID_UNITS if strict
            if self.strict and metric_data["unit"]:
                # Extract unit from metric name (e.g., model.latency_seconds -> seconds)
                if metric_data["unit"] not in VALID_UNITS:
                    raise ValidationError(
                        f"Invalid metric unit: '{metric_data['unit']}'. "
                        f"Valid units: {sorted(VALID_UNITS)}"
                    )

        if "value" in metric_data:
            if not isinstance(metric_data["value"], (int, float)):
                raise ValidationError(
                    f"Metric 'value' field must be numeric, got {type(metric_data['value'])}"
                )

        if "attributes" in metric_data:
            if not isinstance(metric_data["attributes"], dict):
                raise ValidationError(
                    f"Metric 'attributes' field must be a dict, got {type(metric_data['attributes'])}"
                )

    def _raise_validation_error(self, metric_name: str) -> None:
        """Raise a detailed ValidationError with suggestions.

        Args:
            metric_name: The invalid metric name
        """
        # Provide helpful error message with examples
        examples = self._get_examples()

        error_msg = (
            f"Invalid metric name: '{metric_name}'\n"
            f"\n"
            f"For emms_{self.expected_prefix}_* metrics (model_category='{self.model_category}', "
            f"model_type='{self.model_type}'), metric names must follow these patterns:\n"
            f"  - emms_{self.expected_prefix}_*_total (counters)\n"
            f"  - emms_{self.expected_prefix}_*_seconds (latency)\n"
            f"  - emms_{self.expected_prefix}_*_ratio (ratios)\n"
            f"  - emms_{self.expected_prefix}_*_count (counts)\n"
        )

        if self.expected_prefix == "genai":
            error_msg += f"  - emms_{self.expected_prefix}_*_dollars (cost)\n"
            error_msg += f"  - emms_{self.expected_prefix}_*_usd (cost in USD)\n"

        error_msg += "\nExamples:\n"
        for example in examples:
            error_msg += f"  - {example}\n"

        error_msg += "\nTo disable validation, set strict_validation=False in MCAConfig."

        raise ValidationError(error_msg)

    def _get_examples(self) -> list:
        """Get example metric names for this metric prefix."""
        if self.expected_prefix == "model":
            return [
                "emms_model_predictions_total",
                "emms_model_latency_seconds",
                "emms_model_accuracy_ratio",
                "emms_model_error_count",
            ]
        elif self.expected_prefix == "genai":
            return [
                "emms_genai_requests_total",
                "emms_genai_latency_seconds",
                "emms_genai_tokens_total",
                "emms_genai_cost_usd",
            ]
        elif self.expected_prefix == "agentic":
            return [
                "emms_agentic_goals_total",
                "emms_agentic_reasoning_seconds",
                "emms_agentic_success_ratio",
                "emms_agentic_tool_count",
                "emms_agentic_cost_dollars",
            ]
        else:  # vendor
            return [
                "emms_vendor_predictions_total",
                "emms_vendor_latency_seconds",
                "emms_vendor_accuracy_ratio",
            ]


def validate_metric_name(
    metric_name: str,
    model_category: str = "internal",
    model_type: str = "regression",
    strict: bool = True,
) -> None:
    """Convenience function to validate a metric name.

    Uses the two-field taxonomy to determine the expected metric prefix
    and validate against naming conventions.

    Args:
        metric_name: The metric name to validate
        model_category: High-level category ("internal" or "vendor")
        model_type: Specific type ("regression", "time-series", "classification", "generative", "agentic")
        strict: If True, enforce naming conventions

    Raises:
        ValidationError: If metric name doesn't follow conventions (strict mode only)

    Example:
        >>> validate_metric_name("emms_model_predictions_total", "internal", "regression")  # OK
        >>> validate_metric_name("custom_metric", "internal", "regression")  # Raises ValidationError
    """
    validator = MetricNamingConvention(
        model_category=model_category, model_type=model_type, strict=strict
    )
    validator.validate(metric_name)


def validate_model_id(model_id: str) -> bool:
    """Validate model_id to prevent injection attacks.

    Args:
        model_id: Model identifier to validate

    Returns:
        True if valid

    Raises:
        ValidationError: If model_id contains malicious patterns

    Example:
        >>> validate_model_id("mdl-001")  # OK
        >>> validate_model_id("model_123")  # OK
        >>> validate_model_id("model; rm -rf /")  # Raises ValidationError
    """
    if not model_id:
        raise ValidationError("model_id cannot be empty")

    # Check for path traversal attacks
    if ".." in model_id or "/" in model_id or "\\" in model_id:
        raise ValidationError(
            f"model_id contains invalid characters (path traversal attempt): {model_id}"
        )

    # Check for SQL injection patterns
    sql_patterns = [
        "'",
        '"',
        ";",
        "--",
        "/*",
        "*/",
        "xp_",
        "sp_",
        "DROP",
        "SELECT",
        "INSERT",
        "UPDATE",
        "DELETE",
    ]
    for pattern in sql_patterns:
        if pattern in model_id:
            raise ValidationError(
                f"model_id contains invalid pattern (potential SQL injection): {model_id}"
            )

    # Check for command injection patterns
    cmd_patterns = ["|", "&", "$", "`", "$(", "&&", "||", ">", "<"]
    for pattern in cmd_patterns:
        if pattern in model_id:
            raise ValidationError(
                f"model_id contains invalid characters (potential command injection): {model_id}"
            )

    # Check for XSS patterns
    if "<" in model_id or ">" in model_id:
        raise ValidationError(f"model_id contains HTML/script tags (potential XSS): {model_id}")

    # Validate format: alphanumeric, hyphens, underscores only
    if not re.match(r"^[a-zA-Z0-9_-]+$", model_id):
        raise ValidationError(
            f"model_id must contain only alphanumeric characters, hyphens, and underscores: {model_id}"
        )

    # Validate length
    if len(model_id) > 128:
        raise ValidationError(f"model_id exceeds maximum length (128 characters): {len(model_id)}")

    return True


def validate_service_name(service_name: str) -> bool:
    """Validate service_name to prevent injection attacks.

    Args:
        service_name: Service name to validate

    Returns:
        True if valid

    Raises:
        ValidationError: If service_name contains malicious patterns

    Example:
        >>> validate_service_name("my-service")  # OK
        >>> validate_service_name("service_123")  # OK
        >>> validate_service_name("service'; DROP TABLE users;--")  # Raises ValidationError
    """
    if not service_name:
        raise ValidationError("service_name cannot be empty")

    # Check for path traversal attacks
    if ".." in service_name or "/" in service_name or "\\" in service_name:
        raise ValidationError(
            f"service_name contains invalid characters (path traversal attempt): {service_name}"
        )

    # Check for SQL injection patterns
    sql_patterns = ["'", '"', ";", "--", "/*", "*/", "DROP", "SELECT", "INSERT", "UPDATE", "DELETE"]
    for pattern in sql_patterns:
        if pattern in service_name:
            raise ValidationError(
                f"service_name contains invalid pattern (potential SQL injection): {service_name}"
            )

    # Check for command injection patterns
    cmd_patterns = ["|", "&", "$", "`", "$(", "&&", "||", ">", "<"]
    for pattern in cmd_patterns:
        if pattern in service_name:
            raise ValidationError(
                f"service_name contains invalid characters (potential command injection): {service_name}"
            )

    # Check for XSS patterns
    if "<" in service_name or ">" in service_name:
        raise ValidationError(
            f"service_name contains HTML/script tags (potential XSS): {service_name}"
        )

    # Validate format: alphanumeric, hyphens, underscores only
    if not re.match(r"^[a-zA-Z0-9_-]+$", service_name):
        raise ValidationError(
            f"service_name must contain only alphanumeric characters, hyphens, and underscores: {service_name}"
        )

    # Validate length
    if len(service_name) > 128:
        raise ValidationError(
            f"service_name exceeds maximum length (128 characters): {len(service_name)}"
        )

    return True
