"""
Hash Tools - Hash identification and cracking for personal recovery

Provides tools for:
- Hash type identification (MD5, SHA, bcrypt, etc.)
- Dictionary/wordlist attacks (for personal password recovery)
- Hash generation and verification

For recovering YOUR OWN forgotten passwords only.
"""

import hashlib
import re
import subprocess
import tempfile
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Optional

from groknroll.mode2600.core import get_mode2600


class HashType(Enum):
    """Supported hash types."""

    # Common hashes
    MD5 = "md5"
    SHA1 = "sha1"
    SHA256 = "sha256"
    SHA512 = "sha512"
    SHA3_256 = "sha3-256"
    SHA3_512 = "sha3-512"

    # Password hashes
    BCRYPT = "bcrypt"
    SCRYPT = "scrypt"
    ARGON2 = "argon2"
    PBKDF2 = "pbkdf2"

    # Unix/Linux
    MD5CRYPT = "md5crypt"
    SHA256CRYPT = "sha256crypt"
    SHA512CRYPT = "sha512crypt"
    DESCRYPT = "descrypt"

    # Windows
    NTLM = "ntlm"
    LM = "lm"
    MSCASH = "mscash"
    MSCASH2 = "mscash2"

    # Database
    MYSQL323 = "mysql323"
    MYSQL41 = "mysql41"
    POSTGRES_MD5 = "postgres-md5"
    ORACLE_11G = "oracle-11g"
    MSSQL = "mssql"

    # Web
    WORDPRESS = "wordpress"
    DRUPAL = "drupal"
    JOOMLA = "joomla"
    PHPASS = "phpass"

    # Crypto
    BITCOIN = "bitcoin"
    ETHEREUM = "ethereum"
    LITECOIN = "litecoin"

    # Other
    NTLM_V2 = "ntlm-v2"
    KERBEROS = "kerberos"
    JWT = "jwt"
    UNKNOWN = "unknown"


@dataclass
class HashIdentificationResult:
    """Result of hash identification."""

    hash_value: str
    possible_types: list[HashType]
    confidence: float
    length: int
    charset: str
    recommendations: list[str] = field(default_factory=list)


@dataclass
class CrackResult:
    """Result of hash cracking attempt."""

    hash_value: str
    hash_type: HashType
    cracked: bool
    plaintext: Optional[str] = None
    method: str = ""
    attempts: int = 0
    duration_seconds: float = 0.0
    wordlist_used: Optional[str] = None


class HashIdentifier:
    """
    Identify hash types from hash strings.

    Supports 40+ hash types including:
    - Common: MD5, SHA1, SHA256, SHA512
    - Password: bcrypt, scrypt, argon2, PBKDF2
    - Unix: md5crypt, sha256crypt, sha512crypt
    - Windows: NTLM, LM, MSCASH
    - Database: MySQL, PostgreSQL, Oracle, MSSQL
    - Web: WordPress, Drupal, Joomla
    """

    # Hash patterns (regex -> (HashType, confidence))
    PATTERNS: list[tuple[str, HashType, float]] = [
        # Modular crypt format
        (r"^\$2[aby]?\$\d{2}\$[./A-Za-z0-9]{53}$", HashType.BCRYPT, 1.0),
        (r"^\$argon2(i|d|id)\$.*$", HashType.ARGON2, 1.0),
        (r"^\$scrypt\$.*$", HashType.SCRYPT, 1.0),
        (r"^\$pbkdf2.*\$.*$", HashType.PBKDF2, 1.0),
        (r"^\$1\$[./A-Za-z0-9]{8}\$[./A-Za-z0-9]{22}$", HashType.MD5CRYPT, 1.0),
        (r"^\$5\$[./A-Za-z0-9]{0,16}\$[./A-Za-z0-9]{43}$", HashType.SHA256CRYPT, 1.0),
        (r"^\$6\$[./A-Za-z0-9]{0,16}\$[./A-Za-z0-9]{86}$", HashType.SHA512CRYPT, 1.0),
        # WordPress/PHPass
        (r"^\$P\$[A-Za-z0-9./]{31}$", HashType.PHPASS, 1.0),
        (r"^\$H\$[A-Za-z0-9./]{31}$", HashType.PHPASS, 1.0),
        # Drupal
        (r"^\$S\$[A-Za-z0-9./]{52}$", HashType.DRUPAL, 1.0),
        # MySQL
        (r"^[a-f0-9]{16}$", HashType.MYSQL323, 0.5),
        (r"^\*[A-F0-9]{40}$", HashType.MYSQL41, 1.0),
        # PostgreSQL
        (r"^md5[a-f0-9]{32}$", HashType.POSTGRES_MD5, 1.0),
        # JWT
        (r"^eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$", HashType.JWT, 1.0),
        # Length-based detection
        (r"^[a-f0-9]{32}$", HashType.MD5, 0.8),
        (r"^[a-f0-9]{40}$", HashType.SHA1, 0.8),
        (r"^[a-f0-9]{64}$", HashType.SHA256, 0.8),
        (r"^[a-f0-9]{128}$", HashType.SHA512, 0.8),
        # NTLM (uppercase hex, 32 chars)
        (r"^[A-F0-9]{32}$", HashType.NTLM, 0.7),
        # LM hash
        (r"^[A-F0-9]{32}$", HashType.LM, 0.3),
    ]

    def identify(self, hash_value: str) -> HashIdentificationResult:
        """
        Identify the type of a hash.

        Args:
            hash_value: The hash string to identify

        Returns:
            HashIdentificationResult with possible types and confidence
        """
        hash_value = hash_value.strip()
        possible_types: list[tuple[HashType, float]] = []

        # Check against patterns
        for pattern, hash_type, confidence in self.PATTERNS:
            if re.match(pattern, hash_value, re.IGNORECASE):
                possible_types.append((hash_type, confidence))

        # Sort by confidence
        possible_types.sort(key=lambda x: x[1], reverse=True)

        # Determine charset
        charset = self._analyze_charset(hash_value)

        # Build recommendations
        recommendations = self._build_recommendations(
            [t for t, _ in possible_types],
            hash_value,
        )

        return HashIdentificationResult(
            hash_value=hash_value,
            possible_types=[t for t, _ in possible_types] or [HashType.UNKNOWN],
            confidence=possible_types[0][1] if possible_types else 0.0,
            length=len(hash_value),
            charset=charset,
            recommendations=recommendations,
        )

    def _analyze_charset(self, hash_value: str) -> str:
        """Analyze the character set of a hash."""
        has_lower = any(c.islower() for c in hash_value)
        has_upper = any(c.isupper() for c in hash_value)
        has_digit = any(c.isdigit() for c in hash_value)
        has_special = any(c in "./+=$" for c in hash_value)

        parts = []
        if has_lower:
            parts.append("lowercase")
        if has_upper:
            parts.append("uppercase")
        if has_digit:
            parts.append("digits")
        if has_special:
            parts.append("special")

        return "+".join(parts) or "empty"

    def _build_recommendations(
        self,
        types: list[HashType],
        hash_value: str,
    ) -> list[str]:
        """Build cracking recommendations based on hash type."""
        recommendations = []

        if not types or types[0] == HashType.UNKNOWN:
            recommendations.append("Hash type unknown - try hash-identifier or hashid")
            return recommendations

        primary = types[0]

        # Tool recommendations
        if primary in [HashType.BCRYPT, HashType.ARGON2, HashType.SCRYPT]:
            recommendations.append(f"Slow hash ({primary.value}) - use hashcat with GPU")
            recommendations.append("Consider targeted wordlist rather than brute force")

        elif primary in [HashType.MD5, HashType.SHA1, HashType.SHA256]:
            recommendations.append(f"Fast hash ({primary.value}) - hashcat or john")
            recommendations.append("Try rockyou.txt first, then rule-based attacks")

        elif primary in [HashType.NTLM]:
            recommendations.append("NTLM hash - very fast to crack")
            recommendations.append("hashcat -m 1000 or john --format=NT")

        elif primary in [HashType.SHA512CRYPT, HashType.SHA256CRYPT]:
            recommendations.append("Unix crypt hash - moderately slow")
            recommendations.append("hashcat with appropriate mode")

        elif primary in [HashType.WORDPRESS, HashType.PHPASS]:
            recommendations.append("WordPress/PHPass hash")
            recommendations.append("hashcat -m 400 or john --format=phpass")

        # Wordlist recommendations
        if primary not in [HashType.BCRYPT, HashType.ARGON2, HashType.SCRYPT]:
            recommendations.append("Common wordlists: rockyou.txt, passwords.txt")
            recommendations.append("For personal recovery: try known password patterns")

        return recommendations


class HashCracker:
    """
    Hash cracking for personal password recovery.

    Supports multiple backends:
    - hashcat (GPU-accelerated)
    - john (CPU-based)
    - Python fallback (for simple hashes)

    IMPORTANT: Only use for recovering YOUR OWN passwords.
    """

    HASHCAT_MODES = {
        HashType.MD5: 0,
        HashType.SHA1: 100,
        HashType.SHA256: 1400,
        HashType.SHA512: 1700,
        HashType.NTLM: 1000,
        HashType.MD5CRYPT: 500,
        HashType.SHA256CRYPT: 7400,
        HashType.SHA512CRYPT: 1800,
        HashType.BCRYPT: 3200,
        HashType.PHPASS: 400,
        HashType.WORDPRESS: 400,
        HashType.MYSQL41: 300,
    }

    JOHN_FORMATS = {
        HashType.MD5: "Raw-MD5",
        HashType.SHA1: "Raw-SHA1",
        HashType.SHA256: "Raw-SHA256",
        HashType.SHA512: "Raw-SHA512",
        HashType.NTLM: "NT",
        HashType.MD5CRYPT: "md5crypt",
        HashType.SHA256CRYPT: "sha256crypt",
        HashType.SHA512CRYPT: "sha512crypt",
        HashType.BCRYPT: "bcrypt",
        HashType.PHPASS: "phpass",
    }

    def __init__(self):
        """Initialize hash cracker."""
        self.mode2600 = get_mode2600()

    def crack(
        self,
        hash_value: str,
        hash_type: Optional[HashType] = None,
        wordlist: Optional[str] = None,
        backend: str = "auto",
        timeout: int = 300,
        rules: Optional[str] = None,
    ) -> CrackResult:
        """
        Attempt to crack a hash.

        Args:
            hash_value: The hash to crack
            hash_type: Type of hash (auto-detected if not specified)
            wordlist: Path to wordlist file
            backend: "hashcat", "john", "python", or "auto"
            timeout: Maximum time in seconds
            rules: Rules file for mangling (hashcat/john)

        Returns:
            CrackResult with plaintext if successful
        """
        # Require authorization context
        context = self.mode2600.require_context()

        start_time = datetime.now()

        # Auto-detect hash type
        if not hash_type:
            identifier = HashIdentifier()
            result = identifier.identify(hash_value)
            hash_type = result.possible_types[0] if result.possible_types else HashType.UNKNOWN

        # Select backend
        if backend == "auto":
            backend = self._select_backend(hash_type)

        # Default wordlist
        if not wordlist:
            wordlist = self._find_default_wordlist()

        # Log operation
        self.mode2600.log_operation(
            "crack_hash",
            "hash_cracker",
            {
                "hash_type": hash_type.value,
                "backend": backend,
                "wordlist": wordlist,
            },
            f"Attempting to crack {hash_type.value} hash",
            True,
        )

        # Attempt crack based on backend
        if backend == "hashcat":
            result = self._crack_hashcat(hash_value, hash_type, wordlist, timeout, rules)
        elif backend == "john":
            result = self._crack_john(hash_value, hash_type, wordlist, timeout)
        else:
            result = self._crack_python(hash_value, hash_type, wordlist, timeout)

        # Calculate duration
        result.duration_seconds = (datetime.now() - start_time).total_seconds()

        # Log result
        self.mode2600.log_operation(
            "crack_result",
            "hash_cracker",
            {"cracked": result.cracked},
            f"Crack {'successful' if result.cracked else 'failed'}",
            result.cracked,
        )

        return result

    def _select_backend(self, hash_type: HashType) -> str:
        """Select best backend for hash type."""
        import shutil

        # Prefer hashcat for GPU acceleration
        if shutil.which("hashcat") and hash_type in self.HASHCAT_MODES:
            return "hashcat"

        # Fall back to john
        if shutil.which("john") and hash_type in self.JOHN_FORMATS:
            return "john"

        # Python fallback for simple hashes
        return "python"

    def _find_default_wordlist(self) -> Optional[str]:
        """Find a default wordlist on the system."""
        common_paths = [
            "/usr/share/wordlists/rockyou.txt",
            "/usr/share/wordlists/rockyou.txt.gz",
            "/usr/share/john/password.lst",
            "/usr/share/seclists/Passwords/Common-Credentials/10k-most-common.txt",
            Path.home() / "wordlists" / "rockyou.txt",
        ]

        for path in common_paths:
            if Path(path).exists():
                return str(path)

        return None

    def _crack_hashcat(
        self,
        hash_value: str,
        hash_type: HashType,
        wordlist: Optional[str],
        timeout: int,
        rules: Optional[str],
    ) -> CrackResult:
        """Crack hash using hashcat."""
        if hash_type not in self.HASHCAT_MODES:
            return CrackResult(
                hash_value=hash_value,
                hash_type=hash_type,
                cracked=False,
                method="hashcat",
            )

        mode = self.HASHCAT_MODES[hash_type]

        with tempfile.NamedTemporaryFile(mode="w", suffix=".hash", delete=False) as f:
            f.write(hash_value)
            hash_file = f.name

        with tempfile.NamedTemporaryFile(mode="w", suffix=".pot", delete=False) as f:
            pot_file = f.name

        try:
            cmd = [
                "hashcat",
                "-m", str(mode),
                "-a", "0",  # Dictionary attack
                "--potfile-path", pot_file,
                "--quiet",
                hash_file,
            ]

            if wordlist:
                cmd.append(wordlist)

            if rules:
                cmd.extend(["-r", rules])

            result = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=timeout,
            )

            # Check potfile for cracked hash
            if Path(pot_file).exists():
                with open(pot_file) as f:
                    for line in f:
                        if hash_value in line:
                            plaintext = line.split(":")[-1].strip()
                            return CrackResult(
                                hash_value=hash_value,
                                hash_type=hash_type,
                                cracked=True,
                                plaintext=plaintext,
                                method="hashcat",
                                wordlist_used=wordlist,
                            )

            return CrackResult(
                hash_value=hash_value,
                hash_type=hash_type,
                cracked=False,
                method="hashcat",
                wordlist_used=wordlist,
            )

        except subprocess.TimeoutExpired:
            return CrackResult(
                hash_value=hash_value,
                hash_type=hash_type,
                cracked=False,
                method="hashcat (timeout)",
            )
        except Exception as e:
            return CrackResult(
                hash_value=hash_value,
                hash_type=hash_type,
                cracked=False,
                method=f"hashcat (error: {e})",
            )
        finally:
            Path(hash_file).unlink(missing_ok=True)
            Path(pot_file).unlink(missing_ok=True)

    def _crack_john(
        self,
        hash_value: str,
        hash_type: HashType,
        wordlist: Optional[str],
        timeout: int,
    ) -> CrackResult:
        """Crack hash using John the Ripper."""
        if hash_type not in self.JOHN_FORMATS:
            return CrackResult(
                hash_value=hash_value,
                hash_type=hash_type,
                cracked=False,
                method="john",
            )

        john_format = self.JOHN_FORMATS[hash_type]

        with tempfile.NamedTemporaryFile(mode="w", suffix=".hash", delete=False) as f:
            f.write(f"user:{hash_value}\n")
            hash_file = f.name

        try:
            cmd = [
                "john",
                f"--format={john_format}",
                hash_file,
            ]

            if wordlist:
                cmd.append(f"--wordlist={wordlist}")

            subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=timeout,
            )

            # Show cracked
            result = subprocess.run(
                ["john", "--show", hash_file],
                capture_output=True,
                text=True,
            )

            if result.stdout:
                for line in result.stdout.strip().split("\n"):
                    if ":" in line and "password" not in line.lower():
                        parts = line.split(":")
                        if len(parts) >= 2:
                            plaintext = parts[1]
                            return CrackResult(
                                hash_value=hash_value,
                                hash_type=hash_type,
                                cracked=True,
                                plaintext=plaintext,
                                method="john",
                                wordlist_used=wordlist,
                            )

            return CrackResult(
                hash_value=hash_value,
                hash_type=hash_type,
                cracked=False,
                method="john",
                wordlist_used=wordlist,
            )

        except subprocess.TimeoutExpired:
            return CrackResult(
                hash_value=hash_value,
                hash_type=hash_type,
                cracked=False,
                method="john (timeout)",
            )
        except Exception as e:
            return CrackResult(
                hash_value=hash_value,
                hash_type=hash_type,
                cracked=False,
                method=f"john (error: {e})",
            )
        finally:
            Path(hash_file).unlink(missing_ok=True)

    def _crack_python(
        self,
        hash_value: str,
        hash_type: HashType,
        wordlist: Optional[str],
        timeout: int,
    ) -> CrackResult:
        """Crack simple hashes using Python (fallback)."""
        hash_funcs = {
            HashType.MD5: hashlib.md5,
            HashType.SHA1: hashlib.sha1,
            HashType.SHA256: hashlib.sha256,
            HashType.SHA512: hashlib.sha512,
        }

        if hash_type not in hash_funcs:
            return CrackResult(
                hash_value=hash_value,
                hash_type=hash_type,
                cracked=False,
                method="python (unsupported type)",
            )

        hash_func = hash_funcs[hash_type]
        attempts = 0
        start_time = datetime.now()

        # Try wordlist
        if wordlist and Path(wordlist).exists():
            with open(wordlist, "r", errors="ignore") as f:
                for line in f:
                    if (datetime.now() - start_time).total_seconds() > timeout:
                        break

                    word = line.strip()
                    attempts += 1

                    if hash_func(word.encode()).hexdigest() == hash_value.lower():
                        return CrackResult(
                            hash_value=hash_value,
                            hash_type=hash_type,
                            cracked=True,
                            plaintext=word,
                            method="python",
                            attempts=attempts,
                            wordlist_used=wordlist,
                        )

        return CrackResult(
            hash_value=hash_value,
            hash_type=hash_type,
            cracked=False,
            method="python",
            attempts=attempts,
            wordlist_used=wordlist,
        )

    def generate_hash(self, plaintext: str, hash_type: HashType) -> str:
        """Generate a hash from plaintext (for testing)."""
        hash_funcs = {
            HashType.MD5: lambda x: hashlib.md5(x.encode()).hexdigest(),
            HashType.SHA1: lambda x: hashlib.sha1(x.encode()).hexdigest(),
            HashType.SHA256: lambda x: hashlib.sha256(x.encode()).hexdigest(),
            HashType.SHA512: lambda x: hashlib.sha512(x.encode()).hexdigest(),
        }

        if hash_type in hash_funcs:
            return hash_funcs[hash_type](plaintext)

        raise ValueError(f"Hash generation not supported for {hash_type.value}")


# Convenience functions


def identify_hash(hash_value: str) -> HashIdentificationResult:
    """Identify a hash type."""
    return HashIdentifier().identify(hash_value)


def crack_hash(
    hash_value: str,
    hash_type: Optional[HashType] = None,
    wordlist: Optional[str] = None,
    timeout: int = 300,
) -> CrackResult:
    """Attempt to crack a hash."""
    return HashCracker().crack(hash_value, hash_type, wordlist, timeout=timeout)
