import typing

from pathlib import Path

import lxml.etree as etree

import contextlib
import tempfile
import zipfile

from ebookwords.adapter.ebook_parser import models


def parse(file: typing.BinaryIO) -> models.Ebook:
    with _load(file) as epub_folder:
        return _parse_epub_folder(epub_folder)


@contextlib.contextmanager
def _load(epub_file: typing.BinaryIO) -> typing.Generator[Path, None, None]:
    """Loads a file object into an epub folder."""
    with tempfile.NamedTemporaryFile(suffix="epub") as file:
        file.write(epub_file.read())
        with tempfile.TemporaryDirectory() as folder:
            with zipfile.ZipFile(file.name) as zf:
                zf.extractall(folder)
            yield Path(folder)


def _parse_epub_folder(path: Path) -> models.Ebook:
    """Parses the content of an epub folder."""
    root_file = _get_epub_root_file(path)
    content_folder = root_file.parent

    root = etree.parse(root_file).getroot()

    manifest = root.find("{*}manifest")
    if manifest is None:
        raise Exception("Epub is missing manifest.")

    spine = root.find("{*}spine")
    if spine is None:
        raise Exception("Epub is missing spine.")

    title = root.find("{*}metadata/{*}title")
    if title is None or title.text is None:
        raise Exception()

    chapters = [
        _parse_spine_element(element, manifest, content_folder) for element in spine
    ]

    return models.Ebook(title=title.text, chapters=chapters)


def _get_epub_root_file(epub_folder: Path) -> Path:
    """Finds the root file in an epub folder."""
    container_file = Path(epub_folder) / "META-INF" / "container.xml"
    container_root = etree.parse(container_file).getroot()
    root_file = container_root[0][0].get("full-path")
    if root_file is None:
        raise Exception(f"Epub is missing root file {root_file}.")
    return epub_folder / root_file


def _parse_spine_element(
    element: etree.Element, manifest: etree.Element, content_folder: Path
) -> models.EbookChapter:
    idref = element.get("idref")
    if idref is None:
        raise Exception("Spine element is missing 'idref'.")

    xpath = f"{{*}}item[@id='{idref}']"
    item = manifest.find(xpath)
    if item is None:
        raise Exception(
            f"The spine element with id '{idref}' is missing in the manifest."
        )

    item_path = item.get("href")
    if item_path is None:
        raise Exception(
            f"The manifest element with id '{idref}' is missing  the'href' attribute."
        )

    chapter_path = content_folder / item_path
    parser = etree.HTMLParser()
    chapter_element = etree.parse(chapter_path, parser=parser).getroot()

    title_element = chapter_element.find("{*}head/{*}title")
    if title_element is not None and title_element.text is not None:
        title = title_element.text
    else:
        title = chapter_path.stem

    body = chapter_element.find("{*}body")
    if body is None:
        raise Exception(
            f"The chapter file {chapter_path.name} is missing a body element."
        )
    html = etree.tostring(body, method="html", encoding="unicode")

    return models.EbookChapter(title, html)
