"""
Wallet Recovery - Cryptocurrency wallet and seed phrase recovery

Provides tools for recovering YOUR OWN cryptocurrency wallets:
- Seed phrase recovery (partial word completion)
- Wallet password brute-forcing
- Address derivation and verification
- Wallet file format support

IMPORTANT: Only for recovering your own wallets.
Never use for unauthorized access to others' funds.
"""

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

from groknroll.mode2600.core import get_mode2600


class WalletType(Enum):
    """Supported cryptocurrency wallet types."""

    # Bitcoin
    BITCOIN_CORE = "bitcoin-core"
    ELECTRUM = "electrum"
    BITCOIN_WALLET = "bitcoin-wallet"

    # Ethereum
    GETH = "geth"
    METAMASK = "metamask"
    MIST = "mist"
    MEW = "mew"

    # Multi-coin
    EXODUS = "exodus"
    ATOMIC = "atomic"
    LEDGER = "ledger"
    TREZOR = "trezor"
    COINBASE = "coinbase"

    # Other
    LITECOIN_CORE = "litecoin-core"
    MONERO = "monero"
    ZCASH = "zcash"
    DOGECOIN = "dogecoin"

    # Generic
    BIP39 = "bip39"
    BIP44 = "bip44"
    UNKNOWN = "unknown"


@dataclass
class SeedRecoveryResult:
    """Result of seed phrase recovery attempt."""

    success: bool
    complete_phrase: Optional[list[str]] = None
    possible_phrases: list[list[str]] = field(default_factory=list)
    valid_addresses: list[str] = field(default_factory=list)
    attempts: int = 0
    duration_seconds: float = 0.0
    method: str = ""


@dataclass
class WalletRecoveryResult:
    """Result of wallet password recovery."""

    success: bool
    password: Optional[str] = None
    wallet_type: WalletType = WalletType.UNKNOWN
    attempts: int = 0
    duration_seconds: float = 0.0
    addresses_found: list[str] = field(default_factory=list)


# BIP39 English wordlist (2048 words)
# For space reasons, we'll load from file or use first/last words for validation
BIP39_WORDLIST_PATH = Path(__file__).parent / "data" / "bip39_english.txt"


class SeedRecoverer:
    """
    Recover BIP39 seed phrases with missing or partial words.

    Use cases:
    - Complete partial words (e.g., "aban" -> "abandon")
    - Fill in missing word positions
    - Validate checksums
    - Derive addresses for verification

    IMPORTANT: Only for recovering YOUR OWN seed phrases.
    """

    # First few BIP39 words for fallback (full list should be loaded)
    BIP39_SAMPLE = [
        "abandon", "ability", "able", "about", "above", "absent", "absorb",
        "abstract", "absurd", "abuse", "access", "accident", "account",
        "accuse", "achieve", "acid", "acoustic", "acquire", "across", "act",
        "action", "actor", "actress", "actual", "adapt", "add", "addict",
        "address", "adjust", "admit", "adult", "advance", "advice", "aerobic",
        "affair", "afford", "afraid", "again", "age", "agent", "agree",
        "ahead", "aim", "air", "airport", "aisle", "alarm", "album",
        "alcohol", "alert", "alien", "all", "alley", "allow", "almost",
        # ... truncated for brevity, full list loaded from file
    ]

    def __init__(self):
        """Initialize seed recoverer."""
        self.mode2600 = get_mode2600()
        self.wordlist = self._load_wordlist()

    def _load_wordlist(self) -> list[str]:
        """Load BIP39 wordlist."""
        if BIP39_WORDLIST_PATH.exists():
            with open(BIP39_WORDLIST_PATH) as f:
                return [line.strip() for line in f if line.strip()]

        # Try to find system wordlist
        system_paths = [
            "/usr/share/dict/bip39.txt",
            Path.home() / ".local" / "share" / "bip39.txt",
        ]

        for path in system_paths:
            if Path(path).exists():
                with open(path) as f:
                    return [line.strip() for line in f if line.strip()]

        # Return sample (should be replaced with full list)
        return self.BIP39_SAMPLE

    def complete_partial_word(self, partial: str) -> list[str]:
        """
        Find BIP39 words matching a partial word.

        Args:
            partial: Partial word (e.g., "aban")

        Returns:
            List of matching BIP39 words
        """
        partial = partial.lower()
        return [w for w in self.wordlist if w.startswith(partial)]

    def recover_missing_words(
        self,
        known_words: list[Optional[str]],
        known_address: Optional[str] = None,
        max_attempts: int = 100000,
        timeout: int = 300,
    ) -> SeedRecoveryResult:
        """
        Recover missing words from a seed phrase.

        Args:
            known_words: List of words (None for missing positions)
            known_address: Known address for validation
            max_attempts: Maximum combinations to try
            timeout: Timeout in seconds

        Returns:
            SeedRecoveryResult with possible phrases
        """
        context = self.mode2600.require_context()

        start_time = datetime.now()
        attempts = 0
        possible_phrases = []

        # Find missing positions
        missing_positions = [i for i, w in enumerate(known_words) if w is None]

        if not missing_positions:
            # All words known, just validate
            if self._validate_checksum(known_words):
                return SeedRecoveryResult(
                    success=True,
                    complete_phrase=known_words,
                    attempts=1,
                    method="checksum_validation",
                )
            return SeedRecoveryResult(success=False, method="invalid_checksum")

        # Log operation
        self.mode2600.log_operation(
            "seed_recovery",
            "seed_recoverer",
            {"missing_count": len(missing_positions)},
            f"Attempting to recover {len(missing_positions)} missing words",
            True,
        )

        # Generate combinations
        if len(missing_positions) > 2:
            # Too many missing words, would take too long
            return SeedRecoveryResult(
                success=False,
                method="too_many_missing",
                attempts=0,
            )

        # Try all combinations
        for combo in itertools.product(self.wordlist, repeat=len(missing_positions)):
            if (datetime.now() - start_time).total_seconds() > timeout:
                break
            if attempts >= max_attempts:
                break

            attempts += 1

            # Build candidate phrase
            candidate = known_words.copy()
            for i, word in zip(missing_positions, combo):
                candidate[i] = word

            # Validate checksum
            if self._validate_checksum(candidate):
                possible_phrases.append(candidate)

                # If we have a known address, verify
                if known_address:
                    addresses = self._derive_addresses(candidate)
                    if known_address in addresses:
                        return SeedRecoveryResult(
                            success=True,
                            complete_phrase=candidate,
                            valid_addresses=addresses,
                            attempts=attempts,
                            duration_seconds=(datetime.now() - start_time).total_seconds(),
                            method="address_match",
                        )

        duration = (datetime.now() - start_time).total_seconds()

        if possible_phrases:
            return SeedRecoveryResult(
                success=True,
                complete_phrase=possible_phrases[0] if len(possible_phrases) == 1 else None,
                possible_phrases=possible_phrases[:10],  # Limit results
                attempts=attempts,
                duration_seconds=duration,
                method="checksum_match",
            )

        return SeedRecoveryResult(
            success=False,
            attempts=attempts,
            duration_seconds=duration,
            method="no_match",
        )

    def _validate_checksum(self, words: list[str]) -> bool:
        """Validate BIP39 checksum."""
        try:
            # Convert words to indices
            indices = []
            for word in words:
                if word not in self.wordlist:
                    return False
                indices.append(self.wordlist.index(word))

            # Convert to bits
            bits = ""
            for idx in indices:
                bits += bin(idx)[2:].zfill(11)

            # Split entropy and checksum
            phrase_length = len(words)
            checksum_bits = phrase_length // 3

            if len(bits) < checksum_bits:
                return False

            entropy_bits = bits[:-checksum_bits]
            checksum = bits[-checksum_bits:]

            # Calculate expected checksum
            entropy_bytes = int(entropy_bits, 2).to_bytes(len(entropy_bits) // 8, "big")
            hash_bytes = hashlib.sha256(entropy_bytes).digest()
            expected_checksum = bin(hash_bytes[0])[2:].zfill(8)[:checksum_bits]

            return checksum == expected_checksum

        except Exception:
            return False

    def _derive_addresses(
        self,
        words: list[str],
        coin: str = "btc",
        count: int = 5,
    ) -> list[str]:
        """Derive addresses from seed phrase (requires additional libraries)."""
        # This would require bip_utils or similar library
        # Placeholder for now
        return []

    def generate_test_phrase(self, word_count: int = 12) -> list[str]:
        """Generate a random valid BIP39 phrase (for testing)."""
        import os

        # Calculate entropy size
        entropy_bits = {12: 128, 15: 160, 18: 192, 21: 224, 24: 256}
        if word_count not in entropy_bits:
            raise ValueError("Word count must be 12, 15, 18, 21, or 24")

        bits = entropy_bits[word_count]
        entropy = os.urandom(bits // 8)

        # Calculate checksum
        checksum = hashlib.sha256(entropy).digest()[0]
        checksum_bits = bits // 32

        # Combine entropy + checksum
        full_bits = bin(int.from_bytes(entropy, "big"))[2:].zfill(bits)
        full_bits += bin(checksum)[2:].zfill(8)[:checksum_bits]

        # Convert to words
        words = []
        for i in range(0, len(full_bits), 11):
            idx = int(full_bits[i:i+11], 2)
            words.append(self.wordlist[idx])

        return words


class WalletRecovery:
    """
    Cryptocurrency wallet password recovery.

    Supports brute-forcing wallet passwords using:
    - hashcat (with appropriate modules)
    - btcrecover (for Bitcoin wallets)
    - Custom Python implementations

    IMPORTANT: Only for YOUR OWN wallets.
    """

    def __init__(self):
        """Initialize wallet recovery."""
        self.mode2600 = get_mode2600()

    def identify_wallet_type(self, wallet_path: Path) -> WalletType:
        """
        Identify the type of a wallet file.

        Args:
            wallet_path: Path to wallet file

        Returns:
            WalletType enum
        """
        if not wallet_path.exists():
            return WalletType.UNKNOWN

        filename = wallet_path.name.lower()

        # Check filename patterns
        if filename == "wallet.dat":
            return WalletType.BITCOIN_CORE
        if filename.endswith(".json") and "keystore" in str(wallet_path):
            return WalletType.GETH
        if "electrum" in filename:
            return WalletType.ELECTRUM
        if filename.endswith(".asar"):
            return WalletType.EXODUS

        # Check file contents
        try:
            with open(wallet_path, "rb") as f:
                header = f.read(100)

            # Bitcoin Core
            if b"berkeley" in header.lower() or b"\x00\x00\x00\x00" == header[:4]:
                return WalletType.BITCOIN_CORE

            # Electrum
            if b'"wallet_type"' in header or header.startswith(b"BIE1"):
                return WalletType.ELECTRUM

            # Geth/Ethereum keystore
            if b'"crypto"' in header and b'"ciphertext"' in header:
                return WalletType.GETH

        except Exception:
            pass

        return WalletType.UNKNOWN

    def recover_password(
        self,
        wallet_path: Path,
        wallet_type: Optional[WalletType] = None,
        wordlist: Optional[str] = None,
        patterns: Optional[list[str]] = None,
        timeout: int = 3600,
    ) -> WalletRecoveryResult:
        """
        Attempt to recover wallet password.

        Args:
            wallet_path: Path to wallet file
            wallet_type: Type of wallet (auto-detected if None)
            wordlist: Path to password wordlist
            patterns: Password patterns to try
            timeout: Timeout in seconds

        Returns:
            WalletRecoveryResult
        """
        context = self.mode2600.require_context()

        start_time = datetime.now()

        # Auto-detect wallet type
        if not wallet_type:
            wallet_type = self.identify_wallet_type(wallet_path)

        # Log operation
        self.mode2600.log_operation(
            "wallet_recovery",
            "wallet_recovery",
            {"wallet_type": wallet_type.value, "path": str(wallet_path)},
            f"Attempting wallet password recovery ({wallet_type.value})",
            True,
        )

        # Select recovery method
        if wallet_type == WalletType.BITCOIN_CORE:
            result = self._recover_bitcoin_core(wallet_path, wordlist, patterns, timeout)
        elif wallet_type == WalletType.ELECTRUM:
            result = self._recover_electrum(wallet_path, wordlist, patterns, timeout)
        elif wallet_type == WalletType.GETH:
            result = self._recover_geth(wallet_path, wordlist, patterns, timeout)
        else:
            result = WalletRecoveryResult(
                success=False,
                wallet_type=wallet_type,
            )

        result.duration_seconds = (datetime.now() - start_time).total_seconds()

        # Log result
        self.mode2600.log_operation(
            "wallet_recovery_result",
            "wallet_recovery",
            {"success": result.success},
            f"Recovery {'successful' if result.success else 'failed'}",
            result.success,
        )

        return result

    def _recover_bitcoin_core(
        self,
        wallet_path: Path,
        wordlist: Optional[str],
        patterns: Optional[list[str]],
        timeout: int,
    ) -> WalletRecoveryResult:
        """Recover Bitcoin Core wallet password."""
        import shutil

        # Try btcrecover if available
        if shutil.which("btcrecover"):
            return self._btcrecover(wallet_path, wordlist, patterns, timeout)

        # Fallback to hashcat if extract available
        if shutil.which("bitcoin2john"):
            return self._hashcat_bitcoin(wallet_path, wordlist, timeout)

        return WalletRecoveryResult(
            success=False,
            wallet_type=WalletType.BITCOIN_CORE,
        )

    def _recover_electrum(
        self,
        wallet_path: Path,
        wordlist: Optional[str],
        patterns: Optional[list[str]],
        timeout: int,
    ) -> WalletRecoveryResult:
        """Recover Electrum wallet password."""
        import shutil

        if shutil.which("btcrecover"):
            return self._btcrecover(wallet_path, wordlist, patterns, timeout)

        return WalletRecoveryResult(
            success=False,
            wallet_type=WalletType.ELECTRUM,
        )

    def _recover_geth(
        self,
        wallet_path: Path,
        wordlist: Optional[str],
        patterns: Optional[list[str]],
        timeout: int,
    ) -> WalletRecoveryResult:
        """Recover Geth/Ethereum keystore password."""
        # Try hashcat mode 15700 (Ethereum Keystore)
        try:
            with open(wallet_path) as f:
                keystore = json.load(f)

            # Extract hash for hashcat
            crypto = keystore.get("crypto", keystore.get("Crypto", {}))
            kdf_params = crypto.get("kdfparams", {})

            # Build hashcat hash
            # Format depends on kdf type (scrypt or pbkdf2)

            return WalletRecoveryResult(
                success=False,
                wallet_type=WalletType.GETH,
            )

        except Exception:
            return WalletRecoveryResult(
                success=False,
                wallet_type=WalletType.GETH,
            )

    def _btcrecover(
        self,
        wallet_path: Path,
        wordlist: Optional[str],
        patterns: Optional[list[str]],
        timeout: int,
    ) -> WalletRecoveryResult:
        """Use btcrecover for wallet recovery."""
        cmd = ["btcrecover", "--wallet", str(wallet_path)]

        if wordlist:
            cmd.extend(["--passwordlist", wordlist])

        if patterns:
            # Write patterns to temp file
            with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
                f.write("\n".join(patterns))
                patterns_file = f.name
            cmd.extend(["--tokenlist", patterns_file])

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

            # Parse output for password
            if "Password found:" in result.stdout:
                for line in result.stdout.split("\n"):
                    if "Password found:" in line:
                        password = line.split("Password found:")[-1].strip()
                        return WalletRecoveryResult(
                            success=True,
                            password=password,
                            wallet_type=WalletType.BITCOIN_CORE,
                        )

            return WalletRecoveryResult(
                success=False,
                wallet_type=WalletType.BITCOIN_CORE,
            )

        except subprocess.TimeoutExpired:
            return WalletRecoveryResult(
                success=False,
                wallet_type=WalletType.BITCOIN_CORE,
            )
        except Exception:
            return WalletRecoveryResult(
                success=False,
                wallet_type=WalletType.BITCOIN_CORE,
            )

    def _hashcat_bitcoin(
        self,
        wallet_path: Path,
        wordlist: Optional[str],
        timeout: int,
    ) -> WalletRecoveryResult:
        """Use hashcat for Bitcoin wallet recovery."""
        # This would require bitcoin2john extraction first
        return WalletRecoveryResult(
            success=False,
            wallet_type=WalletType.BITCOIN_CORE,
        )

    def check_tools(self) -> dict[str, bool]:
        """Check availability of wallet recovery tools."""
        import shutil

        tools = {
            "btcrecover": shutil.which("btcrecover") is not None,
            "hashcat": shutil.which("hashcat") is not None,
            "bitcoin2john": shutil.which("bitcoin2john") is not None,
            "ethereum2john": shutil.which("ethereum2john") is not None,
        }

        return tools
