"""
GLAMWalk -- Volume 4: Libraries & Archives
04_epub_extractor.py

A complete, stdlib-only EPUB text extractor: an EPUB is just a ZIP file, read with zipfile and xml.etree to find the reading order (container.xml -> .opf -> spine) and extract each chapter in order.

Source: Volume Four, Section 1.4 -- EPUB Is Just a Zip File
Targets Python 3.11.4 -- see Volume One, Chapter 1.5/1.6.
"""

import io
import re
import zipfile
import xml.etree.ElementTree as ET
from html.parser import HTMLParser


class TextExtractor(HTMLParser):
    """Same class as in 03_html_to_text_converter.py -- included
    here too so this file runs standalone."""
    BLOCK_TAGS = {"p", "div", "br", "h1", "h2", "h3", "li", "blockquote"}
    SKIP_TAGS = {"script", "style", "head"}

    def __init__(self):
        super().__init__()
        self._skip_depth = 0
        self.parts = []

    def handle_starttag(self, tag, attrs):
        if tag in self.SKIP_TAGS:
            self._skip_depth += 1
        elif tag in self.BLOCK_TAGS:
            self.parts.append("\n")

    def handle_endtag(self, tag):
        if tag in self.SKIP_TAGS and self._skip_depth > 0:
            self._skip_depth -= 1

    def handle_data(self, data):
        if self._skip_depth == 0:
            self.parts.append(data)


def html_to_text(html_str):
    parser = TextExtractor()
    parser.feed(html_str)
    text = "".join(parser.parts)
    return re.sub(r"\n{3,}", "\n\n", text).strip()

def epub_bytes_to_text(epub_bytes):
    with zipfile.ZipFile(io.BytesIO(epub_bytes)) as zf:
        names = zf.namelist()

        # Step 1: find the package (.opf) file — its location is declared
        # in a small, always-present container file.
        container_xml = zf.read("META-INF/container.xml").decode("utf-8")
        opf_path = re.search(r'full-path="([^"]+)"', container_xml).group(1)

        # Step 2: parse the package file for reading order (the "spine").
        opf_root = ET.fromstring(zf.read(opf_path))
        manifest = {}   # id -> filename
        spine_ids = []   # reading order, as a list of ids

        for el in opf_root.iter():
            tag = el.tag.split("}")[-1]   # strip the XML namespace prefix
            if tag == "item":
                manifest[el.attrib.get("id")] = el.attrib.get("href")
            elif tag == "itemref":
                spine_ids.append(el.attrib.get("idref"))

        # Step 3: read each chapter file, in the declared order, and
        # convert each one to plain text.
        base_dir = "/".join(opf_path.split("/")[:-1])
        chunks = []
        for idref in spine_ids:
            href = manifest.get(idref)
            full_path = f"{base_dir}/{href}" if base_dir else href
            if full_path in names:
                html_str = zf.read(full_path).decode("utf-8", errors="replace")
                chunks.append(html_to_text(html_str))

        return "\n\n".join(chunks)
