import inspect
import requests
from akademiya import constants
from dataclasses import dataclass
from urllib.parse import quote

from akademiya.internal import utils


@dataclass
class BookPdf:
    """
    The PDF file of the book.

    Only useful fields are covered, the rest are ignored.
    """

    FileTitle: str
    ID: int
    Position: int
    Type: str
    Url: str

    @classmethod
    def from_dict(self, data: dict) -> "Book":
        # Ty https://stackoverflow.com/a/55096964
        return self(
            **{k: v for k, v in data.items() if k in inspect.signature(self).parameters}
        )


@dataclass
class Book:
    """
    The product, or in our case the book.

    Only useful fields are covered, the rest are ignored.
    """

    Author: str | None
    CoverPicName: str
    ID: int
    Status: int
    # Category fields
    category_id: int
    category_name: str | None
    category_type: str | None
    collection_name: int
    creator_names: str | None
    # Product fields
    product_code: str | None
    product_cover: str
    product_description: str | None
    product_pdf: list[BookPdf] | None
    product_title: str
    publisher_name: str

    @classmethod
    def from_dict(self, data: dict) -> "Book":
        # Ty https://stackoverflow.com/a/55096964
        return_val = self(
            **{k: v for k, v in data.items() if k in inspect.signature(self).parameters}
        )
        if (data["product_pdf"]) is not None:
            return_val.product_pdf = [
                BookPdf.from_dict(pdf) for pdf in data["product_pdf"]
            ]
        return return_val


@dataclass
class User:
    full_name: str
    id: int
    avatar_url: str
    token: str
    username: str


class Client:
    """
    Client for interacting with the book library server.
    """

    def __init__(self):
        self._session = requests.Session()
        # Update User-Agent to mask as the official client (although it's not necessary)
        self._session.headers.update({"User-Agent": constants.USER_AGENT})
        self._app_version: float = None
        self.user: User = None
        self.unique_key = utils.get_unique_key()
        self.set_app_version()

    def borrow_book(self, book_id: int):
        rsp = self._session.get(
            f"{constants.ENDPOINT}/API/cbs/User/Payment/{self.user.token}/{book_id}/{self.unique_key}/1/1/"
        )
        rsp.raise_for_status()
        result = rsp.json()["BookBorrowsResult"]
        if result["Message"] == "Mượn sách thành công.":
            return
        raise Exception(f"Failed to borrow book: {result['Message']}")

    def get_book_info(self, book_id: int):
        rsp = self._session.get(
            f"{constants.ENDPOINT}/API/cbs/products/getinfo.json/{book_id}/1/"
        )
        rsp.raise_for_status()
        # GetProductByIDResult is the root
        result = rsp.json()["GetProductByIDResult"]
        if result["ValidateMessage"]["Message"] != "Lấy dữ liệu thành công":
            raise Exception(f"Lookup failed: {result['ValidateMessage']['Message']}")
        return Book.from_dict(result)

    def get_book_pdf(self, book: Book):
        """
        Downloads the PDF of the given book.

        Args:
            book (Book): The book to download the PDF from.

        Returns:
            bytes: The PDF data if successful, None otherwise.
        """
        if book.product_pdf is None:
            return None
        if not self.is_borrowing(book.ID):
            raise Exception("You must borrow the book before downloading the PDF.")
        pdf = book.product_pdf[0]
        rsp = self._session.get(
            f"{pdf.Url}{book.product_code}/{self.unique_key}/APPWINDOW.1.0/{self.user.token}/{self.user.username}/1/1/"
        )
        rsp.raise_for_status()
        # Comment so Pylance can stfu
        if not isinstance(rsp.content, bytes):
            raise Exception("Expected bytes, got something else.")
        try:
            data = rsp.json()
            raise Exception(f"Failed to get PDF: {data['ValidateMessage']['Message']}")
        except Exception:
            pass
        return rsp.content

    def is_borrowing(self, book_id: int) -> bool:
        """
        Checks if the user is borrowing a book.

        Args:
            book_id (int): The ID of the book to check.

        Returns:
            bool: True if the user is borrowing the book, False otherwise."""
        rsp = self._session.get(
            f"{constants.ENDPOINT}/API/cbs/User/Payment/Date/{self.user.username}/{book_id}/"
        )
        rsp.raise_for_status()
        result = rsp.json()["BorrowsDateResult"]
        if result["ValidateMessage"]["Message"] == "Sách chưa mượn hoặc đã trả!":
            return False
        return True

    def login(self, username: str, password: str):
        """
        Logs in the user with the given credentials.

        You must call this method before calling any other methods.

        Args:
            username (str): The username.
            password (str): The password.
        """
        # Why did they put the username & password in the URL? :)
        rsp = self._session.get(
            f"{constants.ENDPOINT}/Api/loginrs/{username}/{password}/WINDOWS/"
        )
        rsp.raise_for_status()
        data = rsp.json()
        result = data["GetPatronByAccountResult"]
        if result["ValidateMessage"]["Message"] != "Đăng nhập thành công!":
            raise Exception(f"Login failed: {result['ValidateMessage']['Message']}")
        self.user = User(
            full_name=result["FullName"],
            id=result["Id"],
            avatar_url=result["ImageUrl"],
            token=result["token"],
            username=username,
        )

    def lookup_books(self, name: str):
        """
        Looks up books by name.

        Although this method provides book information, if you need more detailed information for a
        specific book then you should use `get_book_info` along with the given book ID.

        Args:
            name (str): The name to search for.

        Returns:
            list[Product]: A list of products (books) that match the search query.
        """
        # What in the fuck is /0/0/1/20/1 :)
        rsp = self._session.get(
            f"{constants.ENDPOINT}/API/cbs/products/Search/{quote(name)}/0/0/1/20/1/{self.user.token}/"
        )
        rsp.raise_for_status()
        # SearchEngResult is the root
        result = rsp.json()["SearchEngResult"]
        if result["ValidateMessage"]["Message"] != "Tra cứu dữ liệu thành công":
            raise Exception(f"Lookup failed: {result['ValidateMessage']['Message']}")
        products: list[Book] = []
        for product_dict in result["ProductList"]:
            product = Book.from_dict(product_dict)
            products.append(product)
        return products

    def set_app_version(self):
        """
        Sets the app version to the latest version available on the server.

        This method is called automatically when the client is created.
        """
        rsp = self._session.get(
            f"{constants.ENDPOINT}/API/cbs/products/getversionapp/windows/"
        )
        rsp.raise_for_status()
        self._app_version = float(rsp.json()["Result"])
