"""Detection rule execution with security validation."""

from __future__ import annotations

import fnmatch
from dataclasses import dataclass, field
from datetime import UTC, datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any

import httpx
import psutil
import structlog

from curadise_agent.remediation.models import DetectionResult, DetectionRule
from curadise_agent.remediation.security import (
    SecurityError,
    compile_regex_safe,
    execute_command_safe,
    read_file_tail,
    validate_path_security,
    validate_url_security,
)

if TYPE_CHECKING:
    from curadise_agent.remediation.config import RemediationConfig

log = structlog.get_logger(__name__)


@dataclass
class Detector:
    """
    Executes detection rules with security validation.

    Supports four check types:
    - command: Run a shell command and check exit code/output
    - http: Make HTTP request and check status/response
    - process: Check if a process is running
    - log_pattern: Search for patterns in log files
    """

    config: RemediationConfig
    _used_nonces: set[str] = field(default_factory=set, init=False)

    def validate_command(self, command: str, is_remediation: bool = False) -> bool:
        """
        Validate command against allowlist.

        Args:
            command: Command to validate
            is_remediation: If True, check remediation_allowlist; else detection_allowlist

        Returns:
            True if command is allowed
        """
        allowlist = (
            self.config.remediation_allowlist
            if is_remediation
            else self.config.detection_allowlist
        )

        return any(fnmatch.fnmatch(command, pattern) for pattern in allowlist)

    def validate_http_host(self, url: str) -> tuple[bool, str | None]:
        """
        Validate HTTP URL against security policies (prevent SSRF).

        Security measures:
        1. Only allow http/https schemes
        2. Block internal/private IP addresses
        3. Resolve DNS and check resolved IPs
        4. Match against explicit hostname allowlist

        Args:
            url: URL to validate

        Returns:
            Tuple of (is_valid, resolved_ip)
            - is_valid: True if URL passes all security checks
            - resolved_ip: The resolved IP address (for DNS rebinding prevention)
        """
        try:
            validated = validate_url_security(
                url=url,
                allowed_hosts=self.config.allowed_http_hosts,
                resolve_dns=True,
                allowed_schemes=frozenset({"http", "https"}),
            )
            return True, validated.resolved_ip
        except SecurityError as e:
            log.warning(
                "url_validation_failed",
                url=url,
                error=str(e),
            )
            return False, None
        except Exception as e:
            log.error(
                "url_validation_error",
                url=url,
                error=str(e),
            )
            return False, None

    def validate_log_path(self, path: str) -> tuple[bool, str | None]:
        """
        Validate log file path against allowed paths (prevent path traversal).

        Security measures:
        1. Reject paths containing ".." before resolution
        2. Resolve symlinks using os.path.realpath()
        3. Resolve allowed prefixes too (they might contain symlinks)
        4. Check that resolved path starts with an allowed prefix
        5. Verify path exists and is a regular file

        Args:
            path: File path to validate

        Returns:
            Tuple of (is_valid, resolved_path)
            - is_valid: True if path passes all security checks
            - resolved_path: The resolved absolute path
        """
        try:
            resolved = validate_path_security(
                path=path,
                allowed_prefixes=self.config.allowed_log_paths,
                must_exist=False,  # File might not exist yet
                must_be_file=False,  # Will check separately when reading
            )
            return True, resolved
        except SecurityError as e:
            log.warning(
                "path_validation_failed",
                path=path,
                error=str(e),
            )
            return False, None
        except Exception as e:
            log.error(
                "path_validation_error",
                path=path,
                error=str(e),
            )
            return False, None

    async def run_check(self, rule: DetectionRule) -> DetectionResult:
        """
        Run a detection check for a rule.

        Args:
            rule: Detection rule to check

        Returns:
            DetectionResult with check outcome
        """
        started_at = datetime.now(UTC)

        try:
            if rule.type == "command":
                passed, message, details = await self._check_command(rule)
            elif rule.type == "http":
                passed, message, details = await self._check_http(rule)
            elif rule.type == "process":
                passed, message, details = await self._check_process(rule)
            elif rule.type == "log_pattern":
                passed, message, details = await self._check_log_pattern(rule)
            else:
                passed = False
                message = f"Unknown check type: {rule.type}"
                details = {}

        except Exception as e:
            log.error(
                "detection_check_error",
                rule_id=rule.id,
                rule_type=rule.type,
                error=str(e),
            )
            passed = False
            message = f"Check failed with error: {e}"
            details = {"error": str(e)}

        completed_at = datetime.now(UTC)
        duration_ms = (completed_at - started_at).total_seconds() * 1000

        return DetectionResult(
            rule_id=rule.id,
            rule_name=rule.name,
            check_type=rule.type,
            passed=passed,
            message=message,
            details=details,
            checked_at=completed_at,
            duration_ms=duration_ms,
        )

    async def _check_command(
        self, rule: DetectionRule
    ) -> tuple[bool, str | None, dict[str, Any]]:
        """Execute command check using secure subprocess execution."""
        check = rule.check
        command = check.get("command", "")
        expected_exit_code = check.get("expected_exit_code", 0)
        expected_output = check.get("expected_output")
        timeout = check.get("timeout", 30.0)

        # Validate command against allowlist
        if not self.validate_command(command, is_remediation=False):
            return (
                False,
                f"Command not in detection allowlist: {command}",
                {"rejected": True, "command": command},
            )

        try:
            # Use secure command execution (subprocess_exec, not shell)
            exit_code, stdout_str, stderr_str = await execute_command_safe(
                command=command,
                timeout=timeout,
            )

            stdout_str = stdout_str.strip()
            stderr_str = stderr_str.strip()

            details = {
                "exit_code": exit_code,
                "stdout": stdout_str[:1000],  # Truncate for safety
                "stderr": stderr_str[:500],
            }

            # Check exit code
            if exit_code != expected_exit_code:
                return (
                    False,
                    f"Exit code {exit_code} != expected {expected_exit_code}",
                    details,
                )

            # Check output if specified
            if expected_output is not None:
                if expected_output not in stdout_str:
                    return (
                        False,
                        f"Expected output not found: {expected_output}",
                        details,
                    )

            return True, None, details

        except SecurityError as e:
            return (
                False,
                f"Command rejected by security validation: {e}",
                {"rejected": True, "command": command, "security_error": str(e)},
            )
        except TimeoutError:
            return False, f"Command timed out after {timeout}s", {"timeout": True}
        except Exception as e:
            return False, f"Command execution failed: {e}", {"error": str(e)}

    async def _check_http(
        self, rule: DetectionRule
    ) -> tuple[bool, str | None, dict[str, Any]]:
        """Execute HTTP check with SSRF prevention and DNS rebinding protection."""
        check = rule.check
        url = check.get("url", "")
        method = check.get("method", "GET").upper()
        expected_status = check.get("expected_status", 200)
        timeout = check.get("timeout", 10.0)
        headers = dict(check.get("headers", {}))
        body = check.get("body")

        # Validate URL with SSRF prevention (validates scheme, IP, DNS resolution)
        is_valid, resolved_ip = self.validate_http_host(url)
        if not is_valid:
            return (
                False,
                f"HTTP URL failed security validation: {url}",
                {"rejected": True, "url": url},
            )

        try:
            # DNS rebinding prevention: if we resolved an IP, use it directly
            # and set the original Host header
            request_url = url
            from urllib.parse import urlparse, urlunparse

            if resolved_ip:
                parsed = urlparse(url)
                # Store original host for Host header
                original_host = parsed.hostname
                port_suffix = f":{parsed.port}" if parsed.port else ""

                # Replace hostname with resolved IP in URL
                # This prevents DNS rebinding attacks
                new_netloc = f"{resolved_ip}{port_suffix}"
                request_url = urlunparse(
                    (parsed.scheme, new_netloc, parsed.path, parsed.params, parsed.query, parsed.fragment)
                )

                # Set Host header to original hostname
                headers["Host"] = f"{original_host}{port_suffix}"

            async with httpx.AsyncClient(timeout=timeout) as client:
                response = await client.request(
                    method=method,
                    url=request_url,
                    headers=headers,
                    content=body.encode() if body else None,
                )

                details = {
                    "status_code": response.status_code,
                    "response_time_ms": response.elapsed.total_seconds() * 1000
                    if response.elapsed
                    else 0,
                    "content_length": len(response.content),
                    "resolved_ip": resolved_ip,
                }

                if response.status_code != expected_status:
                    return (
                        False,
                        f"Status {response.status_code} != expected {expected_status}",
                        details,
                    )

                return True, None, details

        except httpx.TimeoutException:
            return False, f"HTTP request timed out after {timeout}s", {"timeout": True}
        except Exception as e:
            return False, f"HTTP request failed: {e}", {"error": str(e)}

    async def _check_process(
        self, rule: DetectionRule
    ) -> tuple[bool, str | None, dict[str, Any]]:
        """Execute process check."""
        check = rule.check
        process_name = check.get("name", "")
        min_count = check.get("min_count", 1)
        max_count = check.get("max_count")  # Optional upper bound

        try:
            # Find matching processes
            matching_pids = []
            for proc in psutil.process_iter(["pid", "name", "cmdline"]):
                try:
                    proc_info = proc.info
                    name = proc_info.get("name", "")
                    cmdline = proc_info.get("cmdline") or []

                    # Match by name or cmdline
                    if name == process_name or any(process_name in arg for arg in cmdline):
                        matching_pids.append(proc_info["pid"])
                except (psutil.NoSuchProcess, psutil.AccessDenied):
                    continue

            count = len(matching_pids)
            details = {
                "process_name": process_name,
                "count": count,
                "pids": matching_pids[:10],  # Limit for safety
            }

            # Check minimum count
            if count < min_count:
                return (
                    False,
                    f"Process count {count} < minimum {min_count}",
                    details,
                )

            # Check maximum count if specified
            if max_count is not None and count > max_count:
                return (
                    False,
                    f"Process count {count} > maximum {max_count}",
                    details,
                )

            return True, None, details

        except Exception as e:
            return False, f"Process check failed: {e}", {"error": str(e)}

    async def _check_log_pattern(
        self, rule: DetectionRule
    ) -> tuple[bool, str | None, dict[str, Any]]:
        """Execute log pattern check with secure path validation and ReDoS protection."""
        check = rule.check
        file_path = check.get("file", "")
        pattern = check.get("pattern", "")
        since = check.get("since", "5m")  # Time window
        expect_match = check.get("expect_match", False)  # False = alert if pattern found
        max_lines = check.get("max_lines", 10000)

        # Validate file path with secure path validation
        is_valid, resolved_path = self.validate_log_path(file_path)
        if not is_valid or not resolved_path:
            return (
                False,
                f"Log path failed security validation: {file_path}",
                {"rejected": True, "path": file_path},
            )

        try:
            path = Path(resolved_path)
            if not path.exists():
                return (
                    False,
                    f"Log file not found: {file_path}",
                    {"error": "file_not_found"},
                )

            if not path.is_file():
                return (
                    False,
                    f"Path is not a regular file: {file_path}",
                    {"error": "not_a_file"},
                )

            # Parse time window (reserved for future time-based filtering)
            _ = self._parse_duration(since)

            # Compile regex with ReDoS protection
            try:
                compiled_pattern = compile_regex_safe(pattern)
            except SecurityError as e:
                return (
                    False,
                    f"Invalid regex pattern: {e}",
                    {"rejected": True, "pattern": pattern, "error": str(e)},
                )

            # Read file using bounded tail reading (prevents memory exhaustion)
            try:
                lines = await read_file_tail(
                    path=resolved_path,
                    max_lines=max_lines,
                    max_line_length=10000,
                )
            except Exception as e:
                return (
                    False,
                    f"Failed to read log file: {e}",
                    {"error": str(e)},
                )

            matches = []
            lines_checked = 0

            for line in lines:
                lines_checked += 1
                if compiled_pattern.search(line):
                    matches.append(line[:200])  # Truncate match
                    if len(matches) >= 10:  # Limit stored matches
                        break

            details = {
                "file": file_path,
                "resolved_path": resolved_path,
                "pattern": pattern,
                "lines_checked": lines_checked,
                "match_count": len(matches),
                "matches": matches[:5],  # Return first 5 matches
            }

            # Determine pass/fail
            has_match = len(matches) > 0

            if expect_match:
                # We expect to find the pattern (health check pattern)
                if has_match:
                    return True, None, details
                else:
                    return False, f"Expected pattern not found: {pattern}", details
            else:
                # We expect NOT to find the pattern (error detection)
                if has_match:
                    return (
                        False,
                        f"Pattern found {len(matches)} times: {pattern}",
                        details,
                    )
                else:
                    return True, None, details

        except Exception as e:
            return False, f"Log pattern check failed: {e}", {"error": str(e)}

    def _parse_duration(self, duration: str) -> float:
        """Parse duration string to seconds (e.g., '5m', '1h', '30s')."""
        units = {"s": 1, "m": 60, "h": 3600, "d": 86400}

        try:
            if duration[-1] in units:
                return float(duration[:-1]) * units[duration[-1]]
            return float(duration)
        except (ValueError, IndexError):
            return 300.0  # Default 5 minutes


def create_detector(config: RemediationConfig) -> Detector:
    """
    Factory function to create a detector.

    Args:
        config: Remediation configuration

    Returns:
        Configured Detector
    """
    return Detector(config=config)
