import argparse
import datetime as dt
import json
import multiprocessing as mp
import os
import time
from pathlib import Path


COMMON_PASSWORDS = [
    "123456", "password", "12345678", "qwerty", "123456789", "12345", "1234", "111111",
    "1234567", "dragon", "123123", "baseball", "iloveyou", "trustno1", "sunshine",
    "master", "welcome", "shadow", "ashley", "football", "jesus", "michael", "ninja",
    "mustang", "admin", "admin123", "root", "toor", "pass", "test", "guest", "bitcoin",
    "btc", "wallet", "blockchain", "crypto", "satoshi", "love", "god", "sex", "secret",
    "money", "rich", "king", "queen",
]

ENGLISH_NAMES = [
    "john", "david", "james", "robert", "michael", "william", "richard", "joseph",
    "thomas", "charles", "sarah", "jennifer", "emily", "jessica", "ashley", "amanda",
    "melissa", "stephanie", "elizabeth", "nicole",
]

PINYINS = [
    "a", "ai", "an", "ang", "ao", "ba", "bai", "ban", "bang", "bao", "bei", "ben",
    "bi", "bian", "biao", "bin", "bing", "bo", "cai", "cao", "chen", "cheng", "chi",
    "chong", "chu", "chuan", "da", "dai", "dan", "dao", "de", "deng", "di", "ding",
    "dong", "du", "duan", "fei", "fen", "feng", "fu", "gang", "gao", "ge", "gong",
    "gu", "guan", "guang", "gui", "guo", "hai", "han", "hao", "he", "heng", "hong",
    "hua", "huan", "huang", "hui", "jia", "jian", "jiang", "jiao", "jie", "jin",
    "jing", "jun", "kai", "kang", "ke", "kong", "lei", "li", "lian", "liang", "lin",
    "ling", "liu", "long", "lu", "ma", "mei", "meng", "ming", "nan", "ning", "peng",
    "ping", "qi", "qian", "qiang", "qing", "rong", "rui", "shan", "sheng", "shi",
    "shu", "shuang", "si", "song", "tao", "tian", "ting", "tong", "wan", "wang",
    "wei", "wen", "wu", "xi", "xia", "xian", "xiang", "xiao", "xin", "xing", "xu",
    "xuan", "yan", "yang", "yao", "yi", "yin", "ying", "yong", "you", "yu", "yuan",
    "yue", "yun", "zhan", "zhang", "zhao", "zhen", "zheng", "zhi", "zhong", "zhou",
    "zhu", "zhuo", "zi",
]

COMMON_NAMES = [
    "wei", "fang", "jun", "lei", "yang", "yong", "jian", "tao", "qiang", "min",
    "chao", "bin", "feng", "ping", "hui", "kai", "jie", "hao", "wen", "hua", "dong",
    "shuai", "long", "cheng", "li", "na", "ying", "dan", "ting", "yan", "jing", "xu",
    "zhe", "rui", "han", "chen", "lin", "yu", "xi", "zhuo", "yue", "qi", "lu",
    "meng", "ning", "jia", "xin", "yi", "fan", "shen", "zhi", "run", "bo", "ze",
    "hang",
]

CRYPTO_TERMS = [
    "binance", "okx", "bybit", "metamask", "trust", "ledger", "trezor", "coinbase",
    "eth", "sol", "bnb", "usdt",
]

TAIL_SYMBOLS = ["!", "@", "#", "$", "%", "^", "&", "*", "_", "+", "-", ".", "?", "~", "!!", "@@", "##"]
MID_SYMBOLS = ["_", ".", "@", "#", "$"]
COMMON_NUMBERS = ["123", "1234", "12345", "666", "888", "520", "1314", "111", "222", "000", "1990", "2000", "2010", "2020", "2025"]
KEYBOARD_PATTERNS = ["qwerty", "qwertyuiop", "asdf", "asdfgh", "zxcv", "zxcvbnm", "123456", "123456789", "987654", "qazwsx"]
PREFIXES = ["my", "the", "ok", "no", "yes", "btc", "eth", "crypto", "wallet", "mywallet"]
SUFFIXES = ["123", "!!", "love", "baby", "888", "999", "000"]
REPEAT_PATTERNS = ["aa", "aaa", "1111", "!!!!", "$$$$", "abcabc", "123123", "520520", "13141314"]
LEET_MAP = str.maketrans({"a": "@", "e": "3", "i": "1", "o": "0", "s": "5", "t": "7", "l": "1"})

# =========================
# 配置区：直接改这里的默认参数
# =========================
# 生成进度状态文件前缀。多进程时会生成：
#   p2_generate_state_w01.json
#   p2_generate_state_w02.json
# 这样的文件。
STATE_FILE_PATH = "p2_generate_state"

# 候选密码分片输出目录。
OUTPUT_DIR = "p2_chunks"

# 全部生成完成后，在输出目录里写入的完成标记文件名。
DONE_FILE_NAME = "_done.json"

# 分片文件名前缀。多进程时实际文件类似：
#   chunk_w01_000001.txt
#   chunk_w02_000001.txt
FILE_PREFIX = "chunk"

# 每个分片文件的最大大小，单位 MB。
# 文件写满后会自动切到下一个分片。
MAX_FILE_MB = 1024

# 最多保持几个文件
MAX_FILE_COUNT = 10

# 默认生成进程数。默认取 CPU 核心数。
DEFAULT_WORKERS = max(1, os.cpu_count() or 1)

# 单个进程每次在内存中缓存多少行再批量写盘。
# 值越大通常写盘越快，但内存占用也会稍高。
DEFAULT_BUFFER_LINES = 5000


def unique_keep_order(items):
    seen = set()
    for item in items:
        if item not in seen:
            seen.add(item)
            yield item


def build_words():
    double_pinyins = [
        p1 + p2
        for p1 in COMMON_NAMES
        for p2 in COMMON_NAMES
        if len(p1 + p2) <= 12
    ]
    return list(unique_keep_order(COMMON_PASSWORDS + ENGLISH_NAMES + PINYINS + double_pinyins + CRYPTO_TERMS))


def build_dates(start_year, end_year):
    current = dt.date(start_year, 1, 1)
    end = dt.date(end_year, 12, 31)
    values = []
    while current <= end:
        values.extend([
            current.strftime("%Y%m%d"),
            current.strftime("%Y%m"),
            current.strftime("%Y"),
            current.strftime("%m%d%Y"),
            current.strftime("%Y-%m-%d"),
        ])
        if current.day <= 10:
            values.append(current.strftime("%y%m%d"))
        current += dt.timedelta(days=1)
    return list(unique_keep_order(values))


def word_versions(word):
    for value in unique_keep_order([word.lower(), word.capitalize(), word.upper()]):
        leet = value.translate(LEET_MAP)
        yield from unique_keep_order([value, leet])


def build_bases():
    bases = []
    for word in build_words():
        bases.extend(word_versions(word))
    return bases


def date_candidates(base, date_value):
    for sym in MID_SYMBOLS:
        yield f"{base}{sym}{date_value}"
        yield f"{date_value}{sym}{base}"

    for num in COMMON_NUMBERS:
        yield f"{base}{num}"
        for sym in TAIL_SYMBOLS[:8]:
            yield f"{base}{num}{sym}"

    for pattern in KEYBOARD_PATTERNS:
        yield f"{base}{pattern}"
        for sym in TAIL_SYMBOLS[:5]:
            yield f"{base}{pattern}{sym}"

    for prefix in PREFIXES:
        yield f"{prefix}{base}{date_value}"

    for suffix in SUFFIXES:
        yield f"{base}{suffix}"
        for sym in TAIL_SYMBOLS[:5]:
            yield f"{base}{suffix}{sym}"

    for repeat in REPEAT_PATTERNS:
        yield f"{base}{repeat}"
        for sym in ["!", "@", "#"]:
            yield f"{base}{repeat}{sym}"


DATE_RULE_COUNT = (
    len(MID_SYMBOLS) * 2
    + len(COMMON_NUMBERS) * (1 + 8)
    + len(KEYBOARD_PATTERNS) * (1 + 5)
    + len(PREFIXES)
    + len(SUFFIXES) * (1 + 5)
    + len(REPEAT_PATTERNS) * (1 + 3)
)


def combo_words_for_base(base, words):
    return [other for other in words[:60] if base != other and len(base + other) <= 16]


def base_candidate_count(base, dates, words):
    return len(dates) * DATE_RULE_COUNT + len(combo_words_for_base(base, words)) * 6


def yield_base_candidates(base, dates, words, skip=0):
    date_block_total = len(dates) * DATE_RULE_COUNT
    if skip < date_block_total:
        date_index, date_offset = divmod(skip, DATE_RULE_COUNT)
        for date_value in dates[date_index:]:
            for candidate_index, candidate in enumerate(date_candidates(base, date_value)):
                if date_offset and candidate_index < date_offset:
                    continue
                yield candidate
            date_offset = 0
        skip = 0
    else:
        skip -= date_block_total

    combos = combo_words_for_base(base, words)
    combo_index, combo_offset = divmod(skip, 6)
    for other in combos[combo_index:]:
        combo = base + other
        variants = [combo] + [f"{combo}{sym}" for sym in TAIL_SYMBOLS[:5]]
        for candidate_index, candidate in enumerate(variants):
            if combo_offset and candidate_index < combo_offset:
                continue
            yield candidate
        combo_offset = 0


def save_json_file(path, payload):
    tmp = path.with_suffix(path.suffix + ".tmp")
    with tmp.open("w", encoding="utf-8") as f:
        json.dump(payload, f, ensure_ascii=False, indent=2)
    for _ in range(10):
        try:
            os.replace(tmp, path)
            return
        except PermissionError:
            time.sleep(0.2)
    with path.open("w", encoding="utf-8") as f:
        json.dump(payload, f, ensure_ascii=False, indent=2)
    tmp.unlink(missing_ok=True)


def load_json_file(path, default):
    try:
        with path.open("r", encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError:
        return default
    except (json.JSONDecodeError, OSError, ValueError) as exc:
        raise SystemExit(f"State file is broken: {path} ({exc})")


def worker_prefix(file_prefix, worker_id):
    return f"{file_prefix}_w{worker_id:02d}"


def chunk_txt_name(prefix, index):
    return f"{prefix}_{index:06d}.txt"


def chunk_part_name(prefix, index):
    return f"{prefix}_{index:06d}.part"


def worker_state_path(base_state_path, worker_id):
    return base_state_path.parent / f"{base_state_path.name}_w{worker_id:02d}.json"


def generated_file_count(output_dir, file_prefix):
    count = 0
    for path in output_dir.glob(f"{file_prefix}_w*.*"):
        if path.suffix in {".txt", ".part"}:
            count += 1
    return count


def wait_for_file_slot(output_dir, file_prefix, max_file_count, worker_id=None):
    if max_file_count <= 0:
        return

    label = f"[p2-generate][w{worker_id:02d}]" if worker_id is not None else "[p2-generate]"
    notified = False
    while generated_file_count(output_dir, file_prefix) >= max_file_count:
        if not notified:
            print(f"{label} paused: generated file count reached {max_file_count}", flush=True)
            notified = True
        time.sleep(2)
    if notified:
        print(f"{label} resumed: generated file count below {max_file_count}", flush=True)


def prepare_output_dir(output_dir, state_prefix, file_prefix, workers):
    for old in output_dir.glob(f"{file_prefix}_w*.txt"):
        old.unlink(missing_ok=True)
    for old in output_dir.glob(f"{file_prefix}_w*.part"):
        old.unlink(missing_ok=True)
    for worker_id in range(1, workers + 1):
        worker_state_path(state_prefix, worker_id).unlink(missing_ok=True)


def load_worker_state(path):
    data = load_json_file(path, None)
    if not data:
        return {
            "task_pos": 0,
            "skip_in_base": 0,
            "chunk_index": 1,
            "current_part": None,
            "current_bytes": 0,
            "candidates_done": 0,
        }
    return {
        "task_pos": int(data.get("task_pos", 0)),
        "skip_in_base": int(data.get("skip_in_base", 0)),
        "chunk_index": int(data.get("chunk_index", 1)),
        "current_part": data.get("current_part"),
        "current_bytes": int(data.get("current_bytes", 0)),
        "candidates_done": int(data.get("candidates_done", 0)),
    }


def save_worker_state(path, state):
    payload = dict(state)
    payload["updated_at"] = dt.datetime.now().isoformat(timespec="seconds")
    save_json_file(path, payload)


def ensure_part_file(output_dir, prefix, state):
    if state["current_part"]:
        path = output_dir / state["current_part"]
        if path.exists():
            return path

    state["current_part"] = chunk_part_name(prefix, state["chunk_index"])
    state["current_bytes"] = 0
    return output_dir / state["current_part"]


def finalize_part_file(output_dir, state):
    if not state["current_part"]:
        return None
    part_path = output_dir / state["current_part"]
    if not part_path.exists():
        return None
    txt_path = output_dir / (state["current_part"][:-5] + ".txt")
    os.replace(part_path, txt_path)
    return txt_path


def rotate_part_file(output_dir, prefix, state):
    finalized = finalize_part_file(output_dir, state)
    state["chunk_index"] += 1
    state["current_part"] = chunk_part_name(prefix, state["chunk_index"])
    state["current_bytes"] = 0
    return finalized


def flush_buffer(chunk_file, buffer_lines):
    if not buffer_lines:
        return 0
    chunk_file.write("".join(buffer_lines))
    count = len(buffer_lines)
    buffer_lines.clear()
    return count


def worker_main(worker_id, args_dict):
    output_dir = Path(args_dict["output_dir"]).resolve()
    state_prefix = Path(args_dict["state_file"]).resolve()
    state_path = worker_state_path(state_prefix, worker_id)
    worker_prefix_value = worker_prefix(args_dict["file_prefix"], worker_id)
    words = build_words()
    dates = build_dates(args_dict["start_year"], args_dict["end_year"])
    bases = build_bases()
    assigned_bases = bases[worker_id - 1::args_dict["workers"]]
    max_bytes = int(args_dict["max_file_mb"] * 1024 * 1024)
    checkpoint_seconds = args_dict["checkpoint_seconds"]
    checkpoint_lines = args_dict["checkpoint_lines"]
    max_file_count = args_dict["max_file_count"]

    state = load_worker_state(state_path)
    if state["task_pos"] >= len(assigned_bases) and not state["current_part"]:
        print(f"[p2-generate][w{worker_id:02d}] already finished", flush=True)
        return 0
    wait_for_file_slot(output_dir, args_dict["file_prefix"], max_file_count, worker_id)
    part_path = ensure_part_file(output_dir, worker_prefix_value, state)
    if part_path.exists() and state["current_bytes"] == 0:
        state["current_bytes"] = part_path.stat().st_size
    chunk_file = part_path.open("a", encoding="utf-8", newline="\n", buffering=1024 * 1024)
    buffer_lines = []
    buffered_bytes = 0
    last_checkpoint = time.monotonic()

    print(
        f"[p2-generate][w{worker_id:02d}] tasks={len(assigned_bases)} "
        f"resume_task={state['task_pos']} chunk={state['current_part']}",
        flush=True,
    )

    try:
        for task_pos in range(state["task_pos"], len(assigned_bases)):
            base = assigned_bases[task_pos]
            local_skip = state["skip_in_base"] if task_pos == state["task_pos"] else 0
            produced_for_base = local_skip

            for candidate in yield_base_candidates(base, dates, words, local_skip):
                line = candidate + "\n"
                line_bytes = len(line.encode("utf-8"))

                if state["current_bytes"] + buffered_bytes + line_bytes > max_bytes:
                    flush_buffer(chunk_file, buffer_lines)
                    state["current_bytes"] += buffered_bytes
                    buffered_bytes = 0
                    chunk_file.flush()
                    chunk_file.close()
                    finalized = rotate_part_file(output_dir, worker_prefix_value, state)
                    if finalized:
                        print(f"[p2-generate][w{worker_id:02d}] completed {finalized.name}", flush=True)
                    wait_for_file_slot(output_dir, args_dict["file_prefix"], max_file_count, worker_id)
                    part_path = output_dir / state["current_part"]
                    chunk_file = part_path.open("a", encoding="utf-8", newline="\n", buffering=1024 * 1024)

                buffer_lines.append(line)
                buffered_bytes += line_bytes
                produced_for_base += 1
                state["candidates_done"] += 1
                state["skip_in_base"] = produced_for_base

                should_checkpoint = (
                    state["candidates_done"] % checkpoint_lines == 0
                    or time.monotonic() - last_checkpoint >= checkpoint_seconds
                )
                if should_checkpoint:
                    flushed = flush_buffer(chunk_file, buffer_lines)
                    if flushed:
                        state["current_bytes"] += buffered_bytes
                        buffered_bytes = 0
                    chunk_file.flush()
                    save_worker_state(state_path, state)
                    last_checkpoint = time.monotonic()

            flushed = flush_buffer(chunk_file, buffer_lines)
            if flushed:
                state["current_bytes"] += buffered_bytes
                buffered_bytes = 0
            state["task_pos"] = task_pos + 1
            state["skip_in_base"] = 0

            if state["task_pos"] % 10 == 0:
                chunk_file.flush()
                save_worker_state(state_path, state)

    except KeyboardInterrupt:
        chunk_file.flush()
        save_worker_state(state_path, state)
        print(f"[p2-generate][w{worker_id:02d}] interrupted", flush=True)
        return 1
    except Exception:
        chunk_file.flush()
        save_worker_state(state_path, state)
        raise
    finally:
        flushed = flush_buffer(chunk_file, buffer_lines)
        if flushed:
            state["current_bytes"] += buffered_bytes
        chunk_file.flush()
        chunk_file.close()
        save_worker_state(state_path, state)

    finalized = finalize_part_file(output_dir, state)
    if finalized:
        print(f"[p2-generate][w{worker_id:02d}] finalized {finalized.name}", flush=True)
    state["current_part"] = None
    state["current_bytes"] = 0
    save_worker_state(state_path, state)
    return 0


def run_generator(args):
    if args.start_line and args.workers > 1:
        raise SystemExit("--start-line is only supported with --workers 1")
    if args.limit and args.workers > 1:
        raise SystemExit("--limit is only supported with --workers 1")

    output_dir = Path(args.output_dir).resolve()
    output_dir.mkdir(parents=True, exist_ok=True)
    state_prefix = Path(args.state_file).resolve()
    done_path = output_dir / args.done_file

    if not args.resume:
        done_path.unlink(missing_ok=True)
        prepare_output_dir(output_dir, state_prefix, args.file_prefix, args.workers)

    print(f"[p2-generate] output dir: {output_dir}", flush=True)
    print(f"[p2-generate] workers: {args.workers}", flush=True)
    print(f"[p2-generate] max file size: {args.max_file_mb} MB", flush=True)
    if args.max_file_count > 0:
        print(f"[p2-generate] max file count: {args.max_file_count}", flush=True)

    if args.workers == 1:
        return run_single_process_generator(args, output_dir, state_prefix, done_path)

    args_dict = {
        "output_dir": str(output_dir),
        "state_file": str(state_prefix),
        "file_prefix": args.file_prefix,
        "workers": args.workers,
        "max_file_mb": args.max_file_mb,
        "checkpoint_lines": args.checkpoint_lines,
        "checkpoint_seconds": args.checkpoint_seconds,
        "start_year": args.start_year,
        "end_year": args.end_year,
        "buffer_lines": args.buffer_lines,
        "max_file_count": args.max_file_count,
    }

    processes = []
    try:
        for worker_id in range(1, args.workers + 1):
            proc = mp.Process(target=worker_entrypoint, args=(worker_id, args_dict), name=f"p2-gen-w{worker_id:02d}")
            proc.start()
            processes.append(proc)

        exit_code = 0
        for proc in processes:
            proc.join()
            if proc.exitcode not in (0, None):
                exit_code = proc.exitcode or 1

        if exit_code == 0:
            payload = {
                "finished_at": dt.datetime.now().isoformat(timespec="seconds"),
                "workers": args.workers,
            }
            save_json_file(done_path, payload)
            print(f"[p2-generate] all workers finished, done marker written to {done_path}", flush=True)
        return exit_code
    except KeyboardInterrupt:
        print("[p2-generate] stopping workers...", flush=True)
        for proc in processes:
            if proc.is_alive():
                proc.terminate()
        for proc in processes:
            proc.join()
        return 1


def worker_entrypoint(worker_id, args_dict):
    raise SystemExit(worker_main(worker_id, args_dict))


def candidate_stream_from(start_line=0, start_year=1940, end_year=2015):
    words = build_words()
    dates = build_dates(start_year, end_year)
    skip = start_line
    line_no = 0

    for base in build_bases():
        group_count = base_candidate_count(base, dates, words)
        if skip >= group_count:
            skip -= group_count
            line_no += group_count
            continue

        line_no += skip
        for candidate in yield_base_candidates(base, dates, words, skip):
            line_no += 1
            if line_no > start_line:
                yield line_no, candidate
        skip = 0


def run_single_process_generator(args, output_dir, state_prefix, done_path):
    state_path = worker_state_path(state_prefix, 1)
    if not args.resume:
        state_path.unlink(missing_ok=True)

    max_bytes = int(args.max_file_mb * 1024 * 1024)
    worker_prefix_value = worker_prefix(args.file_prefix, 1)
    state = load_worker_state(state_path) if args.resume else {
        "task_pos": 0,
        "skip_in_base": 0,
        "chunk_index": 1,
        "current_part": None,
        "current_bytes": 0,
        "candidates_done": args.start_line,
    }
    if not args.resume:
        state["candidates_done"] = args.start_line

    part_path = ensure_part_file(output_dir, worker_prefix_value, state)
    if part_path.exists() and state["current_bytes"] == 0:
        state["current_bytes"] = part_path.stat().st_size

    chunk_file = part_path.open("a", encoding="utf-8", newline="\n", buffering=1024 * 1024)
    buffer_lines = []
    buffered_bytes = 0
    last_checkpoint = time.monotonic()
    finished_all = True

    print(f"[p2-generate] start line: {state['candidates_done']:,}", flush=True)
    wait_for_file_slot(output_dir, args.file_prefix, args.max_file_count)

    try:
        for index, candidate in candidate_stream_from(args.start_line if not args.resume else state["candidates_done"], args.start_year, args.end_year):
            line = candidate + "\n"
            line_bytes = len(line.encode("utf-8"))

            if state["current_bytes"] + buffered_bytes + line_bytes > max_bytes:
                flush_buffer(chunk_file, buffer_lines)
                state["current_bytes"] += buffered_bytes
                buffered_bytes = 0
                chunk_file.flush()
                chunk_file.close()
                finalized = finalize_part_file(output_dir, state)
                if finalized:
                    print(f"[p2-generate] completed {finalized.name}", flush=True)
                state["chunk_index"] += 1
                state["current_part"] = chunk_part_name(worker_prefix_value, state["chunk_index"])
                state["current_bytes"] = 0
                wait_for_file_slot(output_dir, args.file_prefix, args.max_file_count)
                part_path = output_dir / state["current_part"]
                chunk_file = part_path.open("a", encoding="utf-8", newline="\n", buffering=1024 * 1024)

            buffer_lines.append(line)
            buffered_bytes += line_bytes
            state["candidates_done"] = index

            should_checkpoint = (
                state["candidates_done"] % args.checkpoint_lines == 0
                or time.monotonic() - last_checkpoint >= args.checkpoint_seconds
            )
            if should_checkpoint:
                flush_buffer(chunk_file, buffer_lines)
                state["current_bytes"] += buffered_bytes
                buffered_bytes = 0
                chunk_file.flush()
                save_worker_state(state_path, state)
                last_checkpoint = time.monotonic()

            if args.limit and index - args.start_line >= args.limit:
                finished_all = False
                break

    except KeyboardInterrupt:
        finished_all = False
        print("[p2-generate] interrupted, saving state...", flush=True)
    finally:
        flush_buffer(chunk_file, buffer_lines)
        state["current_bytes"] += buffered_bytes
        chunk_file.flush()
        chunk_file.close()
        save_worker_state(state_path, state)

    if finished_all:
        finalized = finalize_part_file(output_dir, state)
        if finalized:
            print(f"[p2-generate] finalized {finalized.name}", flush=True)
        state["current_part"] = None
        state["current_bytes"] = 0
        save_worker_state(state_path, state)
        payload = {
            "finished_at": dt.datetime.now().isoformat(timespec="seconds"),
            "workers": 1,
            "candidates_done": state["candidates_done"],
        }
        save_json_file(done_path, payload)
        print(f"[p2-generate] finished, done marker written to {done_path}", flush=True)
    else:
        print(f"[p2-generate] paused at line {state['candidates_done']:,}", flush=True)
    return 0


def parse_args():
    parser = argparse.ArgumentParser(
        description="Generate password candidates into chunk files with a size limit.",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument("--output-dir", default=OUTPUT_DIR, help="Directory used to save generated chunk files.")
    parser.add_argument("--state-file", default=STATE_FILE_PATH, help="Base name used for generator worker state files.")
    parser.add_argument("--done-file", default=DONE_FILE_NAME, help="Completion marker filename inside output dir.")
    parser.add_argument("--file-prefix", default=FILE_PREFIX, help="Chunk filename prefix.")
    parser.add_argument("--max-file-mb", type=float, default=MAX_FILE_MB, help="Maximum size of each chunk file in MB.")
    parser.add_argument("--max-file-count", type=int, default=MAX_FILE_COUNT, help="Pause generation when output file count reaches this value. 0 disables the limit.")
    parser.add_argument("--workers", type=int, default=DEFAULT_WORKERS, help="Number of generator processes.")
    parser.add_argument("--buffer-lines", type=int, default=DEFAULT_BUFFER_LINES, help="Number of candidates buffered before flush.")
    parser.add_argument("--start-line", type=int, default=0, help="Skip to this candidate line before generating. Only for --workers 1.")
    parser.add_argument("--no-resume", dest="resume", action="store_false", help="Ignore existing generator state.")
    parser.set_defaults(resume=True)
    parser.add_argument("--checkpoint-lines", type=int, default=10000, help="Save generator state every N candidates.")
    parser.add_argument("--checkpoint-seconds", type=int, default=30, help="Save generator state at least every N seconds.")
    parser.add_argument("--start-year", type=int, default=1940, help="Date rule start year.")
    parser.add_argument("--end-year", type=int, default=2015, help="Date rule end year.")
    parser.add_argument("--limit", type=int, default=0, help="Generate at most N candidates for testing. Only for --workers 1.")
    return parser.parse_args()


if __name__ == "__main__":
    mp.freeze_support()
    raise SystemExit(run_generator(parse_args()))
