#!/usr/bin/env python3
"""
Impacket GetNPUsers handler.

Handles parsing and display for AS-REP roasting results.
"""

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

import click

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

logger = logging.getLogger(__name__)


class ImpacketGetNPUsersHandler(BaseToolHandler):
    """Handler for Impacket GetNPUsers AS-REP roasting jobs."""

    tool_name = "impacket-getnpusers"
    display_name = "GetNPUsers (AS-REP Roast)"

    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 GetNPUsers AS-REP roasting results."""
        try:
            from souleyez.parsers.impacket_parser import parse_getnpusers

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

                host_manager = HostManager()
            if findings_manager is None:
                from souleyez.storage.findings import FindingsManager

                findings_manager = FindingsManager()
            if credentials_manager is None:
                from souleyez.storage.credentials import CredentialsManager

                credentials_manager = CredentialsManager()

            target = job.get("target", "")

            # Read log file
            if not log_path or not os.path.exists(log_path):
                return {
                    "tool": "impacket-getnpusers",
                    "status": STATUS_ERROR,
                    "error": "Log file not found",
                    "summary": "Log file not found",
                }

            # Use the existing parser
            parsed = parse_getnpusers(log_path, target)
            hashes = parsed.get("hashes", [])
            users_found = parsed.get("users_found", [])

            # Get or create host
            host_id = None
            ip_match = re.search(r"(\d+\.\d+\.\d+\.\d+)", target)
            if ip_match:
                host_ip = ip_match.group(1)
                host = host_manager.get_host_by_ip(engagement_id, host_ip)
                if host:
                    host_id = host["id"]
                else:
                    host_id = host_manager.add_or_update_host(
                        engagement_id, {"ip": host_ip, "status": "up"}
                    )

            # Store hashes as credentials
            hashes_stored = 0
            for hash_info in hashes:
                if host_id:
                    try:
                        username = hash_info.get("username", "unknown")
                        domain = hash_info.get("domain", "")
                        hash_val = hash_info.get("hash", "")

                        credentials_manager.add_credential(
                            engagement_id=engagement_id,
                            host_id=host_id,
                            username=username,
                            password=hash_val,
                            credential_type="kerberos_asrep",
                            source="asrep_roast",
                            tool="impacket-getnpusers",
                            notes=f"AS-REP hash for {username}@{domain} - crack with hashcat -m 18200",
                        )
                        hashes_stored += 1
                    except Exception as e:
                        logger.warning(f"Could not store AS-REP hash: {e}")

            # Create finding if hashes found
            if hashes and host_id:
                usernames = [h.get("username", "?") for h in hashes]
                findings_manager.add_finding(
                    engagement_id=engagement_id,
                    host_id=host_id,
                    finding_type="asrep_roastable_accounts",
                    severity="high",
                    title="AS-REP Roastable Accounts Found",
                    description=(
                        f"Found {len(hashes)} account(s) that do not require Kerberos pre-authentication: "
                        f"{', '.join(usernames)}. AS-REP hashes have been captured and can be cracked "
                        f"offline to recover plaintext passwords."
                    ),
                    evidence=f"Accounts: {', '.join(usernames)}\nHashes extracted: {len(hashes)}",
                    tool="impacket-getnpusers",
                )

            # Determine status
            if hashes:
                status = STATUS_DONE
            elif "error" in str(parsed).lower():
                status = STATUS_ERROR
            else:
                status = STATUS_NO_RESULTS

            # Build summary
            if hashes:
                usernames = [h.get("username", "?") for h in hashes]
                summary = f"Found {len(hashes)} AS-REP roastable hash(es): {', '.join(usernames)}"
            else:
                summary = "No AS-REP roastable accounts found"

            return {
                "tool": "impacket-getnpusers",
                "status": status,
                "target": target,
                "summary": summary,
                "users_found": len(users_found),
                "hashes_found": len(hashes),
                "hashes": hashes,
                "asrep_hashes": hashes,  # For auto-chaining to hashcat
                "hashes_stored": hashes_stored,
            }

        except Exception as e:
            logger.error(f"Error parsing GetNPUsers job: {e}")
            return {
                "tool": "impacket-getnpusers",
                "status": STATUS_ERROR,
                "error": str(e),
                "summary": f"Error: {str(e)}",
            }

    def display_done(
        self,
        job: Dict[str, Any],
        log_path: str,
        show_all: bool = False,
        show_passwords: bool = False,
    ) -> None:
        """Display successful AS-REP roasting results."""
        click.echo()
        click.echo(click.style("=" * 70, fg="green"))
        click.echo(click.style("PARSED RESULTS", fg="green", bold=True))
        click.echo(click.style("=" * 70, fg="green"))
        click.echo()

        try:
            from souleyez.parsers.impacket_parser import parse_getnpusers

            target = job.get("target", "")
            parsed = parse_getnpusers(log_path, target)
            hashes = parsed.get("hashes", [])

            if hashes:
                click.echo(
                    click.style(
                        f"AS-REP Roastable Accounts ({len(hashes)}):", bold=True
                    )
                )
                for h in hashes:
                    username = h.get("username", "?")
                    domain = h.get("domain", "")
                    click.echo(
                        f"  [HIGH] {click.style(username, fg='cyan', bold=True)}@{domain}"
                    )
                    click.echo(f"         Account does not require pre-authentication")
                click.echo()

                click.echo(click.style(f"Hashes Captured ({len(hashes)}):", bold=True))
                for h in hashes:
                    username = h.get("username", "?")
                    hash_type = h.get("hash_type", "AS-REP")
                    click.echo(f"  {hash_type}: {username}")
                click.echo()

                click.echo(click.style("Crack with:", bold=True))
                click.echo("  hashcat -m 18200 hashes.txt /path/to/wordlist")
                click.echo()

                click.echo(click.style("Next Steps:", bold=True))
                click.echo("  - Crack hashes offline with hashcat/john")
                click.echo("  - Use cracked passwords for lateral movement")
                click.echo("  - Check if accounts have admin privileges")
            else:
                click.echo("  No AS-REP hashes captured")

        except Exception as e:
            click.echo(f"  Error reading results: {e}")

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

    def display_warning(
        self,
        job: Dict[str, Any],
        log_path: str,
        log_content: Optional[str] = None,
    ) -> None:
        """Display warning - but check if we actually got results."""
        # First check if we actually got hashes (success case)
        if log_content is None and log_path:
            try:
                with open(log_path, "r", encoding="utf-8", errors="replace") as f:
                    log_content = f.read()
            except Exception:
                log_content = ""

        if log_content and "$krb5asrep$" in log_content:
            # We got hashes! Display as success
            self.display_done(job, log_path)
            return

        click.echo()
        click.echo(click.style("=" * 70, fg="yellow"))
        click.echo(click.style("AS-REP ROASTING - WARNING", fg="yellow", bold=True))
        click.echo(click.style("=" * 70, fg="yellow"))
        click.echo()
        click.echo("  Scan completed with warnings.")
        if log_content:
            if "KDC_ERR" in log_content:
                click.echo("  Kerberos error - credentials may be invalid")
            elif "CCache file is not found" in log_content:
                click.echo("  No cached credentials (this is normal)")
        click.echo()
        click.echo("  Press [r] to view raw logs for details.")
        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 AS-REP roasting error."""
        click.echo()
        click.echo(click.style("=" * 70, fg="red"))
        click.echo(click.style("AS-REP ROASTING FAILED", fg="red", bold=True))
        click.echo(click.style("=" * 70, fg="red"))
        click.echo()

        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 = ""

        if log_content:
            if "KDC_ERR_PREAUTH_FAILED" in log_content:
                click.echo("  Authentication failed - invalid credentials")
            elif "KDC_ERR_C_PRINCIPAL_UNKNOWN" in log_content:
                click.echo("  User not found in domain")
            elif "Connection refused" in log_content:
                click.echo("  Could not connect to domain controller")
            elif (
                "Errno -2" in log_content or "Name or service not known" in log_content
            ):
                click.echo("  Could not resolve domain controller hostname")
            else:
                click.echo("  AS-REP roasting failed - check raw logs for details")
        else:
            click.echo("  Could not read error details")

        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 AS-REP roastable accounts found."""
        click.echo()
        click.echo(click.style("=" * 70, fg="yellow"))
        click.echo(click.style("NO AS-REP ROASTABLE ACCOUNTS", fg="yellow", bold=True))
        click.echo(click.style("=" * 70, fg="yellow"))
        click.echo()
        click.echo("  No accounts with 'Do not require pre-authentication' found.")
        click.echo("  This means no AS-REP roastable accounts exist in this domain.")
        click.echo()
        click.echo(click.style("Tips:", dim=True))
        click.echo("  - Try with a larger user list")
        click.echo("  - Check if users exist with kerbrute first")
        click.echo("  - This finding is common - many domains are properly configured")
        click.echo()
        click.echo(click.style("=" * 70, fg="yellow"))
        click.echo()

    def generate_next_steps(
        self,
        job: Dict[str, Any],
        context: Any,
    ) -> list:
        """
        Generate SMART next steps based on AS-REP roasting results.

        Smart suggestions:
        - Crack AS-REP hashes with hashcat -m 18200
        - Use cracked passwords for authentication
        - Check for password reuse across accounts
        """
        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", "")
        hashes = parse_result.get("hashes", [])
        hashes_found = parse_result.get("hashes_found", 0)

        # Extract domain from target
        domain = "DOMAIN"
        if "/" in target:
            domain = target.split("/")[0]

        # Priority 1: Crack hashes if captured
        if hashes_found > 0:
            usernames = [h.get("username", "?") for h in hashes[:3]]
            user_list = ", ".join(usernames)
            steps.append(
                create_next_step(
                    title=f"Crack {hashes_found} AS-REP hash(es): {user_list}",
                    reason="AS-REP hashes captured - crack for plaintext passwords",
                    commands=[
                        "# Save hashes to file (one per line)",
                        "hashcat -m 18200 asrep.txt /usr/share/wordlists/rockyou.txt",
                        "# With rules for better coverage:",
                        "hashcat -m 18200 asrep.txt /usr/share/wordlists/rockyou.txt -r /usr/share/hashcat/rules/best64.rule",
                    ],
                    priority=1,
                    category="cracking",
                    target=target,
                    tool="hashcat",
                )
            )

        # Priority 2: After cracking - test credentials
        if hashes:
            sample_user = hashes[0].get("username", "user")
            steps.append(
                create_next_step(
                    title="Test cracked credentials",
                    reason="AS-REP roastable accounts may have domain access",
                    commands=[
                        f"# After cracking, test the password:",
                        f"crackmapexec smb DC_IP -u {sample_user} -p 'CRACKED_PASSWORD' -d {domain}",
                        f"# Check for admin access (look for Pwn3d!)",
                        f"crackmapexec smb DC_IP -u {sample_user} -p 'CRACKED_PASSWORD' -d {domain} --shares",
                    ],
                    priority=2,
                    category="post_crack",
                    target=target,
                    tool="crackmapexec",
                )
            )

            # Suggest Kerberoasting with recovered credentials
            steps.append(
                create_next_step(
                    title="Kerberoasting with recovered credentials",
                    reason="Valid credentials enable Kerberoasting",
                    commands=[
                        f"GetUserSPNs.py {domain}/{sample_user}:'CRACKED_PASSWORD' -dc-ip DC_IP -request -outputfile spns.txt",
                        "hashcat -m 13100 spns.txt /usr/share/wordlists/rockyou.txt",
                    ],
                    priority=2,
                    category="kerberos",
                    target=target,
                    tool="impacket_getuserspns",
                )
            )

            # BloodHound collection
            steps.append(
                create_next_step(
                    title="BloodHound collection with credentials",
                    reason="Map AD attack paths with valid credentials",
                    commands=[
                        f"bloodhound-python -c All -u '{sample_user}' -p 'CRACKED_PASSWORD' -d {domain} -dc DC_IP",
                    ],
                    priority=2,
                    category="ad",
                    target=target,
                    tool="bloodhound",
                )
            )

        return steps[:10]


# Register handler
handler = ImpacketGetNPUsersHandler()
