"""
AIPTX Hydra Scanner
===================

Scanner for hydra - network login cracker.
https://github.com/vanhauser-thc/thc-hydra
"""

import asyncio
import logging
import re
import shutil
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional

from ..base import BaseScanner, ScanResult, ScanFinding, ScanSeverity

logger = logging.getLogger(__name__)


# Common services supported by hydra
HYDRA_SERVICES = {
    "ssh": {"port": 22, "description": "SSH"},
    "ftp": {"port": 21, "description": "FTP"},
    "telnet": {"port": 23, "description": "Telnet"},
    "smtp": {"port": 25, "description": "SMTP"},
    "pop3": {"port": 110, "description": "POP3"},
    "imap": {"port": 143, "description": "IMAP"},
    "mysql": {"port": 3306, "description": "MySQL"},
    "mssql": {"port": 1433, "description": "MSSQL"},
    "postgres": {"port": 5432, "description": "PostgreSQL"},
    "oracle": {"port": 1521, "description": "Oracle"},
    "rdp": {"port": 3389, "description": "RDP"},
    "vnc": {"port": 5900, "description": "VNC"},
    "smb": {"port": 445, "description": "SMB"},
    "http-get": {"port": 80, "description": "HTTP Basic Auth"},
    "http-post-form": {"port": 80, "description": "HTTP Form"},
    "https-get": {"port": 443, "description": "HTTPS Basic Auth"},
    "ldap2": {"port": 389, "description": "LDAP"},
    "snmp": {"port": 161, "description": "SNMP"},
    "redis": {"port": 6379, "description": "Redis"},
    "mongodb": {"port": 27017, "description": "MongoDB"},
}


@dataclass
class HydraConfig:
    """Configuration for hydra scanner."""

    # Target options
    service: str = "ssh"  # Service to attack
    port: Optional[int] = None  # Custom port

    # Credentials
    username: Optional[str] = None  # Single username
    username_file: Optional[str] = None  # Username wordlist
    password: Optional[str] = None  # Single password
    password_file: Optional[str] = None  # Password wordlist
    combo_file: Optional[str] = None  # user:pass combo file

    # Attack options
    tasks: int = 16  # Parallel connections
    timeout: int = 30  # Connection timeout
    wait_time: int = 0  # Wait between attempts

    # HTTP specific
    http_path: str = "/"
    http_form_data: str = ""  # For http-post-form
    http_failure_string: str = ""  # String indicating failed login
    http_success_string: str = ""  # String indicating success

    # Output
    output_file: Optional[str] = None
    verbose: bool = False

    # Safety
    exit_on_first: bool = True  # Stop on first valid cred
    loop_users: bool = False  # Try all passwords for each user


class HydraScanner(BaseScanner):
    """
    Scanner for hydra - credential brute-forcing.

    Supports:
    - SSH, FTP, Telnet, SMTP, POP3, IMAP
    - MySQL, MSSQL, PostgreSQL, Oracle
    - RDP, VNC, SMB
    - HTTP Basic/Form authentication
    - LDAP, SNMP, Redis, MongoDB

    Example:
        scanner = HydraScanner(HydraConfig(
            service="ssh",
            username_file="/path/to/users.txt",
            password_file="/path/to/passwords.txt"
        ))
        result = await scanner.scan("192.168.1.100")

        for finding in result.findings:
            print(f"Valid: {finding.evidence}")

    WARNING: Only use with explicit authorization!
    """

    # Default wordlist locations
    DEFAULT_WORDLISTS = {
        "users": [
            "/usr/share/wordlists/metasploit/unix_users.txt",
            "/usr/share/seclists/Usernames/top-usernames-shortlist.txt",
            str(Path.home() / ".aiptx/data/wordlists/users.txt"),
        ],
        "passwords": [
            "/usr/share/wordlists/rockyou.txt",
            "/usr/share/seclists/Passwords/Common-Credentials/10k-most-common.txt",
            str(Path.home() / ".aiptx/data/wordlists/passwords.txt"),
        ],
    }

    def __init__(self, config: Optional[HydraConfig] = None):
        self.config = config or HydraConfig()
        self._process: Optional[asyncio.subprocess.Process] = None
        self._running = False

    def is_available(self) -> bool:
        """Check if hydra is installed."""
        return shutil.which("hydra") is not None

    def _get_wordlist(self, wordlist_type: str) -> Optional[str]:
        """Get wordlist path."""
        for wl in self.DEFAULT_WORDLISTS.get(wordlist_type, []):
            if Path(wl).exists():
                return wl
        return None

    async def scan(
        self,
        target: str,
        service: Optional[str] = None,
        **kwargs
    ) -> ScanResult:
        """
        Run hydra credential attack.

        Args:
            target: Target host or IP
            service: Override service from config
            **kwargs: Additional options

        Returns:
            ScanResult with valid credential findings
        """
        result = ScanResult(scanner="hydra", target=target)
        result.start_time = datetime.utcnow()
        self._running = True

        # Validate inputs
        actual_service = service or self.config.service
        if actual_service not in HYDRA_SERVICES and not actual_service.startswith("http"):
            result.status = "failed"
            result.errors.append(f"Unknown service: {actual_service}")
            return result

        # Ensure we have credentials to test
        if not self._has_credentials():
            result.status = "failed"
            result.errors.append("No credentials provided (need username/password or wordlists)")
            return result

        try:
            cmd = self._build_command(target, actual_service)
            logger.debug(f"Running: {' '.join(cmd)}")

            process = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            self._process = process

            stdout, stderr = await asyncio.wait_for(
                process.communicate(),
                timeout=kwargs.get("timeout", 600)
            )

            result.raw_output = stdout.decode("utf-8", errors="replace")
            result.findings = self.parse_output(result.raw_output, actual_service, target)
            result.status = "completed"

        except asyncio.TimeoutError:
            result.status = "failed"
            result.errors.append("Attack timed out")
        except Exception as e:
            result.status = "failed"
            result.errors.append(str(e))
            logger.error(f"hydra attack failed: {e}")
        finally:
            self._running = False
            result.end_time = datetime.utcnow()
            if result.start_time:
                result.duration_seconds = (result.end_time - result.start_time).total_seconds()

        return result

    def _has_credentials(self) -> bool:
        """Check if any credentials are configured."""
        return bool(
            self.config.username or
            self.config.username_file or
            self.config.password or
            self.config.password_file or
            self.config.combo_file
        )

    def _build_command(self, target: str, service: str) -> List[str]:
        """Build hydra command."""
        cmd = ["hydra"]

        # Credentials
        if self.config.combo_file:
            cmd.extend(["-C", self.config.combo_file])
        else:
            # Username
            if self.config.username:
                cmd.extend(["-l", self.config.username])
            elif self.config.username_file:
                cmd.extend(["-L", self.config.username_file])
            else:
                # Try default wordlist
                default_users = self._get_wordlist("users")
                if default_users:
                    cmd.extend(["-L", default_users])

            # Password
            if self.config.password:
                cmd.extend(["-p", self.config.password])
            elif self.config.password_file:
                cmd.extend(["-P", self.config.password_file])
            else:
                # Try default wordlist
                default_pass = self._get_wordlist("passwords")
                if default_pass:
                    cmd.extend(["-P", default_pass])

        # Options
        cmd.extend(["-t", str(self.config.tasks)])
        cmd.extend(["-w", str(self.config.timeout)])

        if self.config.wait_time > 0:
            cmd.extend(["-W", str(self.config.wait_time)])

        if self.config.exit_on_first:
            cmd.append("-f")

        if self.config.loop_users:
            cmd.append("-u")

        if self.config.verbose:
            cmd.append("-V")

        if self.config.output_file:
            cmd.extend(["-o", self.config.output_file])

        # Port
        port = self.config.port
        if not port and service in HYDRA_SERVICES:
            port = HYDRA_SERVICES[service]["port"]
        if port:
            cmd.extend(["-s", str(port)])

        # Target and service
        cmd.append(target)

        # Service with optional path/form data
        if service == "http-post-form":
            form_string = self._build_http_form_string()
            cmd.append(f"http-post-form")
            cmd.append(form_string)
        elif service == "http-get":
            cmd.append(f"http-get")
            if self.config.http_path:
                cmd.append(self.config.http_path)
        else:
            cmd.append(service)

        return cmd

    def _build_http_form_string(self) -> str:
        """Build HTTP form attack string."""
        # Format: "/path:user=^USER^&pass=^PASS^:F=failure_string"
        path = self.config.http_path or "/login"
        form_data = self.config.http_form_data or "username=^USER^&password=^PASS^"

        if self.config.http_failure_string:
            return f"{path}:{form_data}:F={self.config.http_failure_string}"
        elif self.config.http_success_string:
            return f"{path}:{form_data}:S={self.config.http_success_string}"
        else:
            return f"{path}:{form_data}:F=incorrect"

    def parse_output(self, output: str, service: str, target: str) -> List[ScanFinding]:
        """Parse hydra output."""
        findings = []

        # Pattern for successful logins
        # [22][ssh] host: 192.168.1.1   login: admin   password: admin123
        cred_pattern = re.compile(
            r"\[(\d+)\]\[([^\]]+)\]\s+host:\s*(\S+)\s+login:\s*(\S+)\s+password:\s*(\S*)",
            re.IGNORECASE
        )

        for match in cred_pattern.finditer(output):
            port, svc, host, username, password = match.groups()

            findings.append(ScanFinding(
                title=f"Valid Credentials: {svc.upper()}",
                severity=ScanSeverity.CRITICAL,
                description=f"Service: {svc} on port {port}",
                host=host,
                port=int(port),
                evidence=f"Username: {username}, Password: {password}",
                cwe="CWE-521",  # Weak Password Requirements
                scanner="hydra",
                tags=["credentials", svc.lower(), "brute-force"],
            ))

        # Check for completion without findings
        if not findings:
            if "1 valid password found" in output.lower():
                # Sometimes output format differs
                findings.append(ScanFinding(
                    title=f"Valid Credentials Found: {service.upper()}",
                    severity=ScanSeverity.CRITICAL,
                    description="Valid credentials discovered - check raw output",
                    host=target,
                    cwe="CWE-521",
                    scanner="hydra",
                    tags=["credentials", service.lower()],
                ))
            elif "0 valid passwords found" in output.lower():
                # No valid creds - still useful info
                findings.append(ScanFinding(
                    title=f"No Valid Credentials: {service.upper()}",
                    severity=ScanSeverity.INFO,
                    description="Brute-force completed without valid credentials",
                    host=target,
                    scanner="hydra",
                    tags=["no-findings", service.lower()],
                ))

        return findings

    async def stop(self) -> bool:
        """Stop running attack."""
        if self._process and self._running:
            self._process.terminate()
            try:
                await asyncio.wait_for(self._process.wait(), timeout=5)
            except asyncio.TimeoutError:
                self._process.kill()
            self._running = False
            return True
        return False


class HydraAttackBuilder:
    """
    Builder for common hydra attack configurations.

    Example:
        attack = (HydraAttackBuilder()
            .target("192.168.1.100")
            .ssh()
            .with_users(["admin", "root"])
            .with_password_file("/path/to/passwords.txt")
            .build())

        result = await attack.scan()
    """

    def __init__(self):
        self._target: str = ""
        self._config = HydraConfig()

    def target(self, host: str) -> "HydraAttackBuilder":
        """Set target host."""
        self._target = host
        return self

    def ssh(self) -> "HydraAttackBuilder":
        """Configure for SSH."""
        self._config.service = "ssh"
        self._config.port = 22
        return self

    def ftp(self) -> "HydraAttackBuilder":
        """Configure for FTP."""
        self._config.service = "ftp"
        self._config.port = 21
        return self

    def rdp(self) -> "HydraAttackBuilder":
        """Configure for RDP."""
        self._config.service = "rdp"
        self._config.port = 3389
        return self

    def mysql(self) -> "HydraAttackBuilder":
        """Configure for MySQL."""
        self._config.service = "mysql"
        self._config.port = 3306
        return self

    def http_basic(self, path: str = "/") -> "HydraAttackBuilder":
        """Configure for HTTP Basic Auth."""
        self._config.service = "http-get"
        self._config.http_path = path
        return self

    def http_form(
        self,
        path: str,
        form_data: str,
        failure_string: str = "incorrect"
    ) -> "HydraAttackBuilder":
        """Configure for HTTP Form Auth."""
        self._config.service = "http-post-form"
        self._config.http_path = path
        self._config.http_form_data = form_data
        self._config.http_failure_string = failure_string
        return self

    def port(self, port: int) -> "HydraAttackBuilder":
        """Set custom port."""
        self._config.port = port
        return self

    def with_user(self, username: str) -> "HydraAttackBuilder":
        """Set single username."""
        self._config.username = username
        return self

    def with_users(self, usernames: List[str]) -> "HydraAttackBuilder":
        """Set multiple usernames (writes temp file)."""
        # In production, would write to temp file
        # For now, use first user
        if usernames:
            self._config.username = usernames[0]
        return self

    def with_user_file(self, path: str) -> "HydraAttackBuilder":
        """Set username wordlist."""
        self._config.username_file = path
        return self

    def with_password(self, password: str) -> "HydraAttackBuilder":
        """Set single password."""
        self._config.password = password
        return self

    def with_password_file(self, path: str) -> "HydraAttackBuilder":
        """Set password wordlist."""
        self._config.password_file = path
        return self

    def with_combo_file(self, path: str) -> "HydraAttackBuilder":
        """Set user:pass combo file."""
        self._config.combo_file = path
        return self

    def tasks(self, count: int) -> "HydraAttackBuilder":
        """Set parallel connections."""
        self._config.tasks = min(count, 64)  # Cap at 64
        return self

    def stop_on_first(self, enabled: bool = True) -> "HydraAttackBuilder":
        """Stop on first valid credential."""
        self._config.exit_on_first = enabled
        return self

    def build(self) -> HydraScanner:
        """Build configured scanner."""
        return HydraScanner(config=self._config)

    async def scan(self) -> ScanResult:
        """Build and run scan."""
        scanner = self.build()
        return await scanner.scan(self._target)
