
import subprocess
import os
import sys
import signal
from pathlib import Path
from .session import save_session
from . import ui
from .ui import C



def _is_interactive():
    """True if running in a real terminal (not piped)."""
    return sys.stdin.isatty() and sys.stdout.isatty()


def _print_keybinds():
    """Print hashcat interactive keybind hints."""
    print(f"  {C.DIM}┌─────────────────────────────────────────────┐{C.RESET}")
    print(f"  {C.DIM}│  {C.RESET}{C.BOLD}[s]{C.RESET}{C.DIM} status + progress %  {C.RESET}{C.BOLD}[p]{C.RESET}{C.DIM} pause         │{C.RESET}")
    print(f"  {C.DIM}│  {C.RESET}{C.BOLD}[r]{C.RESET}{C.DIM} resume               {C.RESET}{C.BOLD}[b]{C.RESET}{C.DIM} bypass attack  │{C.RESET}")
    print(f"  {C.DIM}│  {C.RESET}{C.BOLD}[c]{C.RESET}{C.DIM} checkpoint abort     {C.RESET}{C.BOLD}[q]{C.RESET}{C.DIM} quit           │{C.RESET}")
    print(f"  {C.DIM}└─────────────────────────────────────────────┘{C.RESET}")


def get_cracked_result(hash_file, mode=None):
    """
    Ask hashcat if this hash has already been cracked (checks the potfile).

    hashcat --show prints "hash:plaintext" for cracked hashes.
    Returns the plaintext string, or None if not yet cracked.
    """
    cmd = ["hashcat"]
    if mode:
        cmd.extend(["-m", mode])
    cmd.extend(["--show", hash_file])
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
        for line in result.stdout.splitlines():
            if ':' in line:
                _hash, plaintext = line.strip().split(':', 1)
                if plaintext:
                    return plaintext
    except Exception:
        pass
    return None



def run_hashcat(hash_file, mode, attack_args, label, gpu_flags):
    """
    Runs hashcat on the given hash/file with the given -m mode, 
    using the arguments in the attack_args array (Ex.: [-a, 0]) 
    and the given gpu_flags array (Ex.: [-O, --force])
    """
    # Skip if already cracked (potfile check)
    already = get_cracked_result(hash_file, mode)
    if already:
        ui.info(f"Already cracked (skipping): {already}")
        return already

    interactive = _is_interactive()

    cmd = ["hashcat", "-m", mode, hash_file] + attack_args + gpu_flags
    if interactive:
        cmd.extend(["--status", "--status-timer=15"])
    else:
        cmd.append("--quiet")

    ui.info(f"  hashcat | mode={mode} | {label}")

    if interactive:
        _print_keybinds()
        proc = subprocess.Popen(cmd, stderr=subprocess.PIPE)
        try:
            proc.wait()
        except KeyboardInterrupt:
            ui.warn("Interrupted — stopping hashcat...")
            proc.send_signal(signal.SIGINT)
            proc.wait()
        stderr_out = proc.stderr.read().decode(errors='ignore') if proc.stderr else ""
        returncode = proc.returncode
    else:
        result = subprocess.run(cmd, capture_output=True, text=True)
        stderr_out = result.stderr
        returncode = result.returncode

    # Exit codes: 0=cracked, 1=exhausted, 2=user-abort, 255=error
    if returncode == 2:
        ui.warn("Hashcat was aborted by user")
    elif returncode not in (0, 1):
        ui.warn(f"Hashcat error (exit code {returncode})")
        if stderr_out:
            first_err = next((l.strip() for l in stderr_out.split('\n') if l.strip()), "")
            if first_err:
                ui.warn(f"  {first_err}")

    return get_cracked_result(hash_file, mode)


def run_wordlist_attack(hash_file, mode, wordlist, gpu_flags, rule=None, label=""):
    """Wordlist attack (-a 0, the default), optionally with a rule file."""
    attack_args = [wordlist]
    if rule and os.path.exists(rule):
        attack_args.extend(["-r", rule])

    rule_name = Path(rule).name if rule and os.path.exists(rule) else "no rule"
    full_label = f"{Path(wordlist).name} | {rule_name}   [{label}]"
    return run_hashcat(hash_file, mode, attack_args, full_label, gpu_flags)


def run_mask_attack(hash_file, mode, mask, gpu_flags, label=""):
    """Mask attack (-a 3)."""
    attack_args = ["-a", "3", mask]
    return run_hashcat(hash_file, mode, attack_args, label or mask, gpu_flags)


def run_prince_attack(hash_file, mode, wordlist, gpu_flags, max_words=3, label=""):
    attack_args = ["-a", "8", wordlist, f"--prince-elem-cnt-max={max_words}"]
    return run_hashcat(hash_file, mode, attack_args, label or f"PRINCE {max_words}-word", gpu_flags)

def run_combinator_attack(hash_file, mode, left_file, right_file, gpu_flags, label=""):
    """Combinator attack (-a 1) — concatenates left_file lines × right_file lines."""
    attack_args = ["-a", "1", left_file, right_file]
    return run_hashcat(hash_file, mode, attack_args, label, gpu_flags)


def run_tracked_attack(hash_file, mode, wordlist, gpu_flags, rule=None,
                       label="", completed_attacks=None):
    """
    Wordlist attack with session tracking.
    Skips attacks that were already completed (for --resume support).
    """
    completed_attacks = completed_attacks if completed_attacks is not None else []
    attack_id = f"{Path(wordlist).name}_{Path(rule).name if rule else 'norule'}"

    if attack_id in completed_attacks:
        ui.info(f"  Skipping completed: {attack_id}")
        return get_cracked_result(hash_file, mode)

    save_session(hash_file, mode, f"running_{attack_id}", completed_attacks)

    result = run_wordlist_attack(hash_file, mode, wordlist, gpu_flags, rule, label)

    completed_attacks.append(attack_id)
    save_session(hash_file, mode, "completed", completed_attacks)
    return result


# JOHN THE RIPPER

def run_john_wordlist(hash_file, wordlist, fmt=None, label=""):
    """Run john with a wordlist. Returns plaintext if cracked."""
    cmd = ["john", hash_file, f"--wordlist={wordlist}"]
    if fmt:
        cmd.append(f"--format={fmt}")
    ui.info(f"  john | {Path(wordlist).name} | {label}")
    subprocess.run(cmd, capture_output=True, text=True)

    show = subprocess.run(["john", "--show", hash_file], capture_output=True, text=True)
    for line in show.stdout.splitlines():
        if ':' in line:
            parts = line.split(':')
            if len(parts) >= 2 and parts[1]:
                return parts[1]
    return None


def run_john_only(hash_str, available_wl):
    """Full John attack for hash types hashcat doesn't support (e.g. argon2)."""
    ui.section("John the Ripper (hashcat does not support this hash type)")
    ui.warn("Argon2 is extremely slow to crack — even a single hash may take hours.")
    from . import config
    hash_file = str(config.STATE_DIR / "john_only.hash")
    with open(hash_file, 'w') as f:
        f.write(hash_str.strip() + "\n")
    for wl_name, wl_path in available_wl.items():
        result = run_john_wordlist(hash_file, wl_path, label=wl_name)
        if result:
            return result
    return None

def write_hash_file(h):
    """Write a hash string to a temp file for hashcat to consume."""
    from . import config
    path = str(config.STATE_DIR / "target.hash")
    with open(path, 'w') as f:
        f.write(h.strip() + "\n")
    return path

