import math
import os
import uuid
from urllib.parse import urljoin

import isodate
import langcodes
import requests
import xmltodict
from netpyx import HttpClient


class MpdParser(HttpClient):
    """
    MpdParser is a utility class for parsing MPEG-DASH (MPD) manifests.
    It extracts audio and video track information including encryption keys (PSSH/KID),
    bitrate, codec, estimated size, and segment URLs.

    This class inherits from `HttpClient` for HTTP requests and `Helpers` for utilities.
    """
    CERT_PEM: str = os.path.join(os.getcwd(), "cert.pem")

    def __init__(self, mpd_url: str):
        super().__init__(silent=False)
        self.mpd_url = mpd_url
        self.set_user_agent(
            "Crunchyroll/4.77.2 (bundle_identifier:com.crunchyroll.iphone; build_number:4139672.438176041) iOS/18.5.0 Gravity/4.77.2"
        )

    def __enter__(self):
        """Enables use of `with MpdParser(...) as parser:` context syntax."""
        return self

    def parse(self, lang_code: str = None, access_token:str = None) -> dict:
        """
        Parses the MPD manifest and returns audio and video tracks with metadata.

        Args:
            lang_code (str, optional): A fallback language code if none is provided in the MPD.

        Returns:
            dict: A dictionary with "video" and "audio" keys containing track information.
            :param lang_code:
            :param access_token:
        """
        self.set_user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36")
        self.set_headers({
            "Referer": "https://static.crunchyroll.com/",
            "origin": "https://static.crunchyroll.com/",
            "Accept-Language": "es-419,es;q=0.9,es-ES;q=0.8,en;q=0.7",
        })
        response = self.get(self.mpd_url).text

        xml = xmltodict.parse(response)
        duration = isodate.parse_duration(xml["MPD"]["@mediaPresentationDuration"]).total_seconds()

        videos, audios = {}, {}
        for track in xml["MPD"]["Period"]["AdaptationSet"]:

            for rep in self.force_list(track.get("Representation", [])):
                protection = self.extract_protection(track)
                if not protection:
                    continue

                mime_type = rep.get("@mimeType")
                rep_id = rep["@id"]
                codec = rep["@codecs"]
                bitrate = self.convert_size(rep["@bandwidth"])
                size = self.get_size(duration * float(rep["@bandwidth"]) * 0.125)
                segments = self.get_segments(rep["BaseURL"], track["SegmentTemplate"], rep_id, duration)

                if mime_type is None:
                    if "video/" in rep_id:
                        mime_type = "video/mp4"
                    elif "audio/" in rep_id:
                        mime_type = "audio/mp4"

                if mime_type == "video/mp4":
                    videos[rep["@height"]] = {
                        "id": rep_id,
                        "pssh": protection["pssh"],
                        "kid": protection["kid"],
                        "codec": codec,
                        "bitrate": bitrate,
                        "size": size,
                        "segments": segments
                    }

                elif mime_type == "audio/mp4":
                    lang = rep.get("@lang") or self.extract_language_code(lang_code)[0]
                    label = rep.get("@label") or self.extract_language_code(lang_code)[1]
                    audios[label] = {
                        "lang": lang,
                        "id": rep_id,
                        "pssh": protection["pssh"],
                        "kid": protection["kid"],
                        "codec": codec,
                        "bitrate": bitrate,
                        "size": size,
                        "segments": segments
                    }

        return {"video": videos, "audio": audios}

    def force_list(self, item) -> list:
        """
        Ensures that a given item is returned as a list.

        Args:
            item: The item to check.

        Returns:
            list: The item wrapped in a list if it wasn't already.
        """
        return item if isinstance(item, list) else [item]

    def extract_protection(self, track) -> dict | None:
        """
        Extracts PSSH and KID from the ContentProtection tag.

        Args:
            track (dict): The AdaptationSet element from the MPD.

        Returns:
            dict or None: A dictionary with 'pssh' and 'kid', or None if not found.
        """
        protections = self.force_list(track.get("ContentProtection", []))
        for p in protections:
            scheme = p.get("@schemeIdUri", "").lower()
            if "edef8ba9-79d6-4ace-a3c8-27dcd51d21ed" in scheme:
                kid = (
                    p.get("cenc:default_KID", str(uuid.uuid4()))
                    .lower().replace("-", "")
                )
                return {
                    "pssh": p.get("cenc:pssh", ""),
                    "kid": kid
                }
        return None

    def get_size(self, size_bytes: float) -> str:
        """
        Converts a byte size to a human-readable format (e.g., MB, GB).

        Args:
            size_bytes (float): The size in bytes.

        Returns:
            str: Formatted size string.
        """
        units = ["", "KB", "MB", "GB", "TB"]
        i = 0
        while size_bytes >= 1024 and i < len(units) - 1:
            size_bytes /= 1024
            i += 1
        return f"{round(size_bytes, 2)} {units[i]}iB"

    def convert_size(self, size_bytes: str) -> int:
        """
        Converts bits per second to kilobits per second.

        Args:
            size_bytes (str): Bitrate in bits.

        Returns:
            int: Bitrate in kilobits.
        """
        return int(int(size_bytes) / 1000)

    def get_segments(self, base_url: str, template: dict, rep_id: str, duration: float) -> list:
        """
        Generates segment URLs from a SegmentTemplate.

        Args:
            base_url (str): The base URL for segments.
            template (dict): The SegmentTemplate object.
            rep_id (str): The representation ID.
            duration (float): The total media duration in seconds.

        Returns:
            list: List of full URLs for initialization and media segments.
        """
        timeline = template["SegmentTimeline"]["S"]
        if isinstance(timeline, list):
            segment_duration = int(timeline[0]["@d"]) / int(template["@timescale"])
        else:
            segment_duration = int(timeline["@d"]) / int(template["@timescale"])

        total_segments = math.ceil(duration / segment_duration)
        start_number = int(template["@startNumber"])

        urls = [
            urljoin(base_url, template["@initialization"].replace("$RepresentationID$", rep_id))
        ]
        for n in range(start_number, start_number + total_segments):
            if "$Number$" in template["@media"]:
                segment = template["@media"].replace("$Number$", str(n)).replace("$RepresentationID$", rep_id)
                urls.append(urljoin(base_url, segment))
            elif "$Number%05d$" in template["@media"]:
                segment = template["@media"].replace("$Number%05d$", str(n).zfill(5)).replace("$RepresentationID$", rep_id)
                urls.append(urljoin(base_url, segment))

        return urls

    def extract_language_code(self, language_code: str) -> tuple[str | None, str | None]:
        """
        Extracts the normalized language code and its name from a given identifier.
        Special handling for 'es-419' to return 'Latin Spanish'.

        :param language_code: The language code string.
        :return: Tuple of (code, name) or (None, None) if invalid.
        """
        try:
            if language_code is None:
                return None, None
            lang = langcodes.Language.get(language_code.split('-')[0])
            name = lang.language_name()
            code = lang.language if lang else language_code.split('-')[0]

            if language_code == "es-419":
                name = "Latin Spanish"
                code = "es-419"
            return code, name
        except Exception:
            return language_code.split('-')[0], None
