#!/usr/bin/env python3
"""
souleyez.plugins.gobuster
"""
from __future__ import annotations
import subprocess
import time
import re
from typing import List

from .plugin_base import PluginBase
from souleyez.security.validation import validate_url, ValidationError

HELP = {
    "name": "Gobuster — Directory, File & DNS/VHost Brute-Force Tool",
    "description": (
        "Need a blunt but useful tool to knock on every web door?\n\n"
        "Gobuster brute-forces directories, files, and DNS/vhost names fast — great for finding hidden admin panels, forgotten endpoints, "
        "and virtual hosts that don't show up in normal browsing. It's a workhorse for directory discovery and DNS enumeration, and pairs "
        "nicely with targeted scanners once you know where the doors are.\n\n"
        "Gobuster doesn't exploit anything — it simply probes paths and names based on wordlists and reports what responds. That means it "
        "can be loud and produce lots of hits, so tune your wordlists and request rate to avoid overwhelm (and to stay polite to targets).\n\n"
        "Quick tips:\n"
        "- Use focused wordlists to reduce noise and false positives; start small, then expand.\n"
        "- Combine with Nmap/service scans: discovered paths → probe with service checks and vulnerability scans.\n"
        "- For vhosts, try common virtual-host wordlists and inspect HTTP response headers for clues.\n"
        "- Respect rate limits and the target's rules of engagement — brute forcing can trigger alerts.\n"
        "- Save findings (responses, status codes, and URLs) to the job log so you can convert them into Findings or follow-up tasks.\n"
    ),
    "usage": "souleyez jobs enqueue gobuster <target> --args \"dir -u <url> -w <wordlist> -t <threads>\"",
    "examples": [
        "souleyez jobs enqueue gobuster http://example.com --args \"dir -u http://example.com -w data/wordlists/web_dirs_common.txt -t 10\"",
        "souleyez jobs enqueue gobuster http://example.com --args \"dir -u http://example.com -w data/wordlists/web_dirs_common.txt -x php,txt,html -t 20\"",
        "souleyez jobs enqueue gobuster example.com --args \"dns --domain example.com -w data/wordlists/subdomains_common.txt -t 15 --timeout 3s\"",
        "souleyez jobs enqueue gobuster http://example.com --args \"vhost -u http://example.com -w data/wordlists/subdomains_common.txt -t 50\"",
    ],
    "flags": [
        ["dir", "Directory/file enumeration mode"],
        ["dns", "DNS subdomain enumeration mode"],
        ["vhost", "Virtual host discovery mode"],
        ["-u <url>", "Target URL (dir/vhost modes)"],
        ["--domain <domain>", "Target domain (dns mode)"],
        ["-w <wordlist>", "Wordlist path"],
        ["-t <threads>", "Number of threads"],
        ["--timeout <duration>", "DNS timeout per request (e.g., 3s, 5s) - useful to avoid DNS timeouts"],
        ["-x <extensions>", "File extensions to check (comma-separated)"],
        ["-b <codes>", "Status codes to blacklist"],
        ["--wildcard", "Force continued operation when wildcard found"],
    ],
    "preset_categories": {
        "directory_enum": [
            {
                "name": "Quick Scan",
                "args": ["dir", "-u", "<target>", "-w", "data/wordlists/web_dirs_common.txt", "-t", "10"],
                "desc": "Common wordlist (87 entries)"
            },
            {
                "name": "Standard Scan",
                "args": ["dir", "-u", "<target>", "-w", "data/wordlists/web_dirs_extended.txt", "-t", "20"],
                "desc": "Extended wordlist (more coverage)"
            },
            {
                "name": "Common Files (HTML/PHP/TXT)",
                "args": ["dir", "-u", "<target>", "-w", "data/wordlists/web_dirs_common.txt", "-x", "html,htm,php,txt,xml,json", "-t", "15"],
                "desc": "Common paths + web file extensions"
            },
            {
                "name": "PHP Extensions",
                "args": ["dir", "-u", "<target>", "-w", "data/wordlists/web_dirs_common.txt", "-x", "php,phps,php3,php4,php5,phtml", "-t", "15"],
                "desc": "Common paths + PHP variants"
            }
        ],
        "subdomain_enum": [
            {
                "name": "Subdomain Scan (manual domain)",
                "args": ["dns", "--domain", "example.com", "-w", "data/wordlists/subdomains_common.txt", "-t", "15", "--timeout", "3s"],
                "desc": "Subdomain enumeration - EDIT example.com to your domain"
            }
        ],
        "vhost_discovery": [
            {
                "name": "Virtual Hosts",
                "args": ["vhost", "-u", "<target>", "-w", "data/wordlists/subdomains_common.txt", "-t", "50"],
                "desc": "Virtual host discovery"
            }
        ]
    },
    "presets": [
        # Flattened list for backward compatibility
        {"name": "Quick Scan", "args": ["dir", "-u", "<target>", "-w", "data/wordlists/web_dirs_common.txt", "-t", "10"], "desc": "Common wordlist (87 entries)"},
        {"name": "Standard Scan", "args": ["dir", "-u", "<target>", "-w", "data/wordlists/web_dirs_extended.txt", "-t", "20"], "desc": "Extended wordlist (more coverage)"},
        {"name": "Common Files (HTML/PHP/TXT)", "args": ["dir", "-u", "<target>", "-w", "data/wordlists/web_dirs_common.txt", "-x", "html,htm,php,txt,xml,json", "-t", "15"], "desc": "Common paths + web file extensions"},
        {"name": "PHP Extensions", "args": ["dir", "-u", "<target>", "-w", "data/wordlists/web_dirs_common.txt", "-x", "php,phps,php3,php4,php5,phtml", "-t", "15"], "desc": "Common paths + PHP variants"},
        {"name": "Subdomain Scan (manual domain)", "args": ["dns", "--domain", "example.com", "-w", "data/wordlists/subdomains_common.txt", "-t", "15", "--timeout", "3s"], "desc": "Subdomain enumeration - EDIT example.com to your domain"},
        {"name": "Virtual Hosts", "args": ["vhost", "-u", "<target>", "-w", "data/wordlists/subdomains_common.txt", "-t", "50"], "desc": "Virtual host discovery"}
    ],
    "help_sections": [
        {
            "title": "What is Gobuster?",
            "color": "cyan",
            "content": [
                {"title": "Overview", "desc": "Gobuster is a fast directory, file, and DNS/vhost brute-forcing tool perfect for discovering hidden web content and subdomains."},
                {"title": "Use Cases", "desc": "Essential for web application reconnaissance", "tips": [
                    "Find hidden directories and files (admin panels, config files)",
                    "Discover DNS subdomains",
                    "Enumerate virtual hosts",
                    "Identify forgotten endpoints and backups"
                ]}
            ]
        },
        {
            "title": "How to Use",
            "color": "green",
            "content": [
                {"title": "Basic Workflow", "desc": "1. Start with common wordlist for quick wins\n     2. Use file extensions (-x) for targeted discovery\n     3. Expand to larger wordlists if needed\n     4. Follow up discovered paths with manual testing"},
                {"title": "Modes & Features", "desc": "Three main scanning modes", "tips": [
                    "dir: Directory/file enumeration on web servers",
                    "dns: Subdomain brute-forcing",
                    "vhost: Virtual host discovery",
                    "Use -b to blacklist status codes (403, 404)"
                ]}
            ]
        },
        {
            "title": "Tips & Best Practices",
            "color": "yellow",
            "content": [
                ("Best Practices:", [
                    "Start with small wordlists, expand as needed",
                    "Use -x for file extensions (php,txt,html)",
                    "Respect rate limits to avoid overwhelming targets",
                    "Save results and convert findings to job log",
                    "Combine with Nmap service scans for context"
                ]),
                ("Common Issues:", [
                    "Wildcard responses: Use -b to filter status codes",
                    "Too many results: Filter by size (-fs) or words (-fw)",
                    "DNS timeouts: Add --timeout 3s for DNS mode",
                    "No results: Try different wordlists or extensions"
                ])
            ]
        }
    ]
}


class GobusterPlugin(PluginBase):
    name = "Gobuster"
    tool = "gobuster"
    category = "scanning"
    HELP = HELP

    def build_command(self, target: str, args: List[str] = None, label: str = "", log_path: str = None):
        """Build gobuster command for background execution with PID tracking."""
        args = args or []
        
        # Detect the mode from args
        mode = None
        if 'dir' in args:
            mode = 'dir'
        elif 'dns' in args:
            mode = 'dns'
        elif 'vhost' in args:
            mode = 'vhost'
        
        # Validate target and mode compatibility (same validation as run())
        if mode == 'dns':
            if target.startswith(('http://', 'https://')):
                if log_path:
                    with open(log_path, 'w') as f:
                        f.write("ERROR: DNS mode requires a domain name, not a URL\n")
                return None
            
            ip_pattern = re.compile(r'^(\d{1,3}\.){3}\d{1,3}$')
            if ip_pattern.match(target):
                if log_path:
                    with open(log_path, 'w') as f:
                        f.write("ERROR: DNS mode requires a domain name, not an IP address\n")
                return None
        
        elif mode in ['dir', 'vhost']:
            if not target.startswith(('http://', 'https://')):
                target = f"http://{target}"
            
            try:
                target = validate_url(target)
            except ValidationError as e:
                if log_path:
                    with open(log_path, 'w') as f:
                        f.write(f"ERROR: Invalid URL: {e}\n")
                return None
        
        else:
            if not target.startswith(('http://', 'https://')):
                target = f"http://{target}"
            
            try:
                target = validate_url(target)
            except ValidationError as e:
                if log_path:
                    with open(log_path, 'w') as f:
                        f.write(f"ERROR: Invalid URL: {e}\n")
                return None

        processed_args = [arg.replace("<target>", target) for arg in args]

        # Add --no-progress to suppress verbose progress output (gobuster v3.6+)
        # This prevents thousands of "Progress: X / Y" lines in output
        if '--no-progress' not in processed_args:
            processed_args.append('--no-progress')

        cmd = ["gobuster"] + processed_args

        return {
            'cmd': cmd,
            'timeout': 1800  # 30 minutes
        }

    def run(self, target: str, args: List[str] = None, label: str = "", log_path: str = None) -> int:
        """Execute gobuster scan and write output to log_path."""
        
        args = args or []
        
        # Detect the mode from args
        mode = None
        if 'dir' in args:
            mode = 'dir'
        elif 'dns' in args:
            mode = 'dns'
        elif 'vhost' in args:
            mode = 'vhost'
        
        # Validate target and mode compatibility
        if mode == 'dns':
            # DNS mode requires a domain name, not an IP or URL
            if target.startswith(('http://', 'https://')):
                error_msg = (
                    "ERROR: DNS mode requires a domain name, not a URL.\n\n"
                    f"You provided: {target}\n"
                    "DNS mode enumerates subdomains of a domain.\n\n"
                    "FIXES:\n"
                    "1. For DNS enumeration:\n"
                    "   - Use: souleyez jobs enqueue gobuster <domain>\n"
                    "   - Example: souleyez jobs enqueue gobuster vulnweb.com --args \"dns --domain vulnweb.com -w <wordlist>\"\n\n"
                    "2. For directory enumeration of an IP/URL:\n"
                    "   - Use: souleyez jobs enqueue gobuster <ip-or-url> --args \"dir -u http://<ip> -w <wordlist>\"\n"
                    "   - Example: souleyez jobs enqueue gobuster 44.228.249.3 --args \"dir -u http://44.228.249.3 -w <wordlist>\"\n"
                )
                if log_path:
                    with open(log_path, 'w') as f:
                        f.write(error_msg)
                    return 1
                raise ValueError(error_msg)
            
            # Check if target is an IP address
            ip_pattern = re.compile(r'^(\d{1,3}\.){3}\d{1,3}$')
            if ip_pattern.match(target):
                error_msg = (
                    f"ERROR: DNS mode requires a domain name, not an IP address.\n\n"
                    f"You provided: {target}\n"
                    "DNS mode enumerates subdomains (e.g., admin.example.com).\n"
                    "IP addresses don't have subdomains.\n\n"
                    "FIXES:\n"
                    "1. For directory enumeration of the IP:\n"
                    f"   souleyez jobs enqueue gobuster {target} --args \"dir -u http://{target} -w <wordlist>\"\n\n"
                    "2. For DNS enumeration (if you have a domain):\n"
                    "   souleyez jobs enqueue gobuster example.com --args \"dns --domain example.com -w <wordlist>\"\n"
                )
                if log_path:
                    with open(log_path, 'w') as f:
                        f.write(error_msg)
                    return 1
                raise ValueError(error_msg)
        
        elif mode in ['dir', 'vhost']:
            # Dir/vhost modes need a URL with protocol
            if not target.startswith(('http://', 'https://')):
                target = f"http://{target}"
            
            # Validate URL
            try:
                target = validate_url(target)
            except ValidationError as e:
                if log_path:
                    with open(log_path, 'w') as f:
                        f.write(f"ERROR: Invalid URL: {e}\n")
                    return 1
                raise ValueError(f"Invalid URL: {e}")
        
        else:
            # No mode detected, try to handle as URL
            if not target.startswith(('http://', 'https://')):
                target = f"http://{target}"
            
            try:
                target = validate_url(target)
            except ValidationError as e:
                if log_path:
                    with open(log_path, 'w') as f:
                        f.write(f"ERROR: Invalid URL: {e}\n")
                    return 1
                raise ValueError(f"Invalid URL: {e}")

        processed_args = [arg.replace("<target>", target) for arg in args]

        # Add --no-progress to suppress verbose progress output (gobuster v3.6+)
        # This prevents thousands of "Progress: X / Y" lines in output
        if '--no-progress' not in processed_args:
            processed_args.append('--no-progress')

        cmd = ["gobuster"] + processed_args

        if not log_path:
            try:
                proc = subprocess.run(cmd, capture_output=True, timeout=300, check=False)
                return proc.returncode
            except Exception:
                return 1

        try:
            with open(log_path, "a", encoding="utf-8", errors="replace") as fh:
                fh.write(f"Command: {' '.join(cmd)}\n")
                fh.write(f"Started: {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())}\n\n")
                fh.flush()

                proc = subprocess.run(
                    cmd,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.STDOUT,
                    timeout=300,
                    check=False,
                    text=True
                )
                
                output = proc.stdout
                fh.write(output)

                fh.write(f"\nCompleted: {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())}\n")
                fh.write(f"Exit Code: {proc.returncode}\n")
                
                # Check for wildcard error in output
                if proc.returncode == 1 and "the server returns a status code that matches" in output.lower():
                    # Extract details from error message
                    status_match = re.search(r'=> (\d{3})', output)
                    length_match = re.search(r'\(Length: (\d+)\)', output)
                    
                    status_code = status_match.group(1) if status_match else "403"
                    length = length_match.group(1) if length_match else "unknown"
                    
                    fh.write("\n" + "=" * 70 + "\n")
                    fh.write("⚠️  WILDCARD RESPONSE DETECTED\n")
                    fh.write("=" * 70 + "\n\n")
                    fh.write("The server is returning the same response for all URLs (wildcard).\n")
                    fh.write("Gobuster cannot differentiate between real and fake directories.\n\n")
                    fh.write(f"Detected Response:\n")
                    fh.write(f"  Status Code: {status_code}\n")
                    fh.write(f"  Response Length: {length} bytes\n\n")
                    fh.write("SUGGESTED FIXES:\n")
                    fh.write("  Option 1: Exclude the status code\n")
                    fh.write(f"    Add: -b {status_code}\n\n")
                    fh.write("  Option 2: Exclude the response length\n")
                    fh.write(f"    Add: --exclude-length {length}\n\n")
                    fh.write("  Option 3: Use wildcard mode (force continue)\n")
                    fh.write("    Add: --wildcard\n\n")
                    fh.write("RETRY COMMAND:\n")
                    retry_cmd = cmd.copy()
                    retry_cmd.extend(['-b', status_code])
                    fh.write(f"  {' '.join(retry_cmd)}\n\n")
                    fh.write("TIP: You can add these flags when configuring the scan in the\n")
                    fh.write("     interactive menu under 'Additional flags'.\n")
                    fh.write("=" * 70 + "\n")
                
                # Treat wildcard detection (exit 1) as informational success
                # since we've provided helpful guidance
                return 0 if proc.returncode in [0, 1] else proc.returncode

        except subprocess.TimeoutExpired:
            with open(log_path, "a", encoding="utf-8", errors="replace") as fh:
                fh.write("\nERROR: Gobuster timed out after 300 seconds\n")
            return 124

        except FileNotFoundError:
            with open(log_path, "a", encoding="utf-8", errors="replace") as fh:
                fh.write("\nERROR: gobuster not found in PATH\n")
            return 127

        except Exception as e:
            with open(log_path, "a", encoding="utf-8", errors="replace") as fh:
                fh.write(f"\nERROR: {type(e).__name__}: {e}\n")
            return 1


plugin = GobusterPlugin()
