#!/usr/bin/env python3
"""
Batch translation script for AI Engineering From Scratch curriculum.
Translates English content to Simplified Chinese via Claude API.

Usage:
    python translate.py                    # Full translation, auto-resume
    python translate.py --dry-run          # Preview without executing
    python translate.py --stats            # Show progress statistics
    python translate.py --phase 03         # Only translate Phase 3
    python translate.py --file <relpath>   # Only translate one file
"""

import argparse
import json
import os
import re
import signal
import sys
import time
from datetime import datetime, timezone
from pathlib import Path

try:
    import anthropic
except ImportError:
    print("Error: anthropic package not found. Install with: pip install anthropic")
    sys.exit(1)

try:
    import yaml
except ImportError:
    print("Error: pyyaml package not found. Install with: pip install pyyaml")
    sys.exit(1)

# ──────────────────────────────────────────────────────────────────────
# System prompt for technical translation
# ──────────────────────────────────────────────────────────────────────
SYSTEM_PROMPT = """\
You are an expert technical translator specializing in AI/ML engineering education.
Translate the following English content to Simplified Chinese (简体中文).

CRITICAL RULES:
1. DO NOT translate code blocks (``` fenced blocks), ASCII art, mermaid diagrams,
   file paths, URLs, or HTML tags. Leave them exactly as-is.
2. For technical terms on first mention, use: 中文术语 (English Term).
   Examples: 反向传播 (Backpropagation), 梯度下降 (Gradient Descent),
   注意力机制 (Attention Mechanism), 损失函数 (Loss Function).
3. After first mention, use the Chinese term naturally.
4. Preserve ALL markdown formatting: headers (#, ##, etc.), bold, italic,
   lists, links [text](url), images, tables, blockquotes.
5. Preserve ALL links and URLs exactly as written.
6. Translate all explanatory text, instructions, descriptions, and comments.
7. Keep the tone professional and educational.
8. If a sentence is entirely code or a shell command, leave it unchanged.
9. For inline code (`code`), do not translate the code portion.
10. Translate table headers and cell text, but keep code/URLs in cells unchanged.
11. Return ONLY the translated content. No preamble, no explanation.

IMPORTANT: The output must be valid markdown that renders identically to the
original, just with Chinese text where appropriate."""

QUIZ_SYSTEM_PROMPT = """\
You are an expert technical translator. Translate the following quiz questions
from English to Simplified Chinese (简体中文).

Rules:
1. Translate ONLY the text values: questions, options, explanations, correct_answer text.
2. Keep ALL JSON keys exactly as they are (do NOT translate keys).
3. Keep the JSON structure identical (same nesting, same arrays).
4. For multiple-choice options, translate the text but keep the letter labels.
5. Keep any code snippets within the questions unchanged.
6. Return ONLY valid JSON. No markdown, no explanation, no code fences.

The input is a JSON object. Translate all string values to Chinese."""

# ──────────────────────────────────────────────────────────────────────
# Configuration
# ──────────────────────────────────────────────────────────────────────

ROOT = Path(__file__).parent
CONFIG_FILE = ROOT / "translation_config.yaml"
MANIFEST_FILE = ROOT / "translation_manifest.json"

PHASE_DIRS = [
    "00-setup-and-tooling",
    "01-math-foundations",
    "02-ml-fundamentals",
    "03-deep-learning-core",
    "04-computer-vision",
    "05-nlp-foundations-to-advanced",
    "06-speech-and-audio",
    "07-transformers-deep-dive",
    "08-generative-ai",
    "09-reinforcement-learning",
    "10-llms-from-scratch",
    "11-llm-engineering",
    "12-multimodal-ai",
    "13-tools-and-protocols",
    "14-agent-engineering",
    "15-autonomous-systems",
    "16-multi-agent-and-swarms",
    "17-infrastructure-and-production",
    "18-ethics-safety-alignment",
    "19-capstone-projects",
]

# Root-level markdown files to translate (exclude LICENSE, CHANGELOG, SPONSORS)
ROOT_MD = [
    "README.md",
    "ROADMAP.md",
    "CONTRIBUTING.md",
    "FORKING.md",
    "LESSON_TEMPLATE.md",
    "CODE_OF_CONDUCT.md",
]


def load_config():
    """Load configuration from YAML file."""
    defaults = {
        "api": {
            "model": "claude-sonnet-4-20250514",
            "max_tokens": 8192,
            "temperature": 0.0,
        },
        "rate_limit": {
            "requests_per_minute": 40,
            "delay_between_requests": 1.5,
        },
        "processing": {
            "chunk_max_tokens": 10000,
            "retry_max": 3,
            "retry_backoff_base": 2,
        },
        "paths": {
            "source_dir": str(ROOT),
            "target_dir": str(ROOT / "zh"),
            "manifest_file": str(MANIFEST_FILE),
        },
    }
    if CONFIG_FILE.exists():
        with open(CONFIG_FILE, "r", encoding="utf-8") as f:
            user_config = yaml.safe_load(f) or {}
        # Deep merge
        _deep_merge(defaults, user_config)
    return defaults


def _deep_merge(base, override):
    for k, v in override.items():
        if k in base and isinstance(base[k], dict) and isinstance(v, dict):
            _deep_merge(base[k], v)
        else:
            base[k] = v


# ──────────────────────────────────────────────────────────────────────
# Manifest (progress tracking)
# ──────────────────────────────────────────────────────────────────────

class Manifest:
    def __init__(self, path):
        self.path = Path(path)
        self.data = {
            "version": 1,
            "created_at": datetime.now(timezone.utc).isoformat(),
            "updated_at": datetime.now(timezone.utc).isoformat(),
            "total_files": 0,
            "completed_files": 0,
            "failed_files": 0,
            "files": {},
        }
        if self.path.exists():
            try:
                with open(self.path, "r", encoding="utf-8") as f:
                    loaded = json.load(f)
                self.data.update(loaded)
            except (json.JSONDecodeError, IOError):
                pass

    def save(self):
        self.data["updated_at"] = datetime.now(timezone.utc).isoformat()
        tmp = self.path.with_suffix(".tmp")
        with open(tmp, "w", encoding="utf-8") as f:
            json.dump(self.data, f, indent=2, ensure_ascii=False)
        tmp.replace(self.path)

    def mark_done(self, source_path, target_path, word_count):
        self.data["files"][source_path] = {
            "status": "done",
            "target_path": target_path,
            "source_words": word_count,
            "completed_at": datetime.now(timezone.utc).isoformat(),
        }
        self._update_counts()

    def mark_failed(self, source_path, error, retries=0):
        self.data["files"][source_path] = {
            "status": "failed",
            "error": str(error),
            "retries": retries,
            "failed_at": datetime.now(timezone.utc).isoformat(),
        }
        self._update_counts()

    def mark_in_progress(self, source_path):
        self.data["files"][source_path] = {
            "status": "in_progress",
            "started_at": datetime.now(timezone.utc).isoformat(),
        }

    def is_done(self, source_path):
        entry = self.data["files"].get(source_path)
        return entry and entry.get("status") == "done"

    def is_failed(self, source_path):
        entry = self.data["files"].get(source_path)
        return entry and entry.get("status") == "failed"

    def _update_counts(self):
        done = sum(1 for v in self.data["files"].values() if v.get("status") == "done")
        failed = sum(1 for v in self.data["files"].values() if v.get("status") == "failed")
        self.data["completed_files"] = done
        self.data["failed_files"] = failed
        self.data["total_files"] = len(self.data["files"])

    def stats(self):
        total = self.data.get("total_files", 0)
        done = self.data.get("completed_files", 0)
        failed = self.data.get("failed_files", 0)
        pending = total - done - failed
        return {
            "total": total,
            "done": done,
            "failed": failed,
            "pending": pending,
            "created_at": self.data.get("created_at", "N/A"),
            "updated_at": self.data.get("updated_at", "N/A"),
        }


# ──────────────────────────────────────────────────────────────────────
# File discovery
# ──────────────────────────────────────────────────────────────────────

def discover_files(config, phase_filter=None, file_filter=None):
    """
    Discover all translatable files.
    Returns list of (source_path, target_path, file_type) tuples.
    """
    src = Path(config["paths"]["source_dir"])
    tgt = Path(config["paths"]["target_dir"])
    files = []

    # 1. Root-level markdown
    for name in ROOT_MD:
        src_file = src / name
        if src_file.exists():
            tgt_file = tgt / name
            files.append((str(src_file.relative_to(src)), str(tgt_file), "md"))

    # 2. Glossary
    glossary = src / "glossary"
    if glossary.exists():
        for f in glossary.iterdir():
            if f.suffix == ".md" and f.name != "README.md":
                tgt_file = tgt / "glossary" / f.name
                files.append((str(f.relative_to(src)), str(tgt_file), "md"))
        # glossary README.md
        readme = glossary / "README.md"
        if readme.exists():
            files.append((str(readme.relative_to(src)), str(tgt / "glossary" / "README.md"), "md"))

    # 3. Phase files
    phases_dir = src / "phases"
    if not phases_dir.exists():
        return files

    for phase_dir_name in PHASE_DIRS:
        phase_path = phases_dir / phase_dir_name
        if not phase_path.exists():
            continue

        # Apply phase filter
        if phase_filter is not None:
            phase_num = phase_dir_name.split("-")[0]
            if phase_num != phase_filter:
                continue

        for lesson_dir in sorted(phase_path.iterdir()):
            if not lesson_dir.is_dir():
                continue

            # docs/en.md
            en_doc = lesson_dir / "docs" / "en.md"
            if en_doc.exists():
                tgt_doc = tgt / en_doc.relative_to(src)
                # Map en.md -> zh.md
                tgt_doc = tgt_doc.parent / "zh.md"
                rel_src = str(en_doc.relative_to(src))
                rel_tgt = str(tgt_doc.relative_to(tgt))
                if file_filter is None or rel_src == file_filter:
                    files.append((rel_src, rel_tgt, "md"))

            # quiz.json
            quiz = lesson_dir / "quiz.json"
            if quiz.exists():
                tgt_quiz = tgt / quiz.relative_to(src)
                rel_src = str(quiz.relative_to(src))
                rel_tgt = str(tgt_quiz.relative_to(tgt))
                if file_filter is None or rel_src == file_filter:
                    files.append((rel_src, rel_tgt, "quiz"))

            # outputs/*.md
            outputs = lesson_dir / "outputs"
            if outputs.exists():
                for md_file in sorted(outputs.iterdir()):
                    if md_file.suffix == ".md":
                        # en-xxx.md -> zh-xxx.md, or xxx.md -> zh-xxx.md
                        if md_file.name.startswith("en-"):
                            new_name = "zh-" + md_file.name[3:]
                        else:
                            new_name = "zh-" + md_file.name
                        tgt_file = tgt / "outputs" / md_file.relative_to(src).parent / new_name
                        rel_src = str(md_file.relative_to(src))
                        rel_tgt = str(tgt_file.relative_to(tgt))
                        if file_filter is None or rel_src == file_filter:
                            files.append((rel_src, rel_tgt, "md"))

    return files


# ──────────────────────────────────────────────────────────────────────
# Chunking for large files
# ──────────────────────────────────────────────────────────────────────

def split_by_headings(text, max_tokens):
    """
    Split markdown text at heading boundaries, respecting code blocks.
    Returns list of (chunk_text, heading_path) tuples.
    """
    # Rough token estimate
    char_limit = max_tokens * 3.5
    if len(text) <= char_limit:
        return [(text, "")]

    # Find heading positions (## or ### but not inside code blocks)
    lines = text.split("\n")
    chunks = []
    current_chunk = []
    current_heading = ""
    in_code_block = False

    for line in lines:
        stripped = line.strip()
        if stripped.startswith("```"):
            in_code_block = not in_code_block
            current_chunk.append(line)
            continue

        if not in_code_block and re.match(r"^#{2,4}\s", stripped):
            # Check if current chunk is already too large
            chunk_text = "\n".join(current_chunk)
            if len(chunk_text) > char_limit * 0.5 and current_chunk:
                chunks.append(("\n".join(current_chunk), current_heading))
                current_chunk = []
            current_heading = stripped
            current_chunk.append(line)
        else:
            current_chunk.append(line)

    if current_chunk:
        chunks.append(("\n".join(current_chunk), current_heading))

    # If still too large, force split
    result = []
    for chunk_text, heading in chunks:
        if len(chunk_text) > char_limit:
            # Split by paragraphs
            paragraphs = chunk_text.split("\n\n")
            sub_chunk = []
            sub_chars = 0
            for para in paragraphs:
                if sub_chars + len(para) > char_limit and sub_chunk:
                    result.append(("\n\n".join(sub_chunk), heading))
                    sub_chunk = [para]
                    sub_chars = len(para)
                else:
                    sub_chunk.append(para)
                    sub_chars += len(para)
            if sub_chunk:
                result.append(("\n\n".join(sub_chunk), heading))
        else:
            result.append((chunk_text, heading))

    return result if result else [(text, "")]


# ──────────────────────────────────────────────────────────────────────
# Translation engine
# ──────────────────────────────────────────────────────────────────────

class Translator:
    def __init__(self, config):
        self.config = config
        self.client = anthropic.Anthropic()
        self.model = config["api"]["model"]
        self.max_tokens = config["api"]["max_tokens"]
        self.retry_max = config["processing"]["retry_max"]
        self.backoff_base = config["processing"]["retry_backoff_base"]
        self.delay = config["rate_limit"]["delay_between_requests"]

    def translate_text(self, text):
        """Translate a block of text with retry logic."""
        for attempt in range(self.retry_max):
            try:
                response = self.client.messages.create(
                    model=self.model,
                    max_tokens=self.max_tokens,
                    temperature=self.config["api"]["temperature"],
                    system=SYSTEM_PROMPT,
                    messages=[{"role": "user", "content": text}],
                )
                result = response.content[0].text
                # Strip any markdown code fences that Claude might add
                if result.startswith("```") and not text.strip().startswith("```"):
                    # Remove leading and trailing code fences if the original wasn't fenced
                    lines = result.split("\n")
                    if lines[0].startswith("```") and lines[-1].strip().startswith("```"):
                        result = "\n".join(lines[1:-1])
                time.sleep(self.delay)
                return result.strip()
            except Exception as e:
                error_str = str(e).lower()
                if "rate" in error_str or "429" in error_str:
                    wait = self.backoff_base ** attempt * 10
                    print(f"  Rate limited, waiting {wait}s...")
                    time.sleep(wait)
                elif attempt < self.retry_max - 1:
                    wait = self.backoff_base ** attempt * 2
                    print(f"  Error (attempt {attempt+1}), waiting {wait}s: {e}")
                    time.sleep(wait)
                else:
                    raise
        return None

    def translate_quiz(self, json_data):
        """Translate quiz JSON: all string values, preserve structure and keys."""
        # Collect all string values with paths
        paths_and_values = []

        def collect(obj, prefix=""):
            if isinstance(obj, dict):
                for k, v in obj.items():
                    collect(v, f"{prefix}.{k}" if prefix else k)
            elif isinstance(obj, list):
                for i, v in enumerate(obj):
                    collect(v, f"{prefix}[{i}]")
            elif isinstance(obj, str):
                paths_and_values.append((prefix, obj))

        collect(json_data)

        if not paths_and_values:
            return json_data

        # Batch translate: send all as numbered list
        items_text = "\n".join(f"{i+1}. [{path}] {value}" for i, (path, value) in enumerate(paths_and_values))

        prompt = f"""Translate the following quiz question text from English to Simplified Chinese.
Translate ONLY the text values. Keep all JSON keys exactly as they are.
Keep the structure identical. Keep code snippets unchanged.

Return the translated items as a numbered list matching the input format:
1. [{path}] translated_value

Items to translate:
{items_text}"""

        for attempt in range(self.retry_max):
            try:
                response = self.client.messages.create(
                    model=self.model,
                    max_tokens=self.max_tokens,
                    temperature=self.config["api"]["temperature"],
                    system=QUIZ_SYSTEM_PROMPT,
                    messages=[{"role": "user", "content": prompt}],
                )
                result = response.content[0].text
                time.sleep(self.delay)

                # Parse the translated items
                translated = self._parse_quiz_response(result, len(paths_and_values))
                if translated is not None and len(translated) == len(paths_and_values):
                    # Apply translations back to the structure
                    result_data = json.loads(json.dumps(json_data))  # deep copy
                    for i, (path, _) in enumerate(paths_and_values):
                        self._set_value(result_data, path, translated[i])
                    return result_data

                print(f"  Quiz translation parse failed (attempt {attempt+1}), retrying...")
                time.sleep(self.backoff_base ** attempt * 2)
            except Exception as e:
                if attempt < self.retry_max - 1:
                    wait = self.backoff_base ** attempt * 2
                    print(f"  Quiz error (attempt {attempt+1}), waiting {wait}s: {e}")
                    time.sleep(wait)
                else:
                    raise
        return None

    def _parse_quiz_response(self, text, expected_count):
        """Parse numbered list response: '1. [path] value' format."""
        results = []
        # Match: digit. [path] value
        pattern = r"^\d+\.\s*\[([^\]]+)\]\s*(.+)$"
        for line in text.strip().split("\n"):
            line = line.strip()
            m = re.match(pattern, line)
            if m:
                results.append(m.group(2).strip())

        # If we got enough results, good
        if len(results) >= expected_count:
            return results[:expected_count]

        # Fallback: try to extract all non-empty lines that look like translations
        results = []
        for line in text.strip().split("\n"):
            line = line.strip()
            if line and not line.startswith("#") and not line.startswith("```"):
                # Remove the [path] prefix
                m2 = re.match(r"^\d+\.\s*\[[^\]]+\]\s*(.+)$", line)
                if m2:
                    results.append(m2.group(1).strip())
                elif re.match(r"^\d+\.\s+", line):
                    # Numbered item without path - might be continuation
                    pass
        return results if results else None

    def _set_value(self, obj, path, value):
        """Set a value in nested structure using dot-notation path."""
        parts = path.replace("][", ".").replace("[", ".").replace("]", "").split(".")
        parts = [p for p in parts if p]  # remove empty strings

        current = obj
        for part in parts[:-1]:
            if part.isdigit():
                current = current[int(part)]
            else:
                current = current[part]

        last = parts[-1]
        if last.isdigit():
            current[int(last)] = value
        else:
            current[last] = value


# ──────────────────────────────────────────────────────────────────────
# Main orchestration
# ──────────────────────────────────────────────────────────────────────

def run_translation(config, dry_run=False, phase_filter=None, file_filter=None):
    """Main translation loop."""
    manifest = Manifest(config["paths"]["manifest_file"])
    translator = Translator(config)
    files = discover_files(config, phase_filter, file_filter)

    # Filter out completed files
    pending = []
    for src, tgt, ftype in files:
        if not manifest.is_done(src):
            pending.append((src, tgt, ftype))

    total = len(pending)
    done_count = sum(1 for src, _, _ in files if manifest.is_done(src))
    skipped = len(files) - total

    print(f"\n{'='*60}")
    print(f"  Translation Batch")
    print(f"{'='*60}")
    print(f"  Total files to process: {total}")
    print(f"  Already completed:      {done_count}")
    print(f"  Skipped (other filter): {skipped}")
    print(f"  Dry run:                {dry_run}")
    print(f"{'='*60}\n")

    if dry_run or total == 0:
        if total > 0:
            print("Files to translate:")
            for src, tgt, ftype in pending[:20]:
                print(f"  [{ftype:4s}] {src} -> {tgt}")
            if len(pending) > 20:
                print(f"  ... and {len(pending) - 20} more files")
        return

    # Graceful shutdown on Ctrl+C
    interrupted = False

    def handle_sigint(signum, frame):
        nonlocal interrupted
        interrupted = True
        print("\n\nInterrupted! Saving manifest...")
        manifest.save()
        print("Manifest saved. Re-run to resume.")
        sys.exit(0)

    signal.signal(signal.SIGINT, handle_sigint)

    # Process files
    for i, (src, tgt, ftype) in enumerate(pending, 1):
        if interrupted:
            break

        src_path = ROOT / src
        if not src_path.exists():
            print(f"  [{i}/{total}] SKIP (not found): {src}")
            continue

        print(f"  [{i}/{total}] [{ftype:4s}] {src}")

        # Mark in progress
        manifest.mark_in_progress(src)
        manifest.save()

        try:
            if ftype == "quiz":
                _translate_quiz(src_path, tgt, translator, manifest, src)
            else:
                _translate_markdown(src_path, tgt, translator, manifest, src, config)
        except Exception as e:
            word_count = _word_count(src_path.read_text(encoding="utf-8"))
            manifest.mark_failed(src, str(e))
            manifest.save()
            print(f"  FAILED: {e}")

        # Progress
        s = manifest.stats()
        print(f"  Progress: {s['done']} done, {s['failed']} failed, {s['pending']} remaining\n")

    # Final summary
    s = manifest.stats()
    print(f"\n{'='*60}")
    print(f"  Translation Complete (or interrupted)")
    print(f"  Done: {s['done']} | Failed: {s['failed']} | Pending: {s['pending']}")
    print(f"{'='*60}")


def _translate_markdown(src_path, tgt_path, translator, manifest, src, config):
    """Translate a markdown file, with chunking for large files."""
    content = src_path.read_text(encoding="utf-8")
    word_count = _word_count(content)

    # Check if file needs chunking
    max_tokens = config["processing"]["chunk_max_tokens"]
    rough_tokens = len(content) / 3.5

    if rough_tokens > max_tokens:
        # Chunk translation
        chunks = split_by_headings(content, max_tokens)
        translated_parts = []
        for j, (chunk_text, heading) in enumerate(chunks, 1):
            print(f"    Chunk {j}/{len(chunks)} ({len(chunk_text)} chars, heading: {heading[:50]})")
            translated = translator.translate_text(chunk_text)
            if translated:
                translated_parts.append(translated)
            else:
                raise Exception(f"Chunk {j} translation failed")
            time.sleep(0.5)

        result = "\n\n".join(translated_parts)
    else:
        result = translator.translate_text(content)
        if not result:
            raise Exception("Translation returned None")

    # Write translated file
    tgt = ROOT / tgt_path
    tgt.parent.mkdir(parents=True, exist_ok=True)
    tgt.write_text(result, encoding="utf-8")

    manifest.mark_done(src, tgt_path, word_count)
    manifest.save()
    print(f"  -> {tgt_path} ({word_count} words)")


def _translate_quiz(src_path, tgt_path, translator, manifest, src):
    """Translate a quiz.json file."""
    with open(src_path, "r", encoding="utf-8") as f:
        quiz_data = json.load(f)

    result = translator.translate_quiz(quiz_data)
    if result is None:
        raise Exception("Quiz translation failed after all retries")

    # Validate structure
    original_keys = _collect_keys(quiz_data)
    result_keys = _collect_keys(result)
    if original_keys != result_keys:
        raise Exception(f"Key mismatch: original has {len(original_keys)} keys, result has {len(result_keys)} keys")

    # Write translated file
    tgt = ROOT / tgt_path
    tgt.parent.mkdir(parents=True, exist_ok=True)
    with open(tgt, "w", encoding="utf-8") as f:
        json.dump(result, f, indent=2, ensure_ascii=False)

    word_count = sum(_count_strings(quiz_data))
    manifest.mark_done(src, tgt_path, word_count)
    manifest.save()
    print(f"  -> {tgt_path} ({word_count} words)")


def _collect_keys(obj, prefix=""):
    """Collect all keys/paths in a JSON structure."""
    keys = set()
    if isinstance(obj, dict):
        for k, v in obj.items():
            path = f"{prefix}.{k}" if prefix else k
            keys.add(path)
            keys.update(_collect_keys(v, path))
    elif isinstance(obj, list):
        for i, v in enumerate(obj):
            path = f"{prefix}[{i}]"
            keys.update(_collect_keys(v, path))
    return keys


def _count_strings(obj):
    """Count words in all string values of a JSON structure."""
    words = 0
    if isinstance(obj, dict):
        for v in obj.values():
            words += _count_strings(v)
    elif isinstance(obj, list):
        for v in obj:
            words += _count_strings(v)
    elif isinstance(obj, str):
        words += len(obj.split())
    return words


def _word_count(text):
    """Rough word count for English text."""
    return len(text.split())


# ──────────────────────────────────────────────────────────────────────
# Stats
# ──────────────────────────────────────────────────────────────────────

def show_stats(config):
    """Show translation progress statistics."""
    manifest = Manifest(config["paths"]["manifest_file"])
    files = discover_files(config)

    total = len(files)
    s = manifest.stats()

    # Count by type
    md_count = sum(1 for _, _, t in files if t == "md")
    quiz_count = sum(1 for _, _, t in files if t == "quiz")

    # Word count estimate
    total_words = 0
    translated_words = 0
    for src, tgt, ftype in files:
        src_path = ROOT / src
        if src_path.exists():
            content = src_path.read_text(encoding="utf-8")
            wc = _word_count(content)
            total_words += wc
            if manifest.is_done(src):
                translated_words += wc

    print(f"\n{'='*60}")
    print(f"  Translation Statistics")
    print(f"{'='*60}")
    print(f"  Total files:       {total}")
    print(f"    Markdown files:  {md_count}")
    print(f"    Quiz files:      {quiz_count}")
    print(f"")
    print(f"  Progress:")
    print(f"    Completed:       {s['done']}")
    print(f"    Failed:          {s['failed']}")
    print(f"    Pending:         {s['pending']}")
    print(f"")
    print(f"  Word count:")
    print(f"    Total:           {total_words:,}")
    print(f"    Translated:      {translated_words:,}")
    print(f"    Remaining:       {total_words - translated_words:,}")
    print(f"")
    print(f"  Started:           {s['created_at']}")
    print(f"  Last updated:      {s['updated_at']}")
    print(f"{'='*60}\n")


# ──────────────────────────────────────────────────────────────────────
# CLI
# ──────────────────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(description="Translate AI Engineering curriculum to Chinese")
    parser.add_argument("--dry-run", action="store_true", help="Preview files without translating")
    parser.add_argument("--stats", action="store_true", help="Show progress statistics")
    parser.add_argument("--phase", type=str, help="Only translate a specific phase (e.g., 00, 03)")
    parser.add_argument("--file", type=str, help="Only translate a single file (relative path)")
    args = parser.parse_args()

    config = load_config()

    if args.stats:
        show_stats(config)
        return

    run_translation(
        config,
        dry_run=args.dry_run,
        phase_filter=args.phase,
        file_filter=args.file,
    )


if __name__ == "__main__":
    main()
