#!/usr/bin/env python3
"""
Handler for NetExec (nxc) - successor to CrackMapExec.
Parses shares, credentials, and authentication results.
"""

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

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 NxcHandler(BaseToolHandler):
    """Handler for NetExec SMB/WinRM/etc enumeration."""

    tool_name = "nxc"
    display_name = "NetExec"

    has_error_handler = True
    has_warning_handler = True
    has_no_results_handler = True
    has_done_handler = True

    # Patterns for parsing
    SHARE_PATTERN = r"^\s*(\S+)\s+(READ|WRITE|READ,WRITE|NO ACCESS)\s*(.*)$"
    VALID_CRED_PATTERN = r"\[\+\]\s+(\S+)\\([^:]+):([^\s]+)"
    PWNED_PATTERN = r"\(Pwn3d!\)"

    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 nxc results."""
        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

            target = job.get("target", "")
            if not log_path or not os.path.exists(log_path):
                return {
                    "tool": "nxc",
                    "status": STATUS_ERROR,
                    "error": "Log file not found",
                }

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

            # Strip ANSI codes
            log_content = re.sub(r"\x1b\[[0-9;]*m", "", log_content)

            shares = []
            readable_shares = []
            writable_shares = []
            credentials = []
            expired_credentials = []  # Credentials that need password change
            is_pwned = False
            domain = ""
            hostname = ""

            # Parse domain/hostname from banner
            # Format: SMB 10.129.234.72 445 DC [*] Windows Server 2022 (name:DC) (domain:baby2.vl)
            banner_match = re.search(
                r"\(name:([^)]+)\).*\(domain:([^)]+)\)", log_content
            )
            if banner_match:
                hostname = banner_match.group(1)
                domain = banner_match.group(2)

            # Check for Pwn3d (SMB admin access)
            if re.search(self.PWNED_PATTERN, log_content):
                is_pwned = True

            # Check for SSH Shell access (Linux)
            # Format: [+] leia_organa:help_me_obiwan  Linux - Shell access!
            has_shell_access = False
            if "Shell access!" in log_content:
                has_shell_access = True
                # Parse SSH credentials
                ssh_cred_pattern = r"\[\+\]\s+([^:\s]+):(\S+)\s+.*Shell access!"
                for match in re.finditer(ssh_cred_pattern, log_content):
                    username = match.group(1)
                    password = match.group(2)
                    cred = {
                        "username": username,
                        "password": password,
                        "domain": "",
                        "service": "ssh",
                        "status": "valid",
                    }
                    credentials.append(cred)

                    # Store in database
                    if credentials_manager and host_manager:
                        try:
                            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=username,
                                    password=password,
                                    service="ssh",
                                    port=22,
                                    credential_type="password",
                                    tool="nxc",
                                    status="valid",
                                )
                                logger.warning(
                                    f"SSH SHELL ACCESS: {username}:{password} on {target}"
                                )
                        except Exception as e:
                            logger.debug(f"Could not store SSH credential: {e}")

            # Parse valid credentials
            # Format: [+] baby2.vl\Carl.Moore:Carl.Moore
            for match in re.finditer(self.VALID_CRED_PATTERN, log_content):
                domain_part = match.group(1)
                username = match.group(2)
                password = match.group(3)

                cred = {
                    "username": username,
                    "password": password,
                    "domain": domain_part,
                    "service": "smb",
                    "status": "valid",
                }
                credentials.append(cred)

                # Store in database
                if credentials_manager and host_manager:
                    try:
                        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=username,
                                password=password,
                                service="smb",
                                credential_type="password",
                                tool="nxc",
                                status="valid",
                            )
                            logger.warning(
                                f"CREDENTIAL FOUND: {domain_part}\\{username}:{password}"
                            )
                    except Exception as e:
                        logger.debug(f"Could not store credential: {e}")

            # Parse expired credentials (STATUS_PASSWORD_MUST_CHANGE)
            # Format: [-] baby.vl\Caroline.Robinson:BabyStart123! STATUS_PASSWORD_MUST_CHANGE
            expired_pattern = (
                r"\[-\]\s+(\S+)\\([^:]+):(\S+)\s+STATUS_PASSWORD_MUST_CHANGE"
            )
            for match in re.finditer(expired_pattern, log_content):
                domain_part = match.group(1)
                username = match.group(2)
                password = match.group(3)

                cred = {
                    "username": username,
                    "password": password,
                    "domain": domain_part,
                    "service": "smb",
                    "status": "expired",
                }
                expired_credentials.append(cred)

                # Store in database with expired status
                if credentials_manager and host_manager:
                    try:
                        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=username,
                                password=password,
                                service="smb",
                                credential_type="password",
                                tool="nxc",
                                status="expired",
                            )
                            logger.warning(
                                f"EXPIRED CREDENTIAL FOUND: {domain_part}\\{username}:{password} - PASSWORD MUST CHANGE"
                            )
                    except Exception as e:
                        logger.debug(f"Could not store expired credential: {e}")

            # Parse shares
            # nxc format: SMB  IP  PORT  HOST  ShareName  Permissions  Remark
            # Example: SMB  10.129.234.72  445  DC  homes  READ,WRITE
            share_section = False
            for line in log_content.split("\n"):
                if "Enumerated shares" in line:
                    share_section = True
                    continue
                if share_section and line.strip():
                    # Skip header and separator lines
                    if "Share" in line and "Permissions" in line and "Remark" in line:
                        continue
                    if "-----" in line:
                        continue
                    # Stop at completion marker
                    if "=== Completed" in line or "Exit Code" in line:
                        break

                    # nxc output has: SMB  IP  PORT  HOST  ShareName  Permissions  Comment
                    # Parse by splitting on whitespace and extracting after the hostname
                    parts = line.split()
                    if len(parts) >= 5 and parts[0] == "SMB":
                        # Find the share name - it's after the hostname (4th column)
                        # Format: SMB IP PORT HOSTNAME SHARENAME [PERMISSIONS] [COMMENT...]
                        # Index:  0   1  2    3        4         5             6+
                        share_name = parts[4] if len(parts) > 4 else ""

                        # Skip metadata lines
                        if not share_name or share_name in [
                            "[*]",
                            "[+]",
                            "[-]",
                            "Share",
                        ]:
                            continue

                        # Permissions (if present) - look for READ, WRITE, READ,WRITE
                        perms = ""
                        comment = ""
                        if len(parts) > 5:
                            # Check if 5th element is a permission
                            if parts[5] in ["READ", "WRITE", "READ,WRITE"]:
                                perms = parts[5]
                                comment = " ".join(parts[6:]) if len(parts) > 6 else ""
                            else:
                                # No permissions, rest is comment
                                comment = " ".join(parts[5:])

                        share = {
                            "name": share_name,
                            "permissions": perms,
                            "comment": comment,
                        }
                        shares.append(share)

                        if "READ" in perms:
                            readable_shares.append(share)
                        if "WRITE" in perms:
                            writable_shares.append(share)

            # Check for tool errors (supplement to handler's own error checks)
            tool_error = detect_tool_error(log_content, "nxc")

            # Determine status based on results found
            # Findings ALWAYS take priority over error detection
            # Retry logic is handled by background.py before parsing
            if credentials:
                status = STATUS_DONE
            elif has_shell_access:
                status = STATUS_DONE  # SSH shell access without parsed creds
            elif expired_credentials:
                status = STATUS_WARNING  # Expired creds need attention
            elif shares:
                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

            # Build summary
            summary_parts = []
            if credentials:
                summary_parts.append(f"{len(credentials)} valid credential(s)")
            if expired_credentials:
                summary_parts.append(
                    f"{len(expired_credentials)} expired credential(s)"
                )
            if is_pwned:
                summary_parts.append("PWNED!")
            if has_shell_access:
                summary_parts.append("SHELL ACCESS!")
            if shares:
                summary_parts.append(
                    f"{len(shares)} shares ({len(readable_shares)} readable, {len(writable_shares)} writable)"
                )
            summary = " | ".join(summary_parts) if summary_parts else "No findings"

            result = {
                "tool": "nxc",
                "status": status,
                "target": target,
                "domain": domain,
                "hostname": hostname,
                "shares": shares,
                "readable_shares": readable_shares,
                "writable_shares": writable_shares,
                "credentials": credentials,
                "expired_credentials": expired_credentials,
                "is_pwned": is_pwned,
                "has_shell_access": has_shell_access,
                "summary": summary,
            }

            if credentials:
                logger.info(f"nxc: Found {len(credentials)} valid credential(s)")
            if expired_credentials:
                logger.warning(
                    f"nxc: Found {len(expired_credentials)} EXPIRED credential(s) - password change required!"
                )
            if shares:
                logger.info(
                    f"nxc: Found {len(shares)} share(s) ({len(readable_shares)} readable, {len(writable_shares)} writable)"
                )

            # 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="nxc",
                            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}")

            return result

        except Exception as e:
            logger.error(f"Error parsing nxc 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 nxc results."""
        import click

        parse_result = job.get("parse_result", {})

        domain = parse_result.get("domain", "")
        hostname = parse_result.get("hostname", "")
        shares = parse_result.get("shares", [])
        credentials = parse_result.get("credentials", [])
        expired_credentials = parse_result.get("expired_credentials", [])
        is_pwned = parse_result.get("is_pwned", False)
        target = parse_result.get("target", "")

        if domain or hostname:
            click.echo(
                f"  Target: {hostname}.{domain}" if hostname else f"  Domain: {domain}"
            )

        if is_pwned:
            click.secho("  [!] Pwn3d! - Admin access achieved", fg="red", bold=True)

        if credentials:
            click.secho(
                f"\n  Valid Credentials ({len(credentials)}):", fg="green", bold=True
            )
            for cred in credentials:
                domain_part = cred.get("domain", "")
                username = cred.get("username", "")
                password = cred.get("password", "") if show_passwords else "********"
                click.echo(f"    {domain_part}\\{username}:{password}")

        if expired_credentials:
            click.secho(
                f"\n  Expired Credentials ({len(expired_credentials)}):",
                fg="yellow",
                bold=True,
            )
            click.secho("  [!] Password must be changed before use!", fg="yellow")
            for cred in expired_credentials:
                domain_part = cred.get("domain", "")
                username = cred.get("username", "")
                password = cred.get("password", "") if show_passwords else "********"
                click.echo(f"    {domain_part}\\{username}:{password}")
            click.secho(f"\n  To change password, run:", fg="cyan")
            for cred in expired_credentials:
                domain_part = cred.get("domain", "")
                username = cred.get("username", "")
                click.echo(
                    f"    smbpasswd -U {domain_part}/{username} -r {target or domain}"
                )

        if shares:
            click.secho(f"\n  Shares ({len(shares)}):", fg="cyan", bold=True)
            for share in shares:
                name = share.get("name", "")
                perms = share.get("permissions", "")
                comment = share.get("comment", "")

                # Color based on permissions
                if "WRITE" in perms:
                    click.secho(f"    {name:20} {perms:15} {comment}", fg="yellow")
                elif "READ" in perms:
                    click.secho(f"    {name:20} {perms:15} {comment}", fg="green")
                else:
                    click.echo(f"    {name:20} {perms:15} {comment}")

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

        Smart suggestions include:
        - DCSync and Golden Ticket when admin on DC
        - Relay attacks when SMB signing disabled
        - 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", "")
        credentials = parse_result.get("credentials", [])
        expired_credentials = parse_result.get("expired_credentials", [])
        writable_shares = parse_result.get("writable_shares", [])
        readable_shares = parse_result.get("readable_shares", [])
        is_pwned = parse_result.get("is_pwned", False)
        has_shell_access = parse_result.get("has_shell_access", False)
        domain = parse_result.get("domain", "")
        is_dc = parse_result.get("is_dc", False)

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

        # If Pwn3d! - suggest post-exploitation
        if is_pwned and credentials:
            cred = credentials[0]
            username = cred.get("username", "")
            password = cred.get("password", "")
            cred_domain = cred.get("domain", domain)

            # 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 '{cred_domain}/{username}:{password}'@{target} -just-dc",
                            "# Or just krbtgt hash for Golden Ticket:",
                            f"secretsdump.py '{cred_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 {cred_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 '{cred_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 interactive shell via PSExec",
                    reason="Admin access - get SYSTEM shell",
                    commands=[
                        f"psexec.py '{cred_domain}/{username}:{password}'@{target}",
                    ],
                    priority=1,
                    category="post_exploit",
                    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 SSH shell access - suggest post-exploitation
        elif has_shell_access and credentials:
            cred = credentials[0]
            username = cred.get("username", "")
            password = cred.get("password", "")

            steps.append(
                create_next_step(
                    title="SSH into target",
                    reason="SSH shell access confirmed",
                    commands=[
                        f"sshpass -p '{password}' ssh {username}@{target}",
                    ],
                    priority=1,
                    category="access",
                    target=target,
                    tool="ssh",
                    requires_creds=True,
                    cred_type="ssh",
                )
            )

        # If valid credentials (no admin) - suggest enumeration and lateral movement
        elif credentials:
            cred = credentials[0]
            username = cred.get("username", "")
            password = cred.get("password", "")
            cred_domain = cred.get("domain", domain)

            steps.append(
                create_next_step(
                    title="Enumerate shares with credentials",
                    reason="Valid credentials - enumerate accessible shares",
                    commands=[
                        f"smbmap -H {target} -u '{username}' -p '{password}' -d '{cred_domain}'",
                    ],
                    priority=2,
                    category="smb",
                    target=target,
                    tool="smbmap",
                    requires_creds=True,
                    cred_type="smb",
                )
            )

            steps.append(
                create_next_step(
                    title="BloodHound collection",
                    reason="Valid domain credentials - collect AD attack paths",
                    commands=[
                        f"bloodhound-python -c All -u '{username}' -p '{password}' -d {cred_domain} -dc {target}",
                    ],
                    priority=2,
                    category="ad",
                    target=target,
                    tool="bloodhound",
                    requires_creds=True,
                    cred_type="ldap",
                )
            )

        # If writable shares found - suggest file operations
        if writable_shares:
            share_name = writable_shares[0].get("name", "share")
            steps.append(
                create_next_step(
                    title=f"Access writable share: {share_name}",
                    reason=f"Writable share found - check for sensitive files or upload",
                    commands=[
                        f"smbclient //{target}/{share_name} -U 'user%pass'",
                    ],
                    priority=2,
                    category="smb",
                    target=target,
                    tool="smbmap",  # Match chain rule tool for filtering
                )
            )

        # If expired credentials - suggest password change
        if expired_credentials:
            cred = expired_credentials[0]
            username = cred.get("username", "")
            cred_domain = cred.get("domain", domain)

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

        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 = NxcHandler()
