#!/usr/bin/env python3
"""
souleyez.ui.live_formatters - Transform raw tool output into user-friendly live summaries

Instead of showing raw log output, these formatters parse tool output in real-time
and present it as clean, categorized summaries with icons and progress indicators.
"""

import re
from typing import Callable, Optional

import click

# Registry of tool formatters
FORMATTERS: dict[str, Callable] = {}

# Severity color mapping
SEVERITY_COLORS = {
    "critical": ("red", True),  # (color, bold)
    "high": ("red", False),
    "medium": ("yellow", False),
    "low": ("bright_black", False),
    "info": ("cyan", False),
}


def severity_style(text: str, severity: str) -> str:
    """Apply color styling based on severity level."""
    severity = severity.lower()
    if severity in SEVERITY_COLORS:
        color, bold = SEVERITY_COLORS[severity]
        return click.style(text, fg=color, bold=bold)
    return text


def render_progress_bar(current: int, total: int, width: int = 20) -> str:
    """
    Render a text-based progress bar.

    Example: [████████░░░░░░░░░░░░] 42%
    """
    if total <= 0:
        return ""

    percent = min(100, int((current / total) * 100))
    filled = int((current / total) * width)
    empty = width - filled

    bar = "█" * filled + "░" * empty
    return f"[{bar}] {percent}%"


def strip_ansi(text: str) -> str:
    """Remove ANSI escape codes from text."""
    ansi_pattern = re.compile(r"\x1b\[[0-9;]*m")
    return ansi_pattern.sub("", text)


def register_formatter(tool_name: str):
    """Decorator to register a formatter for a specific tool."""

    def decorator(func: Callable):
        FORMATTERS[tool_name] = func
        return func

    return decorator


def format_live_output(lines: list, tool: str, target: str = "") -> list:
    """
    Format raw log lines into user-friendly output.

    Args:
        lines: Raw log lines from the tool
        tool: Tool name (e.g., 'nikto', 'nmap', 'msf_auxiliary')
        target: Target being scanned (for context)

    Returns:
        List of formatted, user-friendly lines
    """
    # Get the appropriate formatter
    formatter = FORMATTERS.get(tool)

    if formatter:
        return formatter(lines, target)
    else:
        # Fallback: generic formatter that cleans up common patterns
        return format_generic(lines, target)


def format_generic(lines: list, target: str = "") -> list:
    """
    Generic formatter for tools without specific handlers.
    Filters metadata and applies basic cleanup.
    """
    result = []

    # Metadata prefixes to skip
    metadata_prefixes = (
        "=== Plugin:",
        "=== Command Execution",
        "=== End Command",
        "Target:",
        "Args:",
        "Label:",
        "Started:",
        "Command:",
        "Timeout:",
        "Exit Code:",
        "Duration:",
        "=== Output",
        "=== Wrapper",
    )

    for line in lines:
        stripped = line.strip()

        # Skip empty lines
        if not stripped:
            continue

        # Skip metadata
        if stripped.startswith(metadata_prefixes):
            continue

        # Pass through other lines
        result.append(line)

    return result


# =============================================================================
# NIKTO FORMATTER
# =============================================================================
@register_formatter("nikto")
def format_nikto(lines: list, target: str = "") -> list:
    """Format nikto output into friendly summary."""
    result = []
    # Categorize findings by severity
    findings_by_severity = {
        "critical": [],
        "high": [],
        "medium": [],
        "low": [],
        "info": [],
    }
    server_info = None
    scanning_started = False

    for line in lines:
        stripped = line.strip()

        # Skip metadata and empty lines
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # Skip nikto banner/headers
        if stripped.startswith("- Nikto v") or stripped.startswith("---"):
            scanning_started = True
            continue

        # Skip target info lines (we already know the target)
        if stripped.startswith("+ Target IP:") or stripped.startswith(
            "+ Target Hostname:"
        ):
            continue
        if stripped.startswith("+ Target Port:") or stripped.startswith(
            "+ Start Time:"
        ):
            continue

        # Parse nikto findings (lines starting with +)
        if stripped.startswith("+"):
            content = stripped[1:].strip()

            # Server identification
            if content.startswith("Server:"):
                server_info = content.replace("Server:", "").strip()
                continue

            # Categorize findings by severity
            if "CVE" in content:
                # CVEs are typically high/critical
                finding = content[:70] + "..." if len(content) > 70 else content
                findings_by_severity["high"].append(finding)
            elif "OSVDB" in content:
                finding = content[:70] + "..." if len(content) > 70 else content
                findings_by_severity["medium"].append(finding)
            elif any(
                h in content
                for h in [
                    "X-Frame-Options",
                    "X-Content-Type-Options",
                    "X-XSS-Protection",
                    "Content-Security-Policy",
                ]
            ):
                # Missing security headers = medium
                if "X-Frame-Options" in content:
                    findings_by_severity["medium"].append(
                        "Missing X-Frame-Options header"
                    )
                elif "X-Content-Type-Options" in content:
                    findings_by_severity["medium"].append(
                        "Missing X-Content-Type-Options header"
                    )
                elif "X-XSS-Protection" in content:
                    findings_by_severity["medium"].append(
                        "Missing X-XSS-Protection header"
                    )
                elif "Content-Security-Policy" in content:
                    findings_by_severity["medium"].append(
                        "Missing Content-Security-Policy header"
                    )
            elif "Strict-Transport-Security" in content:
                findings_by_severity["medium"].append("Missing HSTS header")
            elif "directory" in content.lower() or "folder" in content.lower():
                match = re.search(r"(/[^\s]+)", content)
                if match:
                    findings_by_severity["info"].append(f"Directory: {match.group(1)}")
            elif "No CGI" in content:
                findings_by_severity["info"].append("No CGI directories found")
            elif "allowed HTTP" in content.lower() or "methods" in content.lower():
                findings_by_severity["low"].append(content[:60])
            else:
                # Other findings = low
                if len(content) > 70:
                    content = content[:67] + "..."
                findings_by_severity["low"].append(content)

    # Build formatted output
    if target:
        result.append(click.style(f"  Scanning {target}", fg="cyan"))
        result.append("")

    if server_info:
        result.append(click.style(f"  Server: {server_info}", fg="bright_black"))

    total_findings = sum(len(f) for f in findings_by_severity.values())
    if total_findings > 0:
        result.append("")

        # Show findings by severity (most severe first)
        for sev in ["critical", "high", "medium", "low"]:
            items = findings_by_severity[sev]
            if items:
                result.append(severity_style(f"  {sev.upper()}: {len(items)}", sev))
                for finding in items[:3]:
                    result.append(severity_style(f"    {finding}", sev))
                if len(items) > 3:
                    result.append(
                        click.style(
                            f"    ... +{len(items) - 3} more", fg="bright_black"
                        )
                    )

        # Info items shown separately
        if findings_by_severity["info"]:
            result.append("")
            for item in findings_by_severity["info"][:3]:
                result.append(click.style(f"    {item}", fg="bright_black"))

    elif scanning_started:
        result.append(click.style("  Scanning in progress...", fg="cyan"))

    return result


# =============================================================================
# NMAP FORMATTER
# =============================================================================
@register_formatter("nmap")
def format_nmap(lines: list, target: str = "") -> list:
    """Format nmap output into friendly summary."""
    result = []
    hosts_found = []
    current_host = None
    open_ports = []
    os_info = None
    scan_type = "port scan"

    for line in lines:
        stripped = line.strip()

        # Skip metadata
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # Detect scan type from nmap output
        if "ping scan" in stripped.lower() or "-sn" in stripped:
            scan_type = "host discovery"
        elif "script" in stripped.lower() or "vuln" in stripped.lower():
            scan_type = "vulnerability scan"

        # Host discovery
        if "Nmap scan report for" in stripped:
            # Save previous host if exists
            if current_host and open_ports:
                hosts_found.append(
                    {"host": current_host, "ports": open_ports.copy(), "os": os_info}
                )
                open_ports = []
                os_info = None

            # Extract host
            match = re.search(r"for\s+(\S+)", stripped)
            if match:
                current_host = match.group(1)

        # Open ports
        port_match = re.match(r"(\d+)/(tcp|udp)\s+open\s+(\S+)", stripped)
        if port_match:
            port, proto, service = port_match.groups()
            open_ports.append(f"{port}/{proto} {service}")

        # OS detection
        if stripped.startswith("OS:") or "Running:" in stripped:
            os_match = re.search(r"(?:OS:|Running:)\s*(.+)", stripped)
            if os_match:
                os_info = os_match.group(1).strip()

        # Host is up
        if "Host is up" in stripped:
            if current_host and current_host not in [
                h.get("host") for h in hosts_found
            ]:
                # For ping scans, just note the host is up
                if not open_ports:
                    hosts_found.append(
                        {"host": current_host, "ports": [], "os": None, "up": True}
                    )

    # Don't forget the last host
    if current_host and (
        open_ports or current_host not in [h.get("host") for h in hosts_found]
    ):
        hosts_found.append({"host": current_host, "ports": open_ports, "os": os_info})

    # Build formatted output
    if target:
        result.append(click.style(f"  {scan_type.title()}: {target}", fg="cyan"))
        result.append("")

    if hosts_found:
        for host_info in hosts_found[:5]:  # Limit to 5 hosts in live view
            host = host_info.get("host", "unknown")
            ports = host_info.get("ports", [])
            os_det = host_info.get("os")

            if ports:
                result.append(click.style(f"  {host}", fg="green", bold=True))
                for port in ports[:8]:  # Limit ports shown
                    result.append(click.style(f"      {port}", fg="white"))
                if len(ports) > 8:
                    result.append(
                        click.style(
                            f"      ... +{len(ports) - 8} more ports", fg="bright_black"
                        )
                    )
                if os_det:
                    result.append(
                        click.style(f"      OS: {os_det[:50]}", fg="bright_black")
                    )
            else:
                result.append(click.style(f"  {host} - up", fg="green"))

            result.append("")

        if len(hosts_found) > 5:
            result.append(
                click.style(
                    f"  ... and {len(hosts_found) - 5} more hosts", fg="bright_black"
                )
            )
    else:
        result.append(click.style("  Scanning in progress...", fg="cyan"))

    return result


# =============================================================================
# MSF AUXILIARY (BRUTE FORCE) FORMATTER
# =============================================================================
@register_formatter("msf_auxiliary")
def format_msf_auxiliary(lines: list, target: str = "") -> list:
    """Format MSF auxiliary module output (brute force, enum, etc.)."""
    result = []
    successes = []
    failures = 0
    info_items = []
    module_name = None
    last_attempt = None

    for line in lines:
        stripped = line.strip()

        # Skip metadata
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # Detect module name
        if "auxiliary(" in stripped.lower() or "using auxiliary" in stripped.lower():
            match = re.search(r"auxiliary[/(]([^)\s]+)", stripped.lower())
            if match:
                module_name = match.group(1)

        # Success! (credential found, share accessible, etc.)
        if stripped.startswith("[+]"):
            content = stripped[3:].strip()
            # Extract credential if present
            cred_match = re.search(r"['\"]([^'\"]+:[^'\"]+)['\"]", content)
            if cred_match:
                successes.append(f"Valid: {cred_match.group(1)}")
            else:
                successes.append(content[:60])

        # Failed attempt
        elif stripped.startswith("[-]") and "Failed" in stripped:
            failures += 1
            # Track last attempt for context
            cred_match = re.search(r"['\"]([^'\"]+)['\"]", stripped)
            if cred_match:
                last_attempt = cred_match.group(1)

        # Info/status
        elif stripped.startswith("[*]"):
            content = stripped[3:].strip()
            # Skip noisy lines
            if "Scanned" in content or "Starting" in content:
                continue
            if len(content) > 60:
                content = content[:57] + "..."
            info_items.append(content)

        # SMB share enumeration results
        elif "READ" in stripped or "WRITE" in stripped:
            info_items.append(stripped[:60])

    # Build formatted output
    scan_desc = module_name or "auxiliary scan"
    if target:
        result.append(click.style(f"  {scan_desc}: {target}", fg="cyan"))
        result.append("")

    # Show successes prominently
    if successes:
        result.append(click.style("  Successes:", fg="green", bold=True))
        for success in successes[:5]:
            result.append(click.style(f"    {success}", fg="green"))
        result.append("")

    # Show failure count (collapsed)
    if failures > 0:
        msg = f"  {failures} failed attempts"
        if last_attempt:
            msg += f" (last: {last_attempt})"
        result.append(click.style(msg, fg="bright_black"))

    # Show info items
    if info_items:
        for item in info_items[:5]:
            result.append(click.style(f"    {item}", fg="white"))

    if not successes and not info_items and failures == 0:
        result.append(click.style("  Running...", fg="cyan"))

    return result


# =============================================================================
# GOBUSTER FORMATTER
# =============================================================================
@register_formatter("gobuster")
def format_gobuster(lines: list, target: str = "") -> list:
    """Format gobuster output into friendly summary."""
    result = []
    found_paths = []
    status_counts = {"2xx": 0, "3xx": 0, "4xx": 0, "5xx": 0}
    progress = None

    for line in lines:
        stripped = line.strip()

        # Skip metadata
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # Found paths (Status: 200, 301, etc.)
        match = re.search(r"(/\S*)\s+\(Status:\s*(\d+)", stripped)
        if match:
            path, status = match.groups()
            status_int = int(status)

            if 200 <= status_int < 300:
                status_counts["2xx"] += 1
                found_paths.append((path, status, "green"))
            elif 300 <= status_int < 400:
                status_counts["3xx"] += 1
                found_paths.append((path, status, "yellow"))
            elif 400 <= status_int < 500:
                status_counts["4xx"] += 1
            else:
                status_counts["5xx"] += 1

        # Progress line
        if "Progress:" in stripped:
            prog_match = re.search(r"Progress:\s*(\d+)\s*/\s*(\d+)", stripped)
            if prog_match:
                current, total = int(prog_match.group(1)), int(prog_match.group(2))
                progress = (current, total)

    # Build formatted output
    if target:
        result.append(click.style(f"  Directory scan: {target}", fg="cyan"))
        result.append("")

    if found_paths:
        result.append(click.style("  Found:", fg="white", bold=True))
        for path, status, color in found_paths[:10]:
            result.append(click.style(f"    [{status}] {path}", fg=color))
        if len(found_paths) > 10:
            result.append(
                click.style(f"    ... +{len(found_paths) - 10} more", fg="bright_black")
            )
        result.append("")

    # Show summary counts
    if any(status_counts.values()):
        summary = []
        if status_counts["2xx"]:
            summary.append(click.style(f"{status_counts['2xx']} OK", fg="green"))
        if status_counts["3xx"]:
            summary.append(
                click.style(f"{status_counts['3xx']} redirects", fg="yellow")
            )
        if summary:
            result.append("  " + " | ".join(summary))

    if progress:
        current, total = progress
        bar = render_progress_bar(current, total, width=25)
        result.append(click.style(f"  {bar}", fg="cyan"))
    elif found_paths:
        # Show running indicator with count
        result.append(
            click.style(f"  Scanning... ({len(found_paths)} found)", fg="cyan")
        )
    else:
        result.append(click.style("  Scanning...", fg="cyan"))

    return result


# =============================================================================
# HYDRA FORMATTER
# =============================================================================
@register_formatter("hydra")
def format_hydra(lines: list, target: str = "") -> list:
    """Format hydra output into friendly summary."""
    result = []
    valid_creds = []
    attempts = 0
    service = None

    for line in lines:
        stripped = line.strip()

        # Skip metadata
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue

        # Detect service
        if "Hydra" in stripped and "starting" in stripped.lower():
            match = re.search(r"starting\s+(\S+)", stripped.lower())
            if match:
                service = match.group(1)

        # Valid credentials found
        if stripped.startswith("[") and "host:" in stripped.lower():
            # Extract login and password
            login_match = re.search(r"login:\s*(\S+)", stripped)
            pass_match = re.search(r"password:\s*(\S*)", stripped)
            if login_match:
                login = login_match.group(1)
                password = pass_match.group(1) if pass_match else ""
                valid_creds.append(f"{login}:{password}")

        # Count attempts from DATA lines
        if stripped.startswith("[DATA]"):
            attempts += 16  # Hydra typically does 16 tasks

    # Build formatted output
    desc = f"{service} brute force" if service else "Brute force"
    if target:
        result.append(click.style(f"  {desc}: {target}", fg="cyan"))
        result.append("")

    if valid_creds:
        result.append(click.style("  Valid credentials:", fg="green", bold=True))
        for cred in valid_creds[:5]:
            result.append(click.style(f"    {cred}", fg="green"))
        result.append("")

    if attempts > 0:
        result.append(click.style(f"  ~{attempts} attempts made", fg="bright_black"))

    if not valid_creds and attempts == 0:
        result.append(click.style("  Starting...", fg="cyan"))

    return result


# =============================================================================
# NUCLEI FORMATTER
# =============================================================================
@register_formatter("nuclei")
def format_nuclei(lines: list, target: str = "") -> list:
    """Format nuclei output into friendly summary."""
    result = []
    findings = {"critical": [], "high": [], "medium": [], "low": [], "info": []}
    templates_loaded = 0
    scan_completed = False
    status_msg = None

    for line in lines:
        # Strip ANSI codes for parsing
        stripped = strip_ansi(line.strip())

        # Skip metadata
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue

        # Parse nuclei findings: [template-id] [severity] [protocol] url
        # Example: [CVE-2021-44228] [critical] [http] http://target/path
        # But skip [INF], [WRN], [ERR] status lines
        if not any(stripped.startswith(f"[{s}]") for s in ["INF", "WRN", "ERR"]):
            match = re.search(r"\[([^\]]+)\]\s*\[(\w+)\]", stripped)
            if match:
                template_id, severity = match.groups()
                severity = severity.lower()
                if severity in findings:
                    findings[severity].append(template_id)

        # Templates loaded: "[INF] Templates loaded for current scan: 5739"
        tmpl_match = re.search(
            r"Templates loaded[^:]*:\s*(\d+)", stripped, re.IGNORECASE
        )
        if tmpl_match:
            templates_loaded = int(tmpl_match.group(1))

        # Scan status
        if "Scan completed" in stripped:
            scan_completed = True
            # Extract match count: "0 matches found"
            match_count = re.search(r"(\d+)\s+match", stripped)
            if match_count:
                status_msg = f"{match_count.group(1)} vulnerabilities found"

        # Executing templates
        if "Executing" in stripped and "templates" in stripped:
            exec_match = re.search(r"Executing\s+(\d+)", stripped)
            if exec_match:
                status_msg = f"Running {exec_match.group(1)} templates..."

    # Build formatted output
    if target:
        result.append(click.style(f"  Vulnerability scan: {target}", fg="cyan"))
        result.append("")

    total = sum(len(v) for v in findings.values())
    if total > 0:
        if findings["critical"]:
            result.append(
                severity_style(f"  CRITICAL: {len(findings['critical'])}", "critical")
            )
            for f in findings["critical"][:3]:
                result.append(severity_style(f"    {f}", "critical"))

        if findings["high"]:
            result.append(severity_style(f"  HIGH: {len(findings['high'])}", "high"))
            for f in findings["high"][:3]:
                result.append(severity_style(f"    {f}", "high"))

        if findings["medium"]:
            result.append(
                severity_style(f"  MEDIUM: {len(findings['medium'])}", "medium")
            )
            for f in findings["medium"][:3]:
                result.append(severity_style(f"    {f}", "medium"))

        if findings["low"]:
            result.append(severity_style(f"  LOW: {len(findings['low'])}", "low"))

        if findings["info"]:
            result.append(severity_style(f"  INFO: {len(findings['info'])}", "info"))

        result.append("")

    # Status line
    if scan_completed and status_msg:
        result.append(click.style(f"  {status_msg}", fg="green"))
    elif status_msg:
        result.append(click.style(f"  {status_msg}", fg="cyan"))
    elif templates_loaded > 0:
        result.append(
            click.style(f"  {templates_loaded} templates loaded", fg="bright_black")
        )
    elif total == 0:
        result.append(click.style("  Scanning templates...", fg="cyan"))

    return result


# =============================================================================
# SMBMAP FORMATTER
# =============================================================================
@register_formatter("smbmap")
def format_smbmap(lines: list, target: str = "") -> list:
    """Format smbmap output into friendly summary."""
    result = []
    shares = []
    current_host = None

    for line in lines:
        stripped = line.strip()

        # Skip metadata
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue

        # Host header
        if "Working on it" in stripped or "IP:" in stripped:
            match = re.search(r"(\d+\.\d+\.\d+\.\d+)", stripped)
            if match:
                current_host = match.group(1)

        # Share listing
        # Format: SHARENAME    READ/WRITE/NO ACCESS    Comment
        share_match = re.match(
            r"^\s*(\S+)\s+(READ|WRITE|NO ACCESS|READ, WRITE)", stripped
        )
        if share_match:
            share_name, access = share_match.groups()
            if share_name not in ("Disk", "Share"):  # Skip header
                shares.append((share_name, access))

    # Build formatted output
    if target:
        result.append(click.style(f"  SMB enumeration: {target}", fg="cyan"))
        result.append("")

    if shares:
        result.append(click.style("  Shares found:", fg="white", bold=True))
        for share, access in shares:
            if "WRITE" in access:
                color = "green"
                icon = ""
            elif "READ" in access:
                color = "yellow"
                icon = ""
            else:
                color = "bright_black"
                icon = ""
            result.append(click.style(f"    {icon} {share} ({access})", fg=color))
    else:
        result.append(click.style("  Enumerating shares...", fg="cyan"))

    return result


# =============================================================================
# NETEXEC / CRACKMAPEXEC FORMATTER
# =============================================================================
@register_formatter("netexec")
def format_netexec(lines: list, target: str = "") -> list:
    """Format netexec/crackmapexec output."""
    result = []
    successes = []
    info_items = []
    protocol = None

    for line in lines:
        stripped = line.strip()

        # Skip metadata
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue

        # Detect protocol from output
        proto_match = re.match(r"^(SMB|LDAP|WINRM|SSH|MSSQL|RDP)", stripped)
        if proto_match:
            protocol = proto_match.group(1)

        # Success markers (Pwn3d!, +, etc.)
        if "Pwn3d!" in stripped or "(Pwn3d!)" in stripped:
            successes.append("Admin access!")
        elif "[+]" in stripped:
            # Extract the key info
            content = stripped.split("[+]")[-1].strip()
            successes.append(content[:50])
        elif "[*]" in stripped:
            content = stripped.split("[*]")[-1].strip()
            if len(content) < 60:
                info_items.append(content)

    # Build formatted output
    desc = f"{protocol} scan" if protocol else "Network scan"
    if target:
        result.append(click.style(f"  {desc}: {target}", fg="cyan"))
        result.append("")

    if successes:
        for s in successes[:5]:
            result.append(click.style(f"  {s}", fg="green"))

    if info_items:
        for item in info_items[:5]:
            result.append(click.style(f"    {item}", fg="white"))

    if not successes and not info_items:
        result.append(click.style("  Scanning...", fg="cyan"))

    return result


# Aliases for tools that share output format
FORMATTERS["crackmapexec"] = format_netexec
FORMATTERS["nxc"] = format_netexec


# =============================================================================
# MSF EXPLOIT FORMATTER
# =============================================================================
@register_formatter("msf_exploit")
def format_msf_exploit(lines: list, target: str = "") -> list:
    """Format MSF exploit output (session creation, shell access)."""
    result = []
    exploit_name = None
    sessions = []
    info_items = []
    errors = []

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # Detect exploit name
        if "Exploit:" in stripped or "exploit/" in stripped.lower():
            match = re.search(r"exploit/(\S+)", stripped.lower())
            if match:
                exploit_name = match.group(1)

        # Session created
        if re.search(
            r"(session \d+ opened|Meterpreter session|Command shell session)", stripped
        ):
            session_match = re.search(r"session (\d+) opened", stripped)
            if session_match:
                sessions.append(f"Session #{session_match.group(1)}")
            else:
                sessions.append("Session opened")

        # Success indicators
        elif stripped.startswith("[+]"):
            content = stripped[3:].strip()
            if content and content not in info_items:
                info_items.append(content[:60])

        # Errors
        elif stripped.startswith("[-]"):
            content = stripped[3:].strip()
            if "exploit" in content.lower() or "failed" in content.lower():
                errors.append(content[:60])

        # Status info
        elif stripped.startswith("[*]"):
            content = stripped[3:].strip()
            if any(
                k in content.lower()
                for k in ["sending", "executing", "trigger", "payload"]
            ):
                info_items.append(content[:60])

    # Build formatted output
    desc = exploit_name or "exploit"
    if target:
        result.append(click.style(f"  {desc}: {target}", fg="cyan"))
        result.append("")

    if sessions:
        for s in sessions:
            result.append(click.style(f"  SHELL ACCESS: {s}", fg="green", bold=True))
        result.append("")

    if info_items:
        for item in info_items[:5]:
            result.append(click.style(f"    {item}", fg="white"))

    if errors and not sessions:
        for err in errors[:3]:
            result.append(click.style(f"    {err}", fg="red"))

    if not sessions and not info_items and not errors:
        result.append(click.style("  Exploiting...", fg="cyan"))

    return result


# =============================================================================
# SQLMAP FORMATTER
# =============================================================================
@register_formatter("sqlmap")
def format_sqlmap(lines: list, target: str = "") -> list:
    """Format sqlmap output (SQL injection testing)."""
    result = []
    injectable_params = []
    databases = []
    tables = []
    dumped_entries = 0
    phase = None

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # Injection found
        if "is vulnerable" in stripped.lower() or "injectable" in stripped.lower():
            param_match = re.search(r"parameter '(\w+)'", stripped)
            if param_match:
                injectable_params.append(param_match.group(1))

        # Database enumeration
        if stripped.startswith("[*]") and not any(
            k in stripped.lower() for k in ["testing", "fetching", "starting"]
        ):
            content = stripped[3:].strip()
            # Database names are typically short single words
            if content and len(content) < 40 and not content.startswith("http"):
                databases.append(content)

        # Phase detection
        if "--dbs" in stripped or "available databases" in stripped.lower():
            phase = "databases"
        elif "--tables" in stripped or "Database:" in stripped:
            phase = "tables"
        elif "--dump" in stripped:
            phase = "dump"

        # Table entries
        if phase == "tables" and stripped.startswith("|"):
            tables.append(stripped.strip("| ").strip())

        # Dumped entries count
        entry_match = re.search(r"(\d+)\s+entries", stripped)
        if entry_match:
            dumped_entries += int(entry_match.group(1))

    # Build formatted output
    if target:
        result.append(click.style(f"  SQL injection: {target}", fg="cyan"))
        result.append("")

    if injectable_params:
        result.append(click.style("  Vulnerable parameters:", fg="green", bold=True))
        for param in injectable_params[:5]:
            result.append(click.style(f"    {param}", fg="green"))
        result.append("")

    if databases:
        result.append(click.style(f"  Databases: {len(databases)}", fg="yellow"))
        for db in databases[:5]:
            result.append(click.style(f"    {db}", fg="white"))
        if len(databases) > 5:
            result.append(
                click.style(f"    ... +{len(databases) - 5} more", fg="bright_black")
            )

    if dumped_entries > 0:
        result.append(
            click.style(f"  Dumped {dumped_entries} entries", fg="green", bold=True)
        )

    if not injectable_params and not databases and dumped_entries == 0:
        desc = f"Testing ({phase})..." if phase else "Testing..."
        result.append(click.style(f"  {desc}", fg="cyan"))

    return result


# =============================================================================
# FFUF FORMATTER
# =============================================================================
@register_formatter("ffuf")
def format_ffuf(lines: list, target: str = "") -> list:
    """Format ffuf output (web fuzzing)."""
    result = []
    found_paths = []
    progress = None

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # FFUF result lines contain Status and Size
        match = re.search(r"(\S+)\s+\[Status:\s*(\d+),\s*Size:\s*(\d+)", stripped)
        if match:
            path, status, size = match.group(1), match.group(2), match.group(3)
            status_int = int(status)
            if 200 <= status_int < 300:
                found_paths.append((path, status, "green"))
            elif 300 <= status_int < 400:
                found_paths.append((path, status, "yellow"))

        # Progress line
        prog_match = re.search(r"(\d+)\s*/\s*(\d+)", stripped)
        if prog_match and "Progress" in stripped:
            progress = (int(prog_match.group(1)), int(prog_match.group(2)))

    # Build formatted output
    if target:
        result.append(click.style(f"  Fuzzing: {target}", fg="cyan"))
        result.append("")

    if found_paths:
        result.append(click.style("  Found:", fg="white", bold=True))
        for path, status, color in found_paths[:10]:
            result.append(click.style(f"    [{status}] {path}", fg=color))
        if len(found_paths) > 10:
            result.append(
                click.style(f"    ... +{len(found_paths) - 10} more", fg="bright_black")
            )
        result.append("")

    if progress:
        current, total = progress
        bar = render_progress_bar(current, total, width=25)
        result.append(click.style(f"  {bar}", fg="cyan"))
    elif not found_paths:
        result.append(click.style("  Fuzzing...", fg="cyan"))

    return result


# =============================================================================
# KATANA FORMATTER
# =============================================================================
@register_formatter("katana")
def format_katana(lines: list, target: str = "") -> list:
    """Format katana output (web crawling)."""
    result = []
    urls_found = []
    params_found = set()

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # URLs discovered (katana outputs one URL per line)
        if stripped.startswith("http://") or stripped.startswith("https://"):
            urls_found.append(stripped)
            # Track parameters
            if "?" in stripped:
                param_match = re.findall(r"[?&](\w+)=", stripped)
                params_found.update(param_match)

    # Build formatted output
    if target:
        result.append(click.style(f"  Web crawl: {target}", fg="cyan"))
        result.append("")

    if urls_found:
        result.append(
            click.style(f"  {len(urls_found)} URLs discovered", fg="white", bold=True)
        )
        # Show URLs with parameters first (more interesting)
        param_urls = [u for u in urls_found if "?" in u]
        plain_urls = [u for u in urls_found if "?" not in u]
        for url in param_urls[:5]:
            result.append(click.style(f"    {url[:70]}", fg="yellow"))
        for url in plain_urls[:3]:
            result.append(click.style(f"    {url[:70]}", fg="white"))
        remaining = len(urls_found) - min(len(param_urls), 5) - min(len(plain_urls), 3)
        if remaining > 0:
            result.append(click.style(f"    ... +{remaining} more", fg="bright_black"))

        if params_found:
            result.append("")
            result.append(
                click.style(
                    f"  Parameters: {', '.join(sorted(params_found)[:8])}", fg="yellow"
                )
            )
    else:
        result.append(click.style("  Crawling...", fg="cyan"))

    return result


# =============================================================================
# WPSCAN FORMATTER
# =============================================================================
@register_formatter("wpscan")
def format_wpscan(lines: list, target: str = "") -> list:
    """Format wpscan output (WordPress scanning)."""
    result = []
    wp_version = None
    vulns = []
    users = []
    plugins = []

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # WordPress version
        ver_match = re.search(r"WordPress version\s+([\d.]+)", stripped)
        if ver_match:
            wp_version = ver_match.group(1)

        # Vulnerabilities
        if stripped.startswith("[!]"):
            content = stripped[3:].strip()
            if "vulnerab" in content.lower() or "CVE" in content:
                vulns.append(content[:60])

        # Users found
        user_match = re.search(r"\|\s+(\w+)\s+\|", stripped)
        if user_match and "Username" not in stripped:
            users.append(user_match.group(1))

        # Plugins
        if "Name:" in stripped and "plugin" in stripped.lower():
            name_match = re.search(r"Name:\s*(.+)", stripped)
            if name_match:
                plugins.append(name_match.group(1).strip())

        # Interesting findings
        if stripped.startswith("[+]"):
            content = stripped[3:].strip()
            if "upload" in content.lower() or "directory" in content.lower():
                vulns.append(content[:60])

    # Build formatted output
    if target:
        result.append(click.style(f"  WordPress scan: {target}", fg="cyan"))
        result.append("")

    if wp_version:
        result.append(click.style(f"  Version: {wp_version}", fg="white"))

    if vulns:
        result.append(severity_style(f"  Vulnerabilities: {len(vulns)}", "high"))
        for v in vulns[:5]:
            result.append(severity_style(f"    {v}", "high"))

    if users:
        result.append(click.style(f"  Users: {', '.join(users[:5])}", fg="yellow"))

    if plugins:
        result.append(
            click.style(f"  Plugins: {', '.join(plugins[:5])}", fg="bright_black")
        )

    if not wp_version and not vulns and not users:
        result.append(click.style("  Scanning...", fg="cyan"))

    return result


# =============================================================================
# HASHCAT FORMATTER
# =============================================================================
@register_formatter("hashcat")
def format_hashcat(lines: list, target: str = "") -> list:
    """Format hashcat output (password cracking)."""
    result = []
    cracked = []
    progress = None
    speed = None
    recovered = None

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # Cracked hashes (hash:password format)
        if ":" in stripped and not stripped.startswith(
            ("Progress", "Speed", "Recovered", "Time", "Hash", "Session", "Status")
        ):
            parts = stripped.split(":")
            if len(parts) >= 2 and len(parts[-1]) < 50:
                cracked.append(stripped[:60])

        # Progress
        prog_match = re.search(r"Progress[.\s]*(\d+)/(\d+)", stripped)
        if prog_match:
            progress = (int(prog_match.group(1)), int(prog_match.group(2)))

        # Speed
        speed_match = re.search(r"Speed[.\s#1]*(\S+\s*\S*/s)", stripped)
        if speed_match:
            speed = speed_match.group(1)

        # Recovered
        rec_match = re.search(r"Recovered[.\s]*(\d+)/(\d+)", stripped)
        if rec_match:
            recovered = (int(rec_match.group(1)), int(rec_match.group(2)))

    # Build formatted output
    if target:
        result.append(click.style(f"  Cracking: {target}", fg="cyan"))
        result.append("")

    if cracked:
        result.append(click.style("  Cracked:", fg="green", bold=True))
        for c in cracked[:5]:
            result.append(click.style(f"    {c}", fg="green"))
        if len(cracked) > 5:
            result.append(
                click.style(f"    ... +{len(cracked) - 5} more", fg="bright_black")
            )
        result.append("")

    if recovered:
        rec_cur, rec_total = recovered
        result.append(click.style(f"  Recovered: {rec_cur}/{rec_total}", fg="yellow"))

    if speed:
        result.append(click.style(f"  Speed: {speed}", fg="bright_black"))

    if progress:
        current, total = progress
        bar = render_progress_bar(current, total, width=25)
        result.append(click.style(f"  {bar}", fg="cyan"))
    elif not cracked and not recovered:
        result.append(click.style("  Cracking...", fg="cyan"))

    return result


# =============================================================================
# JOHN THE RIPPER FORMATTER
# =============================================================================
@register_formatter("john")
def format_john(lines: list, target: str = "") -> list:
    """Format john output (password cracking)."""
    result = []
    cracked = []
    loaded = None
    session_status = None

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # Loaded hashes
        loaded_match = re.search(r"Loaded\s+(\d+)\s+password", stripped)
        if loaded_match:
            loaded = int(loaded_match.group(1))

        # Cracked passwords (username:password format in --show output)
        if ":" in stripped and not stripped.startswith(
            ("Loaded", "Proceeding", "Using", "Press", "Session", "Cost", "Will")
        ):
            parts = stripped.split(":")
            if 2 <= len(parts) <= 4 and len(parts[0]) < 30:
                cracked.append(stripped[:60])

        # Session status
        if "Session completed" in stripped:
            session_status = "completed"
        elif "Session aborted" in stripped:
            session_status = "aborted"

    # Build formatted output
    if target:
        result.append(click.style(f"  Cracking: {target}", fg="cyan"))
        result.append("")

    if cracked:
        result.append(click.style("  Cracked:", fg="green", bold=True))
        for c in cracked[:5]:
            result.append(click.style(f"    {c}", fg="green"))
        if len(cracked) > 5:
            result.append(
                click.style(f"    ... +{len(cracked) - 5} more", fg="bright_black")
            )
        result.append("")

    if loaded:
        result.append(click.style(f"  Loaded: {loaded} hashes", fg="white"))

    if session_status:
        color = "green" if session_status == "completed" else "yellow"
        result.append(click.style(f"  Session {session_status}", fg=color))
    elif not cracked:
        result.append(click.style("  Cracking...", fg="cyan"))

    return result


# =============================================================================
# ENUM4LINUX FORMATTER
# =============================================================================
@register_formatter("enum4linux")
def format_enum4linux(lines: list, target: str = "") -> list:
    """Format enum4linux output (SMB/AD enumeration)."""
    result = []
    users = []
    shares = []
    domain = None
    info_items = []

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # Users
        user_match = re.search(r"user:\[([^\]]+)\]", stripped)
        if user_match:
            users.append(user_match.group(1))

        # Shares
        share_match = re.search(r"^\s*(\S+)\s+Disk", stripped)
        if share_match:
            shares.append(share_match.group(1))

        # Mapping results
        if "Mapping:" in stripped:
            shares.append(stripped.split("Mapping:")[0].strip()[:30])

        # Domain/workgroup
        domain_match = re.search(r"Domain:\s*\[([^\]]+)\]", stripped)
        if domain_match:
            domain = domain_match.group(1)

        # Success findings
        if stripped.startswith("[+]"):
            content = stripped[3:].strip()
            if len(content) < 60 and content not in info_items:
                info_items.append(content)

    # Build formatted output
    if target:
        result.append(click.style(f"  SMB enumeration: {target}", fg="cyan"))
        result.append("")

    if domain:
        result.append(click.style(f"  Domain: {domain}", fg="white"))

    if users:
        result.append(click.style(f"  Users: {len(users)}", fg="yellow", bold=True))
        for u in users[:5]:
            result.append(click.style(f"    {u}", fg="yellow"))
        if len(users) > 5:
            result.append(
                click.style(f"    ... +{len(users) - 5} more", fg="bright_black")
            )

    if shares:
        result.append(click.style(f"  Shares: {len(shares)}", fg="white"))
        for s in shares[:5]:
            result.append(click.style(f"    {s}", fg="white"))

    if info_items and not users and not shares:
        for item in info_items[:5]:
            result.append(click.style(f"    {item}", fg="white"))

    if not users and not shares and not info_items:
        result.append(click.style("  Enumerating...", fg="cyan"))

    return result


# =============================================================================
# LDAPSEARCH FORMATTER
# =============================================================================
@register_formatter("ldapsearch")
def format_ldapsearch(lines: list, target: str = "") -> list:
    """Format ldapsearch output (LDAP enumeration)."""
    result = []
    base_dn = None
    user_count = 0
    domains = []
    creds_found = False

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # Base DN
        if stripped.startswith("dn:") and not base_dn:
            base_dn = stripped[3:].strip()

        # Count users
        if stripped.startswith("sAMAccountName:"):
            user_count += 1

        # Domain detection
        dc_match = re.search(r"DC=(\w+)", stripped)
        if dc_match and dc_match.group(1) not in domains:
            domains.append(dc_match.group(1))

        # Credential in description
        if stripped.startswith("description:") and any(
            k in stripped.lower() for k in ["password", "pass:", "pwd"]
        ):
            creds_found = True

    # Build formatted output
    if target:
        result.append(click.style(f"  LDAP enumeration: {target}", fg="cyan"))
        result.append("")

    if base_dn:
        result.append(click.style(f"  Base DN: {base_dn[:50]}", fg="white"))

    if domains:
        domain_str = ".".join(domains)
        result.append(click.style(f"  Domain: {domain_str}", fg="white"))

    if user_count > 0:
        result.append(
            click.style(f"  Users found: {user_count}", fg="yellow", bold=True)
        )

    if creds_found:
        result.append(
            click.style("  Credential in description!", fg="green", bold=True)
        )

    if not base_dn and user_count == 0:
        result.append(click.style("  Querying...", fg="cyan"))

    return result


# =============================================================================
# IMPACKET-SECRETSDUMP FORMATTER
# =============================================================================
@register_formatter("impacket-secretsdump")
def format_impacket_secretsdump(lines: list, target: str = "") -> list:
    """Format impacket-secretsdump output (credential dumping)."""
    result = []
    ntlm_hashes = 0
    lsa_secrets = 0
    krbtgt_found = False

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # NTLM hash format: user:RID:LM:NT:::
        if re.match(r"\S+:\d+:[a-fA-F0-9]{32}:[a-fA-F0-9]{32}:::", stripped):
            ntlm_hashes += 1
            if stripped.lower().startswith("krbtgt:"):
                krbtgt_found = True

        # LSA secrets
        if "DPAPI_SYSTEM" in stripped or "$MACHINE.ACC" in stripped:
            lsa_secrets += 1
        if stripped.startswith("[*]") and "secret" in stripped.lower():
            lsa_secrets += 1

    # Build formatted output
    if target:
        result.append(click.style(f"  Credential dump: {target}", fg="cyan"))
        result.append("")

    if ntlm_hashes > 0:
        result.append(
            click.style(f"  NTLM hashes: {ntlm_hashes}", fg="green", bold=True)
        )
        if krbtgt_found:
            result.append(
                click.style(
                    "  krbtgt hash found (Golden Ticket!)", fg="green", bold=True
                )
            )

    if lsa_secrets > 0:
        result.append(click.style(f"  LSA secrets: {lsa_secrets}", fg="yellow"))

    if ntlm_hashes == 0 and lsa_secrets == 0:
        result.append(click.style("  Dumping...", fg="cyan"))

    return result


# =============================================================================
# IMPACKET-PSEXEC FORMATTER
# =============================================================================
@register_formatter("impacket-psexec")
def format_impacket_psexec(lines: list, target: str = "") -> list:
    """Format impacket-psexec output (remote shell)."""
    result = []
    shell_access = False
    error_msg = None

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # Shell prompt detection
        if re.search(r"(PS [A-Z]:\\|C:\\Windows|Microsoft Windows)", stripped):
            shell_access = True

        # Success indicators
        if stripped.startswith("[*]") and "Requesting shares" in stripped:
            pass  # Normal operation
        elif stripped.startswith("[*]") and "Opening" in stripped:
            pass  # Normal operation

        # Auth failures
        if "STATUS_LOGON_FAILURE" in stripped or "ACCESS_DENIED" in stripped:
            error_msg = "Authentication failed"

    # Build formatted output
    if target:
        result.append(click.style(f"  PsExec: {target}", fg="cyan"))
        result.append("")

    if shell_access:
        result.append(click.style("  SHELL ACCESS", fg="green", bold=True))
    elif error_msg:
        result.append(click.style(f"  {error_msg}", fg="red"))
    else:
        result.append(click.style("  Connecting...", fg="cyan"))

    return result


# =============================================================================
# IMPACKET-GETUSERSPNS FORMATTER
# =============================================================================
@register_formatter("impacket-getuserspns")
def format_impacket_getuserspns(lines: list, target: str = "") -> list:
    """Format impacket-GetUserSPNs output (Kerberoasting)."""
    result = []
    spn_count = 0
    tgs_hashes = 0

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # SPN entries (table format with ServicePrincipalName)
        if (
            "/" in stripped
            and "@" not in stripped
            and not stripped.startswith(("[", "$"))
        ):
            if re.search(r"\w+/\w+", stripped):
                spn_count += 1

        # TGS hashes
        if "$krb5tgs$" in stripped:
            tgs_hashes += 1

    # Build formatted output
    if target:
        result.append(click.style(f"  Kerberoasting: {target}", fg="cyan"))
        result.append("")

    if tgs_hashes > 0:
        result.append(click.style(f"  TGS hashes: {tgs_hashes}", fg="green", bold=True))

    if spn_count > 0:
        result.append(click.style(f"  SPNs found: {spn_count}", fg="yellow"))

    if tgs_hashes == 0 and spn_count == 0:
        result.append(click.style("  Requesting SPNs...", fg="cyan"))

    return result


# Alias: handler uses mixed case but registry lowercases it
FORMATTERS["impacket-GetUserSPNs"] = format_impacket_getuserspns


# =============================================================================
# IMPACKET-GETNPUSERS FORMATTER
# =============================================================================
@register_formatter("impacket-getnpusers")
def format_impacket_getnpusers(lines: list, target: str = "") -> list:
    """Format impacket-GetNPUsers output (AS-REP roasting)."""
    result = []
    asrep_hashes = 0
    users_checked = 0

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        if "$krb5asrep$" in stripped:
            asrep_hashes += 1

        if "UF_DONT_REQUIRE_PREAUTH" in stripped:
            asrep_hashes += 1

        if stripped.startswith("[*]") and "Getting" in stripped:
            users_checked += 1

    # Build formatted output
    if target:
        result.append(click.style(f"  AS-REP roasting: {target}", fg="cyan"))
        result.append("")

    if asrep_hashes > 0:
        result.append(
            click.style(f"  AS-REP hashes: {asrep_hashes}", fg="green", bold=True)
        )
    else:
        result.append(click.style("  Checking users...", fg="cyan"))

    return result


# =============================================================================
# IMPACKET-MSSQLCLIENT FORMATTER
# =============================================================================
@register_formatter("impacket-mssqlclient")
def format_impacket_mssqlclient(lines: list, target: str = "") -> list:
    """Format impacket-mssqlclient output (MSSQL shell)."""
    result = []
    connected = False
    sql_version = ""
    is_sysadmin = False
    xp_cmdshell = False
    error_msg = None

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # Check for SQL prompt (connection success)
        if "SQL>" in stripped or "SQL (" in stripped:
            connected = True

        # Check for SQL Server version
        if "Microsoft SQL Server" in stripped:
            version_match = re.search(r"Microsoft SQL Server\s+(\d+)", stripped)
            if version_match:
                sql_version = version_match.group(1)

        # Check for sysadmin
        if "sysadmin" in stripped.lower():
            is_sysadmin = True

        # Check for xp_cmdshell
        if "xp_cmdshell" in stripped:
            xp_cmdshell = True

        # Check for errors
        if "Login failed" in stripped:
            error_msg = "Authentication failed"
        elif "Connection refused" in stripped:
            error_msg = "Connection refused"

    # Build formatted output
    if target:
        result.append(click.style(f"  MSSQL: {target}", fg="cyan"))
        result.append("")

    if error_msg:
        result.append(click.style(f"  Error: {error_msg}", fg="red"))
    elif connected:
        result.append(click.style("  Connected!", fg="green", bold=True))
        if sql_version:
            result.append(click.style(f"  SQL Server {sql_version}", fg="cyan"))
        if is_sysadmin:
            result.append(click.style("  SYSADMIN PRIVILEGES!", fg="red", bold=True))
        if xp_cmdshell:
            result.append(click.style("  xp_cmdshell enabled", fg="yellow"))
    else:
        result.append(click.style("  Connecting...", fg="cyan"))

    return result


# =============================================================================
# IMPACKET-TICKETER FORMATTER
# =============================================================================
@register_formatter("impacket-ticketer")
def format_impacket_ticketer(lines: list, target: str = "") -> list:
    """Format impacket-ticketer output (Silver/Golden Ticket forging)."""
    result = []
    ticket_created = False
    ccache_file = ""
    impersonated_user = ""
    target_spn = ""
    error_msg = None

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # Check for ticket creation success
        if "Saving ticket in" in stripped:
            ticket_created = True
            match = re.search(r"Saving ticket in\s+(\S+\.ccache)", stripped)
            if match:
                ccache_file = match.group(1)
                # Extract username from ccache filename
                impersonated_user = ccache_file.replace(".ccache", "")

        # Extract SPN from command
        if "-spn" in stripped:
            match = re.search(r"-spn\s+(\S+)", stripped)
            if match:
                target_spn = match.group(1)

        # Check for errors
        if "Error" in stripped or "error" in stripped.lower():
            error_msg = stripped

    # Build formatted output
    if target:
        result.append(click.style(f"  Ticketer: {target}", fg="cyan"))
        result.append("")

    if error_msg:
        result.append(click.style(f"  Error: {error_msg}", fg="red"))
    elif ticket_created:
        result.append(click.style("  SILVER TICKET CREATED!", fg="green", bold=True))
        if impersonated_user:
            result.append(
                click.style(f"  Impersonating: {impersonated_user}", fg="yellow")
            )
        if target_spn:
            result.append(click.style(f"  SPN: {target_spn}", fg="cyan"))
        if ccache_file:
            result.append(click.style(f"  Ticket: {ccache_file}", fg="green"))
    else:
        result.append(click.style("  Forging ticket...", fg="cyan"))

    return result


# =============================================================================
# EVIL-WINRM FORMATTER
# =============================================================================
@register_formatter("evil_winrm")
def format_evil_winrm(lines: list, target: str = "") -> list:
    """Format evil-winrm output (WinRM shell)."""
    result = []
    shell_access = False
    error_msg = None

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        if "Evil-WinRM shell" in stripped or re.search(r"\*Evil-WinRM\*", stripped):
            shell_access = True
        if re.search(r"PS [A-Z]:\\", stripped):
            shell_access = True

        if (
            "WinRM::WinRMAuthorizationError" in stripped
            or "Access is denied" in stripped
        ):
            error_msg = "Authentication failed"

    # Build formatted output
    if target:
        result.append(click.style(f"  WinRM: {target}", fg="cyan"))
        result.append("")

    if shell_access:
        result.append(click.style("  SHELL ACCESS", fg="green", bold=True))
    elif error_msg:
        result.append(click.style(f"  {error_msg}", fg="red"))
    else:
        result.append(click.style("  Connecting...", fg="cyan"))

    return result


# =============================================================================
# KERBRUTE FORMATTER
# =============================================================================
@register_formatter("kerbrute")
def format_kerbrute(lines: list, target: str = "") -> list:
    """Format kerbrute output (Kerberos user enumeration)."""
    result = []
    valid_users = []
    valid_logins = []

    for line in lines:
        stripped = strip_ansi(line.strip())
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        user_match = re.search(r"VALID USERNAME:\s*(\S+)", stripped)
        if user_match:
            valid_users.append(user_match.group(1))

        login_match = re.search(r"VALID LOGIN:\s*(\S+)", stripped)
        if login_match:
            valid_logins.append(login_match.group(1))

    # Build formatted output
    if target:
        result.append(click.style(f"  Kerberos enumeration: {target}", fg="cyan"))
        result.append("")

    if valid_logins:
        result.append(
            click.style(f"  Valid logins: {len(valid_logins)}", fg="green", bold=True)
        )
        for login in valid_logins[:5]:
            result.append(click.style(f"    {login}", fg="green"))

    if valid_users:
        result.append(
            click.style(f"  Valid users: {len(valid_users)}", fg="yellow", bold=True)
        )
        for user in valid_users[:5]:
            result.append(click.style(f"    {user}", fg="yellow"))
        if len(valid_users) > 5:
            result.append(
                click.style(f"    ... +{len(valid_users) - 5} more", fg="bright_black")
            )

    if not valid_users and not valid_logins:
        result.append(click.style("  Enumerating...", fg="cyan"))

    return result


# =============================================================================
# BLOODHOUND FORMATTER
# =============================================================================
@register_formatter("bloodhound")
def format_bloodhound(lines: list, target: str = "") -> list:
    """Format bloodhound-python output (AD collection)."""
    result = []
    counts = {}
    completed = False

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # Collection counts
        for obj_type in ["user", "group", "computer", "ou", "gpo", "domain", "trust"]:
            match = re.search(rf"(\d+)\s+{obj_type}s?(?:\s|$|,)", stripped.lower())
            if match:
                counts[obj_type] = int(match.group(1))

        if "Done" in stripped or "collection complete" in stripped.lower():
            completed = True

    # Build formatted output
    if target:
        result.append(click.style(f"  AD collection: {target}", fg="cyan"))
        result.append("")

    if counts:
        for obj_type, count in counts.items():
            result.append(click.style(f"    {obj_type}s: {count}", fg="white"))

    if completed:
        result.append(click.style("  Collection complete", fg="green"))
    elif not counts:
        result.append(click.style("  Collecting...", fg="cyan"))

    return result


# =============================================================================
# CERTIPY FORMATTER
# =============================================================================
@register_formatter("certipy")
def format_certipy(lines: list, target: str = "") -> list:
    """Format certipy output (ADCS enumeration)."""
    result = []
    templates = 0
    vulns = []
    ca_name = None

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # CA detection
        ca_match = re.search(r"CA\s+Name\s*:\s*(.+)", stripped)
        if ca_match:
            ca_name = ca_match.group(1).strip()

        # Template count
        tmpl_match = re.search(r"Found\s+(\d+)\s+certificate", stripped)
        if tmpl_match:
            templates = int(tmpl_match.group(1))

        # ESC vulnerabilities
        esc_match = re.search(r"(ESC\d+)", stripped)
        if esc_match and esc_match.group(1) not in vulns:
            vulns.append(esc_match.group(1))

    # Build formatted output
    if target:
        result.append(click.style(f"  ADCS enumeration: {target}", fg="cyan"))
        result.append("")

    if ca_name:
        result.append(click.style(f"  CA: {ca_name}", fg="white"))

    if templates > 0:
        result.append(click.style(f"  Templates: {templates}", fg="white"))

    if vulns:
        result.append(severity_style(f"  Vulnerable: {', '.join(vulns)}", "high"))

    if not ca_name and templates == 0:
        result.append(click.style("  Enumerating ADCS...", fg="cyan"))

    return result


# =============================================================================
# RESPONDER FORMATTER
# =============================================================================
@register_formatter("responder")
def format_responder(lines: list, target: str = "") -> list:
    """Format responder output (LLMNR/NBT-NS poisoning)."""
    result = []
    captures = []
    listening = False

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # Hash captures
        if "NTLMv" in stripped or "Hash" in stripped:
            hash_match = re.search(r"(\w+)::.+", stripped)
            if hash_match:
                captures.append(hash_match.group(0)[:60])

        # Listening
        if "Listening" in stripped or "Poisoners" in stripped:
            listening = True

        # Captured
        if stripped.startswith("[+]"):
            content = stripped[3:].strip()
            if content and len(content) < 60:
                captures.append(content)

    # Build formatted output
    if target:
        result.append(click.style(f"  Responder: {target}", fg="cyan"))
        result.append("")

    if captures:
        result.append(
            click.style(f"  Captures: {len(captures)}", fg="green", bold=True)
        )
        for cap in captures[:5]:
            result.append(click.style(f"    {cap}", fg="green"))
    elif listening:
        result.append(click.style("  Listening for hashes...", fg="cyan"))
    else:
        result.append(click.style("  Starting...", fg="cyan"))

    return result


# =============================================================================
# SMBCLIENT FORMATTER
# =============================================================================
@register_formatter("smbclient")
def format_smbclient(lines: list, target: str = "") -> list:
    """Format smbclient output (share browsing)."""
    result = []
    files = []
    dirs = []

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # Directory/file listing
        entry_match = re.match(r"\s*(.+?)\s{2,}([DAHSR]+)\s+(\d+)", stripped)
        if entry_match:
            name, attrs, size = entry_match.groups()
            name = name.strip()
            if name in (".", ".."):
                continue
            if "D" in attrs:
                dirs.append(name)
            else:
                files.append(f"{name} ({size}B)")

    # Build formatted output
    if target:
        result.append(click.style(f"  SMB browse: {target}", fg="cyan"))
        result.append("")

    if dirs:
        result.append(click.style(f"  Directories: {len(dirs)}", fg="white"))
        for d in dirs[:5]:
            result.append(click.style(f"    {d}/", fg="yellow"))

    if files:
        result.append(click.style(f"  Files: {len(files)}", fg="white"))
        for f in files[:5]:
            result.append(click.style(f"    {f}", fg="white"))
        if len(files) > 5:
            result.append(
                click.style(f"    ... +{len(files) - 5} more", fg="bright_black")
            )

    if not dirs and not files:
        result.append(click.style("  Browsing...", fg="cyan"))

    return result


# =============================================================================
# DNSRECON FORMATTER
# =============================================================================
@register_formatter("dnsrecon")
def format_dnsrecon(lines: list, target: str = "") -> list:
    """Format dnsrecon output (DNS enumeration)."""
    result = []
    records = {"A": 0, "AAAA": 0, "MX": 0, "NS": 0, "TXT": 0, "CNAME": 0, "SRV": 0}
    subdomains = []

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # Record type counting
        for rtype in records:
            if re.search(rf"\b{rtype}\b", stripped):
                records[rtype] += 1

        # Subdomain discovery
        host_match = re.search(r"(\S+\.\S+\.\S+)\s+", stripped)
        if host_match:
            hostname = host_match.group(1)
            if hostname not in subdomains and not hostname.startswith("["):
                subdomains.append(hostname)

    # Build formatted output
    if target:
        result.append(click.style(f"  DNS enumeration: {target}", fg="cyan"))
        result.append("")

    active_records = {k: v for k, v in records.items() if v > 0}
    if active_records:
        summary = ", ".join(f"{k}: {v}" for k, v in active_records.items())
        result.append(click.style(f"  Records: {summary}", fg="white"))

    if subdomains:
        result.append(
            click.style(f"  Subdomains: {len(subdomains)}", fg="yellow", bold=True)
        )
        for sub in subdomains[:5]:
            result.append(click.style(f"    {sub}", fg="yellow"))
        if len(subdomains) > 5:
            result.append(
                click.style(f"    ... +{len(subdomains) - 5} more", fg="bright_black")
            )

    if not active_records and not subdomains:
        result.append(click.style("  Resolving...", fg="cyan"))

    return result


# =============================================================================
# THEHARVESTER FORMATTER
# =============================================================================
@register_formatter("theharvester")
def format_theharvester(lines: list, target: str = "") -> list:
    """Format theHarvester output (OSINT)."""
    result = []
    emails = []
    hosts = []
    ips = []

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # Emails
        if "@" in stripped and re.match(r"[\w.+-]+@[\w.-]+\.\w+", stripped):
            emails.append(stripped[:50])

        # Hosts/subdomains
        if re.match(r"[\w.-]+\.\w{2,}$", stripped) and "@" not in stripped:
            hosts.append(stripped)

        # IPs
        if re.match(r"\d+\.\d+\.\d+\.\d+$", stripped):
            ips.append(stripped)

    # Build formatted output
    if target:
        result.append(click.style(f"  OSINT: {target}", fg="cyan"))
        result.append("")

    if emails:
        result.append(click.style(f"  Emails: {len(emails)}", fg="yellow", bold=True))
        for e in emails[:5]:
            result.append(click.style(f"    {e}", fg="yellow"))

    if hosts:
        result.append(click.style(f"  Hosts: {len(hosts)}", fg="white"))
        for h in hosts[:5]:
            result.append(click.style(f"    {h}", fg="white"))

    if ips:
        result.append(click.style(f"  IPs: {len(ips)}", fg="bright_black"))

    if not emails and not hosts and not ips:
        result.append(click.style("  Gathering OSINT...", fg="cyan"))

    return result


# =============================================================================
# SMBPASSWD FORMATTER
# =============================================================================
@register_formatter("smbpasswd")
def format_smbpasswd(lines: list, target: str = "") -> list:
    """Format smbpasswd output (password change)."""
    result = []
    success = False
    error_msg = None

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        if "Password changed" in stripped or "success" in stripped.lower():
            success = True
        elif "NT_STATUS_" in stripped or "UNSUCCESSFUL" in stripped:
            error_msg = stripped[:60]

    if target:
        result.append(click.style(f"  Password change: {target}", fg="cyan"))
        result.append("")

    if success:
        result.append(click.style("  Password changed!", fg="green", bold=True))
    elif error_msg:
        result.append(click.style(f"  Failed: {error_msg}", fg="red"))
    else:
        result.append(click.style("  Changing password...", fg="cyan"))

    return result


# =============================================================================
# GPP_EXTRACT FORMATTER
# =============================================================================
@register_formatter("gpp_extract")
def format_gpp_extract(lines: list, target: str = "") -> list:
    """Format gpp_extract output (Group Policy Preference password extraction)."""
    result = []
    creds_found = []
    files_checked = 0

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # GPP password found
        if "password" in stripped.lower() and ":" in stripped:
            creds_found.append(stripped[:60])
        elif "userName" in stripped or "cpassword" in stripped:
            creds_found.append(stripped[:60])

        if "Groups.xml" in stripped or "Services.xml" in stripped:
            files_checked += 1

    if target:
        result.append(click.style(f"  GPP extract: {target}", fg="cyan"))
        result.append("")

    if creds_found:
        result.append(click.style("  Credentials found!", fg="green", bold=True))
        for c in creds_found[:5]:
            result.append(click.style(f"    {c}", fg="green"))
    elif files_checked > 0:
        result.append(
            click.style(f"  Checked {files_checked} policy files", fg="white")
        )
    else:
        result.append(click.style("  Extracting...", fg="cyan"))

    return result


# =============================================================================
# LFI_EXTRACT FORMATTER
# =============================================================================
@register_formatter("lfi_extract")
def format_lfi_extract(lines: list, target: str = "") -> list:
    """Format lfi_extract output (Local File Inclusion extraction)."""
    result = []
    files_extracted = []

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        # File paths extracted
        if stripped.startswith("/") or "etc/passwd" in stripped:
            files_extracted.append(stripped[:60])
        elif "extracted" in stripped.lower() or "found" in stripped.lower():
            files_extracted.append(stripped[:60])

    if target:
        result.append(click.style(f"  LFI extraction: {target}", fg="cyan"))
        result.append("")

    if files_extracted:
        result.append(
            click.style(
                f"  Files extracted: {len(files_extracted)}", fg="green", bold=True
            )
        )
        for f in files_extracted[:5]:
            result.append(click.style(f"    {f}", fg="green"))
    else:
        result.append(click.style("  Extracting files...", fg="cyan"))

    return result


# =============================================================================
# WHOIS FORMATTER
# =============================================================================
@register_formatter("whois")
def format_whois(lines: list, target: str = "") -> list:
    """Format whois output (domain information)."""
    result = []
    registrar = None
    created = None
    nameservers = []

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        if stripped.lower().startswith("registrar:"):
            registrar = stripped.split(":", 1)[1].strip()
        elif stripped.lower().startswith("creation date:"):
            created = stripped.split(":", 1)[1].strip()[:20]
        elif stripped.lower().startswith("name server:"):
            ns = stripped.split(":", 1)[1].strip()
            if ns not in nameservers:
                nameservers.append(ns)

    if target:
        result.append(click.style(f"  WHOIS: {target}", fg="cyan"))
        result.append("")

    if registrar:
        result.append(click.style(f"  Registrar: {registrar[:40]}", fg="white"))
    if created:
        result.append(click.style(f"  Created: {created}", fg="white"))
    if nameservers:
        result.append(
            click.style(f"  NS: {', '.join(nameservers[:3])}", fg="bright_black")
        )

    if not registrar and not nameservers:
        result.append(click.style("  Looking up...", fg="cyan"))

    return result


# =============================================================================
# RDP-SEC-CHECK FORMATTER
# =============================================================================
@register_formatter("rdp-sec-check")
def format_rdp_sec_check(lines: list, target: str = "") -> list:
    """Format rdp-sec-check output (RDP security assessment)."""
    result = []
    issues = []
    nla_status = None

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        if "NLA" in stripped or "Network Level Authentication" in stripped:
            if "not" in stripped.lower() or "disabled" in stripped.lower():
                nla_status = "disabled"
                issues.append("NLA not required")
            else:
                nla_status = "enabled"

        if "VULNERABLE" in stripped.upper() or "WARNING" in stripped.upper():
            issues.append(stripped[:60])

        if "encryption" in stripped.lower() and "none" in stripped.lower():
            issues.append("No encryption required")

    if target:
        result.append(click.style(f"  RDP security: {target}", fg="cyan"))
        result.append("")

    if issues:
        result.append(severity_style(f"  Issues: {len(issues)}", "medium"))
        for issue in issues[:5]:
            result.append(severity_style(f"    {issue}", "medium"))
    elif nla_status:
        result.append(click.style(f"  NLA: {nla_status}", fg="white"))
    else:
        result.append(click.style("  Checking...", fg="cyan"))

    return result


# =============================================================================
# WEB LOGIN TEST FORMATTER
# =============================================================================
@register_formatter("web_login_test")
def format_web_login_test(lines: list, target: str = "") -> list:
    """Format web_login_test output."""
    result = []
    login_result = None

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        if "success" in stripped.lower() or "authenticated" in stripped.lower():
            login_result = "success"
        elif "failed" in stripped.lower() or "invalid" in stripped.lower():
            login_result = "failed"

    if target:
        result.append(click.style(f"  Login test: {target}", fg="cyan"))
        result.append("")

    if login_result == "success":
        result.append(click.style("  Login successful!", fg="green", bold=True))
    elif login_result == "failed":
        result.append(click.style("  Login failed", fg="red"))
    else:
        result.append(click.style("  Testing...", fg="cyan"))

    return result


# =============================================================================
# BASH FORMATTER
# =============================================================================
@register_formatter("bash")
def format_bash(lines: list, target: str = "") -> list:
    """Format bash command output (clean pass-through)."""
    return format_generic(lines, target)


# =============================================================================
# SERVICE EXPLORER FORMATTER
# =============================================================================
@register_formatter("service_explorer")
def format_service_explorer(lines: list, target: str = "") -> list:
    """Format service_explorer output."""
    result = []
    services_found = 0

    for line in lines:
        stripped = line.strip()
        if not stripped or stripped.startswith(("===", "Target:", "Args:", "Label:")):
            continue
        if stripped.startswith(("Started:", "Command:", "Timeout:")):
            continue

        if "service" in stripped.lower() or re.match(r"\d+/(tcp|udp)", stripped):
            services_found += 1

    if target:
        result.append(click.style(f"  Service exploration: {target}", fg="cyan"))
        result.append("")

    if services_found > 0:
        result.append(click.style(f"  Services: {services_found}", fg="white"))
    else:
        result.append(click.style("  Exploring...", fg="cyan"))

    return result
