"""EPUB processing and text extraction module."""

import base64
import re
from dataclasses import dataclass
from typing import Optional

import ebooklib  # type: ignore
from bs4 import BeautifulSoup
from ebooklib import epub
from loguru import logger

from .config import METADATA_FIELDS, ErrorCodes, WarningTypes
from .helpers import ConversionError, ConversionWarning, StrPath


@dataclass
class Chapter:
    """Class representing a chapter in the EPUB book."""

    title: str
    content: str
    order: int
    id: str

    def __str__(self) -> str:
        """Return a string representation of the chapter."""
        return (
            f"Chapter(title='{self.title}', id='{self.id}', order={self.order}, "
            + f"content='{self.content[: len(self.title)]}...')"
        )

    def __eq__(self, other: object) -> bool:
        """Check if two chapters are equal."""
        if not isinstance(other, Chapter):
            return False
        return (
            self.id == other.id
            and self.order == other.order
            and self.title == other.title
            and self.content == other.content
        )

    def __lt__(self, other: object) -> bool:
        """Compare two chapters."""
        if not isinstance(other, Chapter):
            return False
        return self.order < other.order

    def __le__(self, other: object) -> bool:
        """Compare two chapters."""
        if not isinstance(other, Chapter):
            return False
        return self.order <= other.order


@dataclass
class BookMetadata:
    """Class representing book metadata."""

    title: str
    creator: Optional[str] = None
    date: Optional[str] = None
    identifier: Optional[str] = None
    language: Optional[str] = None
    publisher: Optional[str] = None
    description: Optional[str] = None
    cover_image: Optional[str] = None  # Base64 encoded str

    @property
    def book_sentence(self) -> str:
        """Get a sentence that describes the book."""
        return (
            f"A book by {self.creator}, published in {self.date} by {self.publisher}."
            + "Audio generated by Epub2Audio."
        )

    def __str__(self) -> str:
        """Return a string representation of the book metadata."""
        return (
            f"BookMetadata(title={self.title}, "
            + (f"creator={self.creator}, " if self.creator else "")
            + (f"date={self.date}, " if self.date else "")
            + (f"publisher={self.publisher}, " if self.publisher else "")
            + (f"description={self.description}, " if self.description else "")
            + (f"cover_image={len(self.cover_image)}, " if self.cover_image else "")
            + ")"
        )


@dataclass
class CoverImage:
    """Class representing a cover image."""

    image: bytes
    width: int
    height: int


@dataclass
class Book:
    """Class representing a book."""

    metadata: BookMetadata
    chapters: list[Chapter]

    def __str__(self) -> str:
        """Return a string representation of the book."""
        return (
            f"Book(\n\tmetadata={self.metadata},\n\t"
            + f"chapters={'\n\t\t'.join(str(chapter) for chapter in self.chapters)}\n)"
        )


def get_book_length(chapters: list[Chapter]) -> int:
    """Get the length of an EPUB book.

    Args:
        chapters: List of chapters
    """
    return sum(len(chapter.content) for chapter in chapters)


class EpubProcessor:
    """Class for processing EPUB files and extracting content."""

    def __init__(self, epub_path: StrPath):
        """Initialize the EPUB processor.

        Args:
            epub_path: Path to the EPUB file

        Raises:
            ConversionError: If the EPUB file is invalid or cannot be read
        """
        self.epub_path = epub_path
        self.warnings: list[ConversionWarning] = []
        try:
            self.epub = epub.read_epub(epub_path, options={"ignore_ncx": True})
        except Exception as e:
            raise ConversionError(
                f"Failed to read EPUB file: {str(e)}", ErrorCodes.INVALID_EPUB
            ) from e

        self._chapters_in_one_page = False
        self.metadata = self._extract_metadata()
        self.chapters = self._extract_chapters()
        if len(self.chapters) == 0:
            raise ConversionError(
                "No valid chapters found in EPUB file", ErrorCodes.INVALID_EPUB
            )
        if len(self.chapters) == 1:
            logger.warning("Only one chapter found in EPUB file")
            self.warnings.append(
                ConversionWarning(
                    type=WarningTypes.FORMATTING_ISSUE,
                    message="Only one chapter found in EPUB file",
                )
            )
        self.book = Book(metadata=self.metadata, chapters=self.chapters)

        logger.debug(f"Metadata: {self.metadata}")
        logger.trace(f"Number of chapters: {len(self.chapters)}")
        logger.trace(f"Chapter titles: {[chapter.title for chapter in self.chapters]}")
        logger.trace(f"Book length: {get_book_length(self.chapters)}")

    def _extract_metadata(self) -> BookMetadata:
        """Extract metadata from the EPUB file.

        Returns:
            BookMetadata: Extracted metadata
        """
        metadata = {}

        # Extract Dublin Core metadata
        for field in METADATA_FIELDS:
            value = self.epub.get_metadata("DC", field)
            if value:
                metadata[field] = value[0][0]
            else:
                if field == "title":
                    raise ConversionError(
                        "EPUB file missing required title metadata",
                        ErrorCodes.INVALID_EPUB,
                    )
                logger.warning(f"Missing metadata field: {field}")
                self.warnings.append(
                    ConversionWarning(
                        type=WarningTypes.UNSUPPORTED_METADATA,
                        message=f"Missing metadata field: {field}",
                    )
                )

        for cover in self.epub.get_items_of_type(ebooklib.ITEM_COVER):
            logger.trace(f"Cover: {cover}")
            cover_bytes = cover.get_content()
            # cover_image = Image.open(io.BytesIO(cover_bytes))
            # cover_image.thumbnail((256, 256))
            # cover_image.show()
            # TODO: if more than one cover,
            # show the cover images to the user, or use the largest
            if cover_bytes:
                metadata["cover_image"] = base64.b64encode(cover_bytes).decode("utf-8")
                # just take the first cover
                break

        return BookMetadata(**metadata)

    def _clean_text(self, html_content: str) -> str:
        """Clean HTML content and extract plain text.

        Args:
            html_content: Raw HTML content

        Returns:
            str: Cleaned text content
        """
        soup = BeautifulSoup(html_content, "html.parser")

        # Remove script and style elements
        for element in soup(["script", "style"]):
            element.decompose()

        # Handle non-text elements
        for element in soup.find_all(["img", "svg"]):
            logger.warning(f"Skipping non-text element: {element.name}")
            self.warnings.append(
                ConversionWarning(
                    type=WarningTypes.NON_TEXT_ELEMENT,
                    message=f"Skipping non-text element: {element.name}",
                )
            )
            element.decompose()

        # Get text and clean it
        text = soup.get_text()

        # Normalize whitespace
        text = re.sub(r"\s+", " ", text)
        text = text.strip()

        return text

    def _flatten_table_of_contents(
        self, table_of_contents: list[epub.Section | epub.Link]
    ) -> list[epub.EpubItem | epub.Link]:
        """Flatten the table of contents.

        Args:
            table_of_contents: Table of contents

        Returns:
            list[epub.EpubItem]: Flattened list of EPUB items
        """
        logger.trace(f"length of toc: {len(table_of_contents)}")
        flattened_table_of_contents = []
        for item in table_of_contents:
            if isinstance(item, epub.Link):
                link_split = item.href.split("#")
                if len(link_split) > 1:
                    # link within one big page
                    big_page = self.epub.get_item_with_href(link_split[0])
                    if (
                        big_page not in flattened_table_of_contents
                        and not self._chapters_in_one_page
                    ):
                        flattened_table_of_contents.append(big_page)
                    else:
                        self._chapters_in_one_page = True
                    flattened_table_of_contents.append(item)
                else:
                    item_from_link = self.epub.get_item_with_href(item.href)
                    if (
                        item_from_link in flattened_table_of_contents
                        or item_from_link in table_of_contents
                    ):
                        self._chapters_in_one_page = True
                    flattened_table_of_contents.append(item_from_link)
            elif isinstance(item, epub.Section):
                flattened_table_of_contents.extend(
                    self._flatten_table_of_contents(item.children)
                )
            elif isinstance(item, tuple):
                section, children = item
                flattened_table_of_contents.extend(
                    self._flatten_table_of_contents(children)
                )
            else:
                logger.warning(f"Skipping non-link or section item: {item}")
                self.warnings.append(
                    ConversionWarning(
                        type=WarningTypes.FORMATTING_ISSUE,
                        message=f"Skipping non-link or section item: {item}",
                    )
                )
        return flattened_table_of_contents

    def _get_chapters_in_one_page(
        self, item: epub.EpubItem, table_of_contents: list[epub.Link]
    ) -> list[Chapter]:
        """Get the chapters from a single page EPUB format.

        Args:
            item: The HTML content of the book page
            table_of_contents: The table of contents of the book

        Returns:
            list[Chapter]: List of chapters extracted from the page
        """
        chapters: list[Chapter] = []
        order = 0

        raw_content = item.get_content().decode("utf-8")
        soup = BeautifulSoup(raw_content, "html.parser")

        # Add book title if it exists
        title_tag = soup.find("p", class_="Title")
        if title_tag:
            title = title_tag.get_text(strip=True)
            if title.lower() != self.metadata.title.lower():
                logger.trace(
                    f"Book title found: {title}, "
                    f"not metadata title: {self.metadata.title}"
                )
                book_sentence = f"{title}. Audio generated by Epub2Audio."
                chapters.insert(
                    0, Chapter(title=title, content=book_sentence, order=-1, id="title")
                )

        # Find all chapter headings (they have class "Heading-2")
        chapter_heads = soup.find_all("p", class_="Heading-2")

        for head in chapter_heads:
            # Get the chapter title
            title = head.get_text(strip=True)
            # if not title.startswith("Chapter"):
            #     continue

            # Get the chapter content by finding all content between this heading
            # and the next heading (or end of document)
            content = []
            current = head.find_next_sibling()
            while current and not (
                current.name
                and current.name == "p"
                and "Heading-2" in current.get("class", [])
            ):
                if current.name and current.name == "p":
                    content.append(current.get_text(strip=True))
                current = current.find_next_sibling()

            # Join the content paragraphs
            chapter_content = " ".join(content)
            order += 1

            # Create the chapter
            chapter = Chapter(
                title=title,
                content=chapter_content,
                order=order,
                id=head.get("id", f"chapter_{order}"),
            )
            chapters.append(chapter)

        return chapters

    def _get_title_chapter(self) -> Chapter:
        """Get the title chapter."""
        return Chapter(
            title=self.metadata.title,
            content=self.metadata.book_sentence,
            order=-1,
            id="title",
        )

    def _get_chapters_from_pages(
        self,
        table_of_contents: list[epub.EpubItem],
    ) -> list[Chapter]:
        """Get the chapters from the pages of the EPUB file.

        Args:
            table_of_contents: The table of contents of the book
        """
        chapters: list[Chapter] = []
        order = 0

        for item in table_of_contents:
            # Skip non-chapter items (e.g., TOC, copyright pages)
            if not self._is_chapter(item):
                logger.trace(f"Skipping non-chapter item: {item}")
                continue

            # Extract title from content or use fallback
            raw_content = item.get_content().decode("utf-8")
            title = self._extract_chapter_title(raw_content) or f"Chapter {order + 1}"
            logger.trace(f"chapter title: {title}")

            # Skip likely table of contents
            if title == self.metadata.title:
                continue

            content = self._clean_text(raw_content)
            # remove the title from the content
            content = self._remove_title_from_content(content, title)

            # Skip empty chapters
            if not content:
                logger.trace(f"Skipping empty chapter: {title}")
                continue

            chapter = Chapter(title=title, content=content, order=order, id=item.id)
            chapters.append(chapter)
            order += 1

        return chapters

    def _extract_chapters(self) -> list[Chapter]:
        """Extract chapters from the EPUB file.

        Returns:
            List[Chapter]: List of extracted chapters
        """
        chapters = []

        # Get the table of contents
        table_of_contents = self.epub.toc
        if table_of_contents:
            logger.trace(f"raw toc: {table_of_contents}")
            table_of_contents = self._flatten_table_of_contents(table_of_contents)
        else:
            table_of_contents = self.epub.get_items_of_type(ebooklib.ITEM_DOCUMENT)
            logger.trace("no toc, using all documents")

        logger.trace(f"toc: {table_of_contents}")

        # If we detected chapters in one page, process the first document
        if self._chapters_in_one_page:
            chapters = self._get_chapters_in_one_page(
                table_of_contents[0], table_of_contents[1:]
            )
        else:
            chapters = self._get_chapters_from_pages(table_of_contents)

        if not chapters:
            raise ConversionError(
                "No valid chapters found in EPUB file", ErrorCodes.INVALID_EPUB
            )

        if self.metadata.title:
            chapters.insert(0, self._get_title_chapter())
            logger.trace(f"Book title added: {chapters[0]}")

        return sorted(chapters)

    def _is_cover(self, item: epub.EpubItem) -> bool:
        """Determine if an EPUB item is a cover.

        Args:
            item: EPUB item to check
        """
        return re.search(r"cover\.x?html$", item.file_name.lower()) is not None

    def _is_chapter(self, item: epub.EpubItem) -> bool:
        """Determine if an EPUB item is a chapter.

        Args:
            item: EPUB item to check

        Returns:
            bool: True if the item is a chapter
        """
        # Skip common non-chapter files
        skip_patterns = [r"toc\.x?html$", r"copyright\.x?html$", r"cover\.x?html$"]
        skip_id_patterns = [r"pg-(header|footer|toc)$"]

        for pattern in skip_patterns:
            if re.search(pattern, item.file_name.lower()):
                return False

        for pattern in skip_id_patterns:
            if re.search(pattern, item.id.lower()):
                return False

        return True

    @staticmethod
    def _extract_chapter_title(raw_content: str) -> Optional[str]:
        """Extract chapter title from an EPUB item.

        Args:
            raw_content: Raw string of html content to extract title from

        Returns:
            Optional[str]: Extracted title or None
        """
        soup = BeautifulSoup(raw_content, "html.parser")

        # Try to find title in common heading elements
        for heading in soup.find_all(["h1", "h2", "h3"]):
            title = heading.get_text(strip=True)
            if title:
                return title

        return None

    @staticmethod
    def _remove_title_from_content(content: str, title: str) -> str:
        """Remove the title from the content.

        Args:
            content: Content to remove title from
            title: Title to remove

        Returns:
            str: Content with title removed
        """
        title_split = title.split()
        content_split = content[: len(title)].split()
        rest_of_content = content[len(title) :]
        while title_split and content_split and title_split[0] == content_split[0]:
            logger.trace(f"removing title from content: {title_split[0]}")
            title_split.pop(0)
            content_split.pop(0)
        content_split.append(rest_of_content)
        content = " ".join(content_split)
        return content
