#!/usr/bin/env python3
"""Extract FictionBook (.fb2 or .fb2.zip) into per-chapter text files.

Usage:
  python scripts/extract_fb2_chapters.py book.fb2.zip --outdir /tmp/book
  python scripts/extract_fb2_chapters.py book.fb2 --outdir /tmp/book --skip-front-matter

Outputs:
  manifest.json with book metadata and chapter list
  chapter_001.txt, chapter_002.txt, ...

Notes:
  - Many FB2 books contain short front-matter sections before numbered chapters.
  - Use --skip-front-matter to exclude sections whose titles are not numeric.
"""
from __future__ import annotations

import argparse
import json
import re
import zipfile
from pathlib import Path
import xml.etree.ElementTree as ET

NS = {"fb": "http://www.gribuser.ru/xml/fictionbook/2.0"}


def clean_text(text: str) -> str:
    return re.sub(r"\s+", " ", text).strip()


def element_text(el) -> str:
    if el is None:
        return ""
    return clean_text("".join(el.itertext()))


def load_fb2(path: Path) -> bytes:
    if path.suffix.lower() == ".zip":
        with zipfile.ZipFile(path) as zf:
            fb2_names = [n for n in zf.namelist() if n.lower().endswith(".fb2")]
            if not fb2_names:
                raise SystemExit(f"No .fb2 file found inside {path}")
            return zf.read(fb2_names[0])
    return path.read_bytes()


def section_title(sec, fallback: str) -> str:
    title_el = sec.find("fb:title", NS)
    if title_el is None:
        return fallback
    parts = [element_text(p) for p in title_el.findall("fb:p", NS)]
    title = clean_text(" ".join(p for p in parts if p))
    return title or fallback


def section_body_text(sec) -> str:
    parts: list[str] = []

    def walk(el):
        tag = el.tag.split("}", 1)[-1]
        if tag == "title":
            return
        if tag in {"p", "subtitle", "text-author", "v"}:
            text = element_text(el)
            if text:
                parts.append(text)
            return
        for child in list(el):
            walk(child)

    walk(sec)
    return "\n\n".join(parts).strip()


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("input", type=Path, help=".fb2 or .fb2.zip file")
    parser.add_argument("--outdir", type=Path, required=True)
    parser.add_argument("--skip-front-matter", action="store_true", help="Keep only sections with numeric titles")
    args = parser.parse_args()

    args.outdir.mkdir(parents=True, exist_ok=True)
    data = load_fb2(args.input)
    (args.outdir / "book.fb2").write_bytes(data)

    root = ET.fromstring(data)
    book_title = element_text(root.find(".//fb:title-info/fb:book-title", NS))
    authors = []
    for author in root.findall(".//fb:title-info/fb:author", NS):
        name = " ".join(
            part
            for part in [
                element_text(author.find("fb:first-name", NS)),
                element_text(author.find("fb:middle-name", NS)),
                element_text(author.find("fb:last-name", NS)),
            ]
            if part
        )
        if name:
            authors.append(name)

    body = root.find("fb:body", NS)
    sections = []
    if body is not None:
        for sec in body.findall("fb:section", NS):
            title = section_title(sec, f"Глава {len(sections) + 1}")
            if args.skip_front_matter and not title.isdigit():
                continue
            text = section_body_text(sec)
            if text:
                sections.append({"title": title, "text": text})

    manifest = {"book_title": book_title, "authors": authors, "chapter_count": len(sections), "chapters": []}
    for i, chapter in enumerate(sections, 1):
        filename = f"chapter_{i:03d}.txt"
        (args.outdir / filename).write_text(chapter["text"], encoding="utf-8")
        manifest["chapters"].append(
            {"index": i, "title": chapter["title"], "file": filename, "chars": len(chapter["text"])}
        )

    (args.outdir / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
    print(json.dumps(manifest, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
