"""
EMU Trigger DSL Validator.

Provides local validation of EMU definitions before API submission,
enabling faster feedback during development.
"""

from dataclasses import dataclass, field
from typing import Any

from .parser import (
    tokenize,
    validate_accessor_format,
    validate_duration_format,
    validate_string_literal,
    Token,
    TokenType,
    LexerError,
)


class DSLSyntaxError(Exception):
    """Error in DSL trigger syntax."""

    def __init__(self, message: str, line: int = 1, column: int = 1):
        self.line = line
        self.column = column
        super().__init__(f"Syntax error at line {line}, column {column}: {message}")


class DSLValidationError(Exception):
    """Validation error in EMU definition."""

    def __init__(self, errors: list[str]):
        self.errors = errors
        super().__init__(f"Validation failed with {len(errors)} error(s): {'; '.join(errors)}")


@dataclass
class ValidationResult:
    """Result of EMU validation."""

    valid: bool
    errors: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)

    def __bool__(self) -> bool:
        return self.valid


# Valid action types
VALID_ACTION_TYPES = {"tool_call", "decision_prompt", "route", "context_directive"}

# Valid EMU states
VALID_EMU_STATES = {"draft", "active", "canary", "shadow", "archived"}

# Valid cooldown gates
VALID_COOLDOWN_GATES = {"activation", "ack"}

# Valid policy modes
VALID_POLICY_MODES = {"auto", "require_human", "advisory"}

# Mode restrictions by action type
# route and context_directive normalize to auto for non-auto modes
MODE_RESTRICTED_ACTIONS = {"route", "context_directive"}


def validate_trigger(trigger: str) -> ValidationResult:
    """
    Validate a trigger DSL expression.

    Args:
        trigger: The trigger expression string

    Returns:
        ValidationResult with any errors or warnings

    Raises:
        DSLSyntaxError: If the trigger has syntax errors
    """
    errors: list[str] = []
    warnings: list[str] = []

    if not trigger or not trigger.strip():
        errors.append("Trigger expression cannot be empty")
        return ValidationResult(valid=False, errors=errors)

    # Tokenize the expression
    try:
        tokens = list(tokenize(trigger))
    except LexerError as e:
        raise DSLSyntaxError(str(e), e.line, e.column) from e

    # Remove EOF token for validation
    tokens = [t for t in tokens if t.type != TokenType.EOF]

    if not tokens:
        errors.append("Trigger expression cannot be empty")
        return ValidationResult(valid=False, errors=errors)

    # Validate accessor formats
    accessor_errors = validate_accessor_format(tokens)
    errors.extend(accessor_errors)

    # Validate balanced parentheses
    paren_depth = 0
    bracket_depth = 0
    for token in tokens:
        if token.type == TokenType.LPAREN:
            paren_depth += 1
        elif token.type == TokenType.RPAREN:
            paren_depth -= 1
            if paren_depth < 0:
                errors.append(f"Unmatched ')' at column {token.column}")
        elif token.type == TokenType.LBRACKET:
            bracket_depth += 1
        elif token.type == TokenType.RBRACKET:
            bracket_depth -= 1
            if bracket_depth < 0:
                errors.append(f"Unmatched ']' at column {token.column}")

    if paren_depth > 0:
        errors.append("Unclosed '(' in expression")
    if bracket_depth > 0:
        errors.append("Unclosed '[' in expression")

    # Validate string literals
    for token in tokens:
        if token.type == TokenType.STRING:
            errors.extend(validate_string_literal(token))

    # Validate IN operator is followed by duration
    for i, token in enumerate(tokens):
        if token.type == TokenType.IN and i + 1 < len(tokens):
            next_token = tokens[i + 1]
            if next_token.type == TokenType.STRING:
                if not validate_duration_format(next_token.value):
                    errors.append(
                        f"Invalid duration format '{next_token.value}' at column {next_token.column}. "
                        "Expected ISO 8601 duration (e.g., 'PT1H', 'P7D')"
                    )
            elif next_token.type == TokenType.LBRACKET:
                # IN can also be used with lists: value IN ['a', 'b', 'c']
                pass
            else:
                # Check if it's a temporal query or set membership
                # Look back to see if there's an event accessor
                has_event_accessor = any(
                    t.type == TokenType.EVENT for t in tokens[:i]
                )
                if has_event_accessor and next_token.type != TokenType.STRING:
                    warnings.append(
                        f"IN operator at column {token.column} may need a duration string "
                        "for temporal event queries"
                    )

    # Check for operator at start (except NOT and EXISTS and COUNT)
    if tokens[0].type in (TokenType.AND, TokenType.OR):
        errors.append(f"Expression cannot start with '{tokens[0].value}'")

    # Check for consecutive operators
    for i in range(len(tokens) - 1):
        curr = tokens[i]
        next_t = tokens[i + 1]
        if curr.type in (TokenType.AND, TokenType.OR) and next_t.type in (TokenType.AND, TokenType.OR):
            errors.append(f"Consecutive operators '{curr.value}' and '{next_t.value}' at column {curr.column}")

    return ValidationResult(
        valid=len(errors) == 0,
        errors=errors,
        warnings=warnings,
    )


def validate_action(action: dict[str, Any]) -> list[str]:
    """
    Validate an EMU action definition.

    Args:
        action: The action dictionary

    Returns:
        List of error messages (empty if valid)
    """
    errors: list[str] = []

    if not isinstance(action, dict):
        errors.append("Action must be a dictionary")
        return errors

    # Check required 'type' field
    if "type" not in action:
        errors.append("Action missing required 'type' field")
        return errors

    action_type = action["type"]
    if action_type not in VALID_ACTION_TYPES:
        errors.append(
            f"Invalid action type '{action_type}'. "
            f"Must be one of: {', '.join(sorted(VALID_ACTION_TYPES))}"
        )
        return errors

    # Validate based on action type
    if action_type == "tool_call":
        if "tool" not in action and "tool_id" not in action:
            errors.append("tool_call action requires 'tool' or 'tool_id' field")
        if "tool" in action:
            tool = action["tool"]
            if isinstance(tool, dict):
                if "tool_id" not in tool:
                    errors.append("tool_call.tool requires 'tool_id' field")

    elif action_type == "decision_prompt":
        if "prompt" not in action:
            errors.append("decision_prompt action requires 'prompt' field")

    elif action_type == "route":
        if "route" not in action:
            errors.append("route action requires 'route' field")

    elif action_type == "context_directive":
        if "directive" not in action:
            errors.append("context_directive action requires 'directive' field")

    return errors


def validate_policy_mode_action_compatibility(
    policy: dict[str, Any],
    action: dict[str, Any]
) -> list[str]:
    """
    Validate policy mode is compatible with action type.

    Mode behavior by action type:
    - tool_call: All modes work (auto=execute, require_human=approval, advisory=suggest)
    - decision_prompt: Mode is ignored (always emits prompt)
    - route: Only auto is meaningful (others normalize to auto)
    - context_directive: Only auto is meaningful (others normalize to auto)

    Returns list of warning messages.
    """
    warnings: list[str] = []

    if not isinstance(policy, dict) or not isinstance(action, dict):
        return warnings

    mode = policy.get("mode", "auto")
    action_type = action.get("type")

    if action_type == "decision_prompt" and mode != "auto":
        warnings.append(
            f"policy.mode='{mode}' is ignored for decision_prompt actions (mode is always ignored)"
        )

    if action_type in MODE_RESTRICTED_ACTIONS and mode != "auto":
        warnings.append(
            f"policy.mode='{mode}' with {action_type} action will normalize to 'auto' "
            f"(only auto mode is supported for {action_type})"
        )

    return warnings


def validate_policy(policy: dict[str, Any]) -> list[str]:
    """
    Validate an EMU policy definition.

    Args:
        policy: The policy dictionary

    Returns:
        List of error messages (empty if valid)
    """
    errors: list[str] = []

    if not isinstance(policy, dict):
        errors.append("Policy must be a dictionary")
        return errors

    # Validate mode
    if "mode" in policy:
        mode = policy["mode"]
        if mode not in VALID_POLICY_MODES:
            errors.append(f"Invalid policy mode '{mode}'. Must be one of: {', '.join(VALID_POLICY_MODES)}")

    # Validate priority (0-9)
    if "priority" in policy:
        priority = policy["priority"]
        if not isinstance(priority, (int, float)):
            errors.append("Policy priority must be a number")
        elif priority < 0 or priority > 9:
            errors.append(f"Policy priority must be between 0 and 9, got {priority}")

    # Validate cooldown
    if "cooldown" in policy:
        cooldown = policy["cooldown"]
        if isinstance(cooldown, dict):
            if "seconds" in cooldown:
                if not isinstance(cooldown["seconds"], (int, float)):
                    errors.append("Cooldown seconds must be a number")
                elif cooldown["seconds"] < 0:
                    errors.append("Cooldown seconds cannot be negative")

            if "gate" in cooldown:
                gate = cooldown["gate"]
                if gate not in VALID_COOLDOWN_GATES:
                    errors.append(f"Invalid cooldown gate '{gate}'. Must be one of: {', '.join(VALID_COOLDOWN_GATES)}")
        else:
            errors.append("Cooldown must be a dictionary with 'seconds' and optional 'gate' fields")

    # Validate exclusion_groups
    if "exclusion_groups" in policy:
        groups = policy["exclusion_groups"]
        if not isinstance(groups, list):
            errors.append("exclusion_groups must be a list")
        elif not all(isinstance(g, str) for g in groups):
            errors.append("All exclusion_groups entries must be strings")

    return errors


def validate_emu(emu_def: dict[str, Any]) -> ValidationResult:
    """
    Validate a complete EMU definition.

    Args:
        emu_def: The EMU definition dictionary

    Returns:
        ValidationResult with any errors or warnings
    """
    errors: list[str] = []
    warnings: list[str] = []

    if not isinstance(emu_def, dict):
        errors.append("EMU definition must be a dictionary")
        return ValidationResult(valid=False, errors=errors)

    # Required fields
    required_fields = ["emu_key", "trigger", "action"]
    for field in required_fields:
        if field not in emu_def:
            errors.append(f"EMU missing required field: {field}")

    if errors:
        return ValidationResult(valid=False, errors=errors)

    # Validate emu_key format
    emu_key = emu_def["emu_key"]
    if not isinstance(emu_key, str):
        errors.append("emu_key must be a string")
    elif len(emu_key) < 3:
        errors.append("emu_key must be at least 3 characters")
    elif len(emu_key) > 128:
        errors.append("emu_key cannot exceed 128 characters")
    elif not all(c.isalnum() or c in ":_.-" for c in emu_key):
        errors.append("emu_key can only contain alphanumeric characters and :_.-")

    # Validate trigger
    trigger = emu_def["trigger"]
    if not isinstance(trigger, str):
        errors.append("trigger must be a string")
    else:
        try:
            trigger_result = validate_trigger(trigger)
            errors.extend(trigger_result.errors)
            warnings.extend(trigger_result.warnings)
        except DSLSyntaxError as e:
            errors.append(f"Trigger syntax error: {e}")

    # Validate action
    if "action" in emu_def:
        action_errors = validate_action(emu_def["action"])
        errors.extend(action_errors)

    # Validate policy (optional)
    if "policy" in emu_def:
        policy_errors = validate_policy(emu_def["policy"])
        errors.extend(policy_errors)

    # Validate policy mode / action type compatibility
    if "policy" in emu_def and "action" in emu_def:
        compat_warnings = validate_policy_mode_action_compatibility(
            emu_def["policy"], emu_def["action"]
        )
        warnings.extend(compat_warnings)

    # Validate state (optional)
    if "state" in emu_def:
        state = emu_def["state"]
        if state not in VALID_EMU_STATES:
            errors.append(f"Invalid EMU state '{state}'. Must be one of: {', '.join(VALID_EMU_STATES)}")

    # Validate expected_utility (optional, 0.0-1.0)
    if "expected_utility" in emu_def:
        utility = emu_def["expected_utility"]
        if not isinstance(utility, (int, float)):
            errors.append("expected_utility must be a number")
        elif utility < 0 or utility > 1:
            errors.append(f"expected_utility must be between 0.0 and 1.0, got {utility}")

    # Validate confidence (optional, 0.0-1.0)
    if "confidence" in emu_def:
        confidence = emu_def["confidence"]
        if not isinstance(confidence, (int, float)):
            errors.append("confidence must be a number")
        elif confidence < 0 or confidence > 1:
            errors.append(f"confidence must be between 0.0 and 1.0, got {confidence}")

    return ValidationResult(
        valid=len(errors) == 0,
        errors=errors,
        warnings=warnings,
    )
