#!/usr/bin/env python3
"""Read-only EPUB 3 package/nav checks. Not a conformance or completeness test.

Usage: python3 check-wikisource-epub.py your-book.epub
No downloads, extraction, book text output, or file writes. Python standard library.
"""

import argparse
from collections import Counter
import hashlib
import json
from pathlib import Path
import posixpath
import sys
from urllib.parse import unquote, urlsplit
import xml.etree.ElementTree as ET
import zipfile

NS = {"c": "urn:oasis:names:tc:opendocument:xmlns:container",
      "p": "http://www.idpf.org/2007/opf",
      "h": "http://www.w3.org/1999/xhtml"}


def repeated(values):
    return {key: count for key, count in Counter(values).items() if count > 1}


def inspect(path):
    with zipfile.ZipFile(path) as archive:
        entries = archive.infolist()
        if len(entries) > 10000 or sum(x.file_size for x in entries) > 64 * 1024 * 1024:
            raise ValueError("This small checker is limited to 10,000 entries / 64 MiB uncompressed.")
        names = set(archive.namelist())
        container = ET.fromstring(archive.read("META-INF/container.xml"))
        rootfile = container.find(".//c:rootfile", NS)
        if rootfile is None:
            raise ValueError("No package rootfile found.")
        package_name = rootfile.attrib["full-path"]
        package = ET.fromstring(archive.read(package_name))
        items = package.findall("p:manifest/p:item", NS)
        by_id = {}
        for item in items:
            by_id.setdefault(item.attrib["id"], item)
        spine = [x.attrib["idref"] for x in package.findall("p:spine/p:itemref", NS)]
        nav_items = [x for x in items if "nav" in x.attrib.get("properties", "").split()]
        targets = []
        for item in nav_items:
            nav_path = posixpath.normpath(posixpath.join(
                posixpath.dirname(package_name), unquote(item.attrib["href"])))
            nav = ET.fromstring(archive.read(nav_path))
            for section in nav.findall(".//h:nav", NS):
                for link in section.findall(".//h:a", NS):
                    href = link.attrib.get("href", "")
                    url = urlsplit(href)
                    if url.scheme or url.netloc:
                        targets.append({"href": href, "result": "external-not-checked"})
                        continue
                    target = (posixpath.normpath(posixpath.join(
                        posixpath.dirname(nav_path), unquote(url.path)))
                        if url.path else nav_path)
                    result = "ok" if target in names else "missing-file"
                    if result == "ok" and url.fragment:
                        doc = ET.fromstring(archive.read(target))
                        if not any(x.attrib.get("id") == unquote(url.fragment) for x in doc.iter()):
                            result = "missing-fragment"
                    targets.append({"href": href, "result": result})
        return {
            "file": path.name,
            "bytes": path.stat().st_size,
            "sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
            "zip_crc_error": archive.testzip(),
            "duplicate_zip_names": repeated(archive.namelist()),
            "duplicate_manifest_ids": repeated(x.attrib["id"] for x in items),
            "spine_entries": len(spine),
            "duplicate_spine_references": repeated(spine),
            "unresolved_spine_ids": [key for key in spine if key not in by_id],
            "epub3_navigation_documents": len(nav_items),
            "navigation_targets_checked": sum(x["result"] != "external-not-checked" for x in targets),
            "navigation_issues": [x for x in targets if x["result"] != "ok"],
            "scope": "Package identifiers and EPUB 3 navigation only. No text completeness, NCX, rendering, rights, or EPUB conformance verdict. Missing navigation is not a pass.",
        }


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("epub", type=Path)
    args = parser.parse_args()
    try:
        print(json.dumps(inspect(args.epub), ensure_ascii=False, indent=2))
    except (OSError, ValueError, KeyError, ET.ParseError, zipfile.BadZipFile, RuntimeError) as error:
        print(f"Inspection failed: {error}", file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main())
