"""Security utilities for Gremlin Autopilot.

Provides:
- File path validation and allowlisting
- URL validation against webhook profiles
- LLM context consent enforcement
- Parameter sanitization
"""

from __future__ import annotations

import fnmatch
import re
from pathlib import Path

from .models import (
    ConsentError,
    LLMContext,
    LLMContextLevel,
    SampleOverrideToken,
    SheetContext,
    UserSettings,
    ValidationError,
)

# Shell metacharacters that could be dangerous
SHELL_METACHARACTERS = re.compile(r'[;&|`$(){}[\]<>\\"\'\n\r\x00]')


def get_default_allowed_paths() -> list[Path]:
    """Get the default list of allowed paths.

    Only ~/.g_gremlin/ is allowed by default. CWD is NOT included.
    """
    return [Path.home() / ".g_gremlin"]


def get_allowed_paths(settings: UserSettings) -> list[Path]:
    """Get allowed paths including user-configured ones.

    Args:
        settings: User settings containing allowed_paths

    Returns:
        List of allowed Path objects
    """
    paths = get_default_allowed_paths()

    for path_str in settings.allowed_paths:
        try:
            path = Path(path_str).expanduser().resolve()
            paths.append(path)
        except (OSError, ValueError):
            # Skip invalid paths
            continue

    return paths


def validate_file_path(path: str, allowed_roots: list[Path]) -> Path:
    """Validate a file path against the allowlist.

    Args:
        path: The path to validate (must be absolute)
        allowed_roots: List of allowed root directories

    Returns:
        The resolved Path if valid

    Raises:
        ValidationError: If path is invalid or outside allowed directories
    """
    # Reject relative paths
    if not path.startswith(("/", "~")) and ":" not in path:  # Allow Windows absolute paths
        raise ValidationError(f"Absolute path required, got: {path}")

    try:
        # Expand user and resolve to absolute path
        resolved = Path(path).expanduser().resolve()
    except (OSError, ValueError) as e:
        raise ValidationError(f"Invalid path: {e}")

    # Check if path is under any allowed root
    for root in allowed_roots:
        try:
            resolved.relative_to(root)
            return resolved
        except ValueError:
            continue

    raise ValidationError(
        f"Path {path} is outside allowed directories. "
        f"Allowed: {[str(r) for r in allowed_roots]}"
    )


def validate_webhook_url(url: str, settings: UserSettings) -> bool:
    """Validate a webhook URL against configured profiles.

    Args:
        url: The URL to validate
        settings: User settings containing webhook_profiles

    Returns:
        True if URL matches a profile

    Raises:
        ValidationError: If URL doesn't match any profile
    """
    if not settings.webhook_profiles:
        raise ValidationError(
            "No webhook profiles configured. "
            "Add profiles in settings before using webhook URLs."
        )

    for profile in settings.webhook_profiles:
        pattern = profile.get("url", "")
        if fnmatch.fnmatch(url, pattern):
            return True

    raise ValidationError(
        f"URL {url} is not in allowed webhook profiles. "
        f"Allowed patterns: {[p.get('url') for p in settings.webhook_profiles]}"
    )


def sanitize_tool_arg(value: str) -> str:
    """Sanitize a tool argument by removing shell metacharacters.

    Args:
        value: The string value to sanitize

    Returns:
        Sanitized string with metacharacters removed
    """
    return SHELL_METACHARACTERS.sub("", value)


def validate_tool_args(tool_name: str, args: dict) -> dict:
    """Validate tool arguments for safety.

    Args:
        tool_name: Name of the tool
        args: Dictionary of arguments

    Returns:
        Validated arguments

    Raises:
        ValidationError: If any argument contains dangerous content
    """
    validated = {}

    for key, value in args.items():
        if isinstance(value, str):
            # Check for shell metacharacters
            if SHELL_METACHARACTERS.search(value):
                raise ValidationError(
                    f"Tool '{tool_name}' argument '{key}' contains "
                    f"invalid characters: {value!r}"
                )
            validated[key] = value
        elif isinstance(value, list):
            # Validate list items
            validated[key] = [
                validate_tool_args(tool_name, {"item": v})["item"]
                if isinstance(v, str)
                else v
                for v in value
            ]
        else:
            validated[key] = value

    return validated


def build_llm_context(
    sheet_context: SheetContext,
    settings: UserSettings,
) -> LLMContext:
    """Build LLM context with consent enforcement.

    Args:
        sheet_context: The current sheet context
        settings: User settings including consent

    Returns:
        LLMContext with appropriate data based on consent

    Raises:
        ConsentError: If samples are present but consent not given
    """
    # Check consent for samples
    if sheet_context.sample_rows:
        if settings.llm_context_level != LLMContextLevel.WITH_SAMPLES:
            raise ConsentError(
                "Sample rows require explicit consent in settings. "
                "Enable 'with_samples' in Data Sharing Preferences."
            )
        if not settings.consent_given_at:
            raise ConsentError(
                "Consent timestamp missing for sample sharing. "
                "Please re-confirm Data Sharing Preferences."
            )

    # Build context based on consent level
    sample_rows = None
    if settings.llm_context_level == LLMContextLevel.WITH_SAMPLES:
        sample_rows = sheet_context.sample_rows

    return LLMContext(
        sheet_id=sheet_context.sheet_id,
        tab_name=sheet_context.tab_name,
        headers=sheet_context.headers,
        row_count=sheet_context.row_count,
        column_types=sheet_context.column_types,
        sample_rows=sample_rows,
    )


def build_llm_context_with_override(
    sheet_context: SheetContext,
    settings: UserSettings,
    override_token: SampleOverrideToken | None,
) -> LLMContext:
    """Build LLM context with optional per-request override.

    Args:
        sheet_context: The current sheet context
        settings: User settings
        override_token: Optional override token for temporary samples

    Returns:
        LLMContext with appropriate data

    Raises:
        ConsentError: If neither settings nor valid override permits samples
    """
    # If override token is valid, allow samples
    if override_token and override_token.is_valid():
        return LLMContext(
            sheet_id=sheet_context.sheet_id,
            tab_name=sheet_context.tab_name,
            headers=sheet_context.headers,
            row_count=sheet_context.row_count,
            column_types=sheet_context.column_types,
            sample_rows=sheet_context.sample_rows[:override_token.sample_count]
            if sheet_context.sample_rows
            else None,
        )

    # Fall back to normal consent check
    return build_llm_context(sheet_context, settings)
