import argparse
import datetime as dt
import json
import os
import queue
import shlex
import shutil
import subprocess
import sys
import threading
import time
import urllib.parse
import urllib.request
from pathlib import Path


# =========================
# 配置区：直接改这里的默认参数
# =========================
# hashcat 可执行文件路径。留空则走环境变量 HASHCAT 或系统 PATH。
HASHCAT_PATH = r"E:\btc\hashcat\hashcat.exe"

# hashcat 工作目录。留空时，如果上面的 HASHCAT_PATH 是具体文件，
# 会自动使用 hashcat.exe 所在目录。
HASHCAT_WORKDIR = ""

# 目标 hash 文件路径。
HASH_FILE_PATH = r"E:\btc\1239.txt"

# hashcat 找到密码后的输出文件。
FOUND_FILE_PATH = "p2_found.txt"

# hashcat potfile 文件路径，用来记录已破解结果。
POTFILE_PATH = "p2_hashcat.potfile"

# hashcat 模式号。
HASHCAT_MODE = 11300

# hashcat 工作负载等级，对应 -w。
HASHCAT_WORKLOAD = 3

# hashcat session 名称。
HASHCAT_SESSION = "p2_chunks"

# 是否默认给 hashcat 加 --force。
USE_FORCE = True

# Telegram 机器人 token。留空则读环境变量 TG_BOT_TOKEN。
TG_BOT_TOKEN = ""

# Telegram 用户 ID 或聊天 ID。留空则读环境变量。
TG_USER_ID = ""

# Telegram 消息格式。留空表示普通文本。
TG_PARSE_MODE = ""

# 启动时是否发送 Telegram 通知。
TG_NOTIFY_ON_START = False

# 生成器输出目录。这里会持续扫描新的 .txt 分片文件。
INPUT_DIR = "p2_chunks"

# 生成器全部结束后写入的完成标记文件名。
DONE_FILE_NAME = "_done.json"

# 没有新分片时，worker 的轮询间隔，单位秒。
POLL_SECONDS = 5.0


def is_windows():
    return os.name == "nt"


def abs_path(path):
    return os.path.abspath(os.fspath(path))


def default_hashcat():
    return HASHCAT_PATH or os.getenv("HASHCAT") or ("hashcat.exe" if is_windows() else "hashcat")


def default_hash_file():
    if HASH_FILE_PATH:
        return HASH_FILE_PATH
    env_value = os.getenv("P2_HASH_FILE") or os.getenv("HASH_FILE")
    if env_value:
        return env_value
    return "hash.txt"


def resolve_hashcat_workdir(hashcat):
    if HASHCAT_WORKDIR:
        return str(Path(HASHCAT_WORKDIR).expanduser())
    hashcat_path = Path(hashcat).expanduser()
    if hashcat_path.is_file():
        return str(hashcat_path.parent)
    return None


def format_cmd(cmd):
    if is_windows():
        return subprocess.list2cmdline([str(part) for part in cmd])
    return shlex.join([str(part) for part in cmd])


def send_tg(text, token=None, user_id=None):
    token = token or TG_BOT_TOKEN or os.getenv("TG_BOT_TOKEN")
    user_id = user_id or TG_USER_ID or os.getenv("TG_USER_ID") or os.getenv("TG_CHAT_ID")
    if not token or not user_id:
        return False

    url = f"https://api.telegram.org/bot{token}/sendMessage"
    payload = {"chat_id": user_id, "text": text}
    parse_mode = TG_PARSE_MODE or os.getenv("TG_PARSE_MODE")
    if parse_mode:
        payload["parse_mode"] = parse_mode
    body = urllib.parse.urlencode(payload).encode("utf-8")
    try:
        with urllib.request.urlopen(url, data=body, timeout=15) as resp:
            return 200 <= resp.status < 300
    except Exception:
        return False


def reader_thread(pipe, output_queue):
    try:
        for line in iter(pipe.readline, ""):
            output_queue.put(line.rstrip("\r\n"))
    finally:
        output_queue.put(None)


def read_found_passwords(path):
    if not path.exists():
        return []
    with path.open("r", encoding="utf-8", errors="replace") as f:
        return [line.strip() for line in f if line.strip()]


def drain_hashcat_output(output_queue):
    while True:
        try:
            line = output_queue.get_nowait()
        except queue.Empty:
            return False
        if line is None:
            return True
        print(f"[hashcat] {line}", flush=True)


def build_hashcat_cmd(args, wordlist_path):
    cmd = [
        args.hashcat,
        "-m", str(args.mode),
        "-a", "0",
        abs_path(args.hash_file),
        abs_path(wordlist_path),
        "-w", str(args.workload),
        "--session", args.session,
        "--status",
        "--status-timer", str(args.status_timer),
        "--outfile", abs_path(args.outfile),
        "--outfile-format", "2",
        "--potfile-path", abs_path(args.potfile),
    ]
    if args.force:
        cmd.append("--force")
    if args.extra_hashcat_args:
        extra_args = args.extra_hashcat_args
        if extra_args and extra_args[0] == "--":
            extra_args = extra_args[1:]
        cmd.extend(extra_args)
    return cmd


def next_chunk_file(input_dir, done_file):
    files = sorted(
        path for path in input_dir.glob("*.txt")
        if path.is_file()
    )
    if files:
        return files[0], False
    return None, (input_dir / done_file).exists()


def run_single_chunk(args, chunk_path, known_found):
    cmd = build_hashcat_cmd(args, chunk_path)
    print(f"[p2-hashcat] running {chunk_path.name}", flush=True)
    print(f"[p2-hashcat] command: {format_cmd(cmd)}", flush=True)

    hashcat_workdir = resolve_hashcat_workdir(args.hashcat)
    popen_kwargs = {}
    if is_windows():
        popen_kwargs["creationflags"] = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
    else:
        popen_kwargs["start_new_session"] = True

    proc = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        encoding="utf-8",
        errors="replace",
        bufsize=1,
        cwd=hashcat_workdir,
        **popen_kwargs,
    )

    output_queue = queue.Queue()
    thread = threading.Thread(target=reader_thread, args=(proc.stdout, output_queue), daemon=True)
    thread.start()

    last_found_check = 0.0
    completed = False
    try:
        while proc.poll() is None:
            now = time.monotonic()
            drain_hashcat_output(output_queue)

            if now - last_found_check >= args.found_check_seconds:
                last_found_check = now
                found = set(read_found_passwords(Path(args.outfile)))
                new_found = found - known_found
                if new_found:
                    message = "p2/hashcat found passwords:\n" + "\n".join(sorted(new_found))
                    print(f"[p2-hashcat] {message}", flush=True)
                    send_tg(message, args.tg_token, args.tg_user_id)
                    known_found = found
                    if args.stop_on_found:
                        proc.terminate()
                        break

            time.sleep(0.2)

        return_code = proc.wait(timeout=30)
        completed = return_code in (0, 1, 255)
    except KeyboardInterrupt:
        print("[p2-hashcat] interrupted, keeping current chunk file.", flush=True)
        if proc.poll() is None:
            proc.terminate()
        return_code = proc.wait(timeout=30)
    finally:
        drain_hashcat_output(output_queue)

    found = set(read_found_passwords(Path(args.outfile)))
    known_found = found

    if completed and args.delete_after:
        chunk_path.unlink(missing_ok=True)
        print(f"[p2-hashcat] deleted {chunk_path.name}", flush=True)

    return return_code, known_found, completed


def run_hashcat(args):
    input_dir = Path(args.input_dir).resolve()
    input_dir.mkdir(parents=True, exist_ok=True)

    hash_file_path = Path(abs_path(args.hash_file))
    outfile_path = Path(abs_path(args.outfile))
    potfile_path = Path(abs_path(args.potfile))

    if not hash_file_path.exists():
        raise SystemExit(f"Hash file not found: {hash_file_path}")

    hashcat_path = Path(args.hashcat)
    hashcat_found = hashcat_path.exists() or shutil.which(args.hashcat)
    if not hashcat_found:
        raise SystemExit(f"hashcat not found: {args.hashcat}")

    for path in (outfile_path, potfile_path):
        if path.parent and str(path.parent) != ".":
            path.parent.mkdir(parents=True, exist_ok=True)

    print(f"[p2-hashcat] watching: {input_dir}", flush=True)
    print(f"[p2-hashcat] outfile: {outfile_path}", flush=True)
    if args.tg_notify_on_start:
        send_tg(
            f"p2/hashcat worker started\nsession={args.session}\ntime={dt.datetime.now().isoformat(timespec='seconds')}",
            args.tg_token,
            args.tg_user_id,
        )

    known_found = set(read_found_passwords(outfile_path))
    processed = 0

    while True:
        chunk_path, done = next_chunk_file(input_dir, args.done_file)
        if chunk_path is None:
            if done:
                print("[p2-hashcat] no more chunk files, done marker found.", flush=True)
                break
            time.sleep(args.poll_seconds)
            continue

        return_code, known_found, completed = run_single_chunk(args, chunk_path, known_found)
        if return_code not in (0, 1, 255):
            raise SystemExit(f"hashcat failed on {chunk_path.name}, return code={return_code}")

        processed += 1
        if args.stop_on_found and known_found:
            break
        if not completed:
            break

    found = read_found_passwords(outfile_path)
    if found:
        message = "p2/hashcat finished, found passwords:\n" + "\n".join(found)
        print(f"[p2-hashcat] {message}", flush=True)
        send_tg(message, args.tg_token, args.tg_user_id)
    else:
        send_tg(
            f"p2/hashcat finished\nprocessed_files={processed}\ntime={dt.datetime.now().isoformat(timespec='seconds')}",
            args.tg_token,
            args.tg_user_id,
        )
    return 0


def parse_args():
    parser = argparse.ArgumentParser(
        description="Consume generated chunk files with hashcat and delete each file after it finishes.",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument("--input-dir", default=INPUT_DIR, help="Directory containing generated chunk files.")
    parser.add_argument("--done-file", default=DONE_FILE_NAME, help="Completion marker filename inside input dir.")
    parser.add_argument("--poll-seconds", type=float, default=POLL_SECONDS, help="Polling interval when waiting for new chunks.")
    parser.add_argument("--hashcat", default=default_hashcat(), help="Path to hashcat executable.")
    parser.add_argument("--hash-file", default=default_hash_file(), help="Target hash file path.")
    parser.add_argument("--mode", type=int, default=HASHCAT_MODE, help="hashcat mode.")
    parser.add_argument("--workload", "-w", type=int, default=HASHCAT_WORKLOAD, help="hashcat workload level.")
    parser.add_argument("--session", default=HASHCAT_SESSION, help="hashcat session name.")
    parser.add_argument("--force", action="store_true", default=USE_FORCE, help="Pass --force to hashcat.")
    parser.add_argument("--no-force", dest="force", action="store_false", help="Do not pass --force to hashcat.")
    parser.add_argument("--outfile", default=FOUND_FILE_PATH, help="Found password outfile.")
    parser.add_argument("--potfile", default=POTFILE_PATH, help="hashcat potfile path.")
    parser.add_argument("--status-timer", type=int, default=60, help="hashcat status timer in seconds.")
    parser.add_argument("--found-check-seconds", type=float, default=5.0, help="Check found outfile every N seconds.")
    parser.add_argument("--delete-after", action="store_true", default=True, help="Delete chunk file after hashcat finishes it.")
    parser.add_argument("--stop-on-found", action="store_true", help="Stop the worker after new passwords are found.")
    parser.add_argument("--tg-token", default=None, help="Telegram bot token.")
    parser.add_argument("--tg-user-id", default=None, help="Telegram user ID.")
    parser.add_argument("--tg-notify-on-start", action="store_true", default=TG_NOTIFY_ON_START, help="Send a Telegram notification on start.")
    parser.add_argument(
        "extra_hashcat_args",
        nargs=argparse.REMAINDER,
        help="Extra hashcat args after --, for example: -- --username",
    )
    return parser.parse_args()


if __name__ == "__main__":
    raise SystemExit(run_hashcat(parse_args()))
