import os
import re
import uuid
from dataclasses import dataclass
from datetime import datetime, timedelta
from functools import wraps

import langcodes
from logita import Logita
from netpyx import HttpClient
from savekit import SaveKit


@dataclass
class Bootstrap(HttpClient, Logita, SaveKit):
    """
    Base utility class for Crunchyroll-related operations.
    Inherits from:
    - HttpClient: custom HTTP client.
    - Logita: logging utility with colored output.
    - SaveKit: settings handler with JSON persistence.

    Provides preconfigured URLs, request headers, language handling,
    and media title formatting tools.
    """

    API_URL: str = "https://beta-api.crunchyroll.com"
    WEB_URL: str = "https://www.crunchyroll.com"
    PLAY_URL: str = "https://cr-play-service.prd.crunchyrollsvc.com"
    LICENSE_URL: str = "https://cr-license-proxy.prd.crunchyrollsvc.com"

    CERT_PEM: str = os.path.join(os.getcwd(), "cert.pem")

    def __init__(self):
        super().__init__(silent=False)
        Logita.__init__(self)
        SaveKit.__init__(self, filename="settings.json")
        self.temp_dir = os.path.join(os.path.abspath(os.sep), "temp_downloads")
        self.set_user_agent(self.user_agent)

    # ─── Properties ────────────────────────────────────────────────────────────

    @property
    def access_token(self) -> str:
        return self.get_item("access_token")

    @property
    def client_id(self) -> str:
        return self.get_item("client_id")

    @property
    def client_secret(self) -> str:
        return self.get_item("client_secret")

    @property
    def default_language(self) -> str:
        return self.get_item("default_language")

    @property
    def email(self) -> str:
        return self.get_item("email", None)

    @property
    def languages(self) -> list[str]:
        return (self.get_item("languages") or []) + [self.get_item("default_language")]

    @property
    def parameters(self) -> dict:
        return {
            "scope": "offline_access",
            "device_id": self.gen_uuid(),
            "device_name": self.get_item("device_name"),
            "device_type": self.get_item("device_type"),
            "locale": self.default_language,
            "preferred_audio_language": self.default_language,
        }

    @property
    def password(self) -> str:
        return self.get_item("password", None)

    @property
    def refresh_token(self) -> str:
        return self.get_item("refresh_token")

    @property
    def user_agent(self) -> str:
        return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"

    # ─── Methods ───────────────────────────────────────────────────────────────

    def clean_name(self, input_string: str) -> str:
        """
        Cleans a string by removing special characters and replacing accented letters.
        """
        name = input_string.translate(str.maketrans('', '', '!?¡¿(),:#\'"~./\\<>'))
        name = (name.replace('ñ', 'n')
                     .replace('á', 'a')
                     .replace('é', 'e')
                     .replace('í', 'i')
                     .replace('ó', 'o')
                     .replace('ú', 'u')
                     .replace('-', ' ')
                     .replace("*",""). replace('  ', ' '))
        return name.strip()

    def compose_episode_title(self, episode: dict) -> str:
        """
        Generates a formatted title for an episode.
        """
        season_title = episode.get('season_title')

        s = episode.get("season_display_number")
        if s == "" or s is None:
            s = episode.get('season_number')
        s = self.format_num(s)

        e = episode.get('episode')
        if e == "" or e is None:
            e = episode.get('sequence_number')
        try:
            e = self.format_num(e)
        except Exception as error:
            e = episode.get('sequence_number')
            e = self.format_num(e)

        name = self.clean_name(episode.get('title'))
        title = f"{self.clean_name(episode.get('series_title'))} - S{s}E{e} - {name}"

        if self.get_item("prefix") not in ("", None):
            title = f"{self.get_item('prefix')} {title}"

        title = os.path.join(
            self.clean_name(episode.get('series_title')) + "[CR][WEB-DL]",
            f"Season {s}",
            f"{title}"
        )

        if "OVA" in season_title:
            title = title.replace(f"Season {s}", f"Season {s} (OVAS)")

        return title

    def compose_movie_title(self, movie: dict, guid: str) -> str:
        """
        Generates a formatted title for a movie.
        """
        title = f"{self.clean_name(movie.get('title'))} [CR][WEB-DL][{guid}]"
        return title

    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

    def extract_type_and_id(self, url: str) -> str:
        """
        Extracts Crunchyroll ID from a given URL.
        """
        pattern = re.compile(
            r"^https?://(?:www\.)?crunchyroll\.com/"
            r"(?:series|watch)/"
            r"(?P<id>[A-Z0-9]+)/?"
        )
        match = pattern.match(url)
        if not match:
            self.error("Invalid Crunchyroll URL.")
            exit()
        return match.group("id")

    def format_num(self, number: str) -> str:
        """
        Formats a number string to always have at least 2 digits.
        Includes decimal part only if non-zero.
        """
        num_float = float(number)
        integer_part = int(num_float)
        formatted = f"0{integer_part}" if integer_part < 10 else str(integer_part)

        if num_float != integer_part:
            decimal_part = str(num_float).partition('.')[2]
            return f"{formatted}.{decimal_part}"

        return formatted

    def gen_uuid(self) -> str:
        """
        Generates a random UUID in uppercase.
        """
        return str(uuid.uuid4()).upper()

    def get_headers_widevine(self, guid: str, token: str) -> dict:
        """
        Generates headers for Widevine license request.
        """
        return {
            'authority': 'cr-license-proxy.prd.crunchyrollsvc.com',
            'accept': '*/*',
            'content-type': 'application/octet-stream',
            'origin': 'https://static.crunchyroll.com',
            'referer': 'https://static.crunchyroll.com/',
            'x-cr-content-id': guid,
            'x-cr-video-token': token,
            "Authorization": f"Bearer {self.access_token}",
            "Accept-Encoding": "gzip;q=1.0, compress;q=0.5",
            "Accept-Language": "es-419,es;q=0.9,es-ES;q=0.8,en;q=0.7",
            "sec-ch-ua-platform": '"Windows"'
        }

    def get_expiry(self, seconds: int) -> int:
        """
        Calculates a future Unix timestamp based on the current time plus a given number of seconds.

        This is typically used to define expiration times (e.g., for tokens, cache entries, or temporary files).

        Args:
            seconds (int): The number of seconds to add to the current time.

        Returns:
            int: The future time as a Unix timestamp (in seconds).
        """
        return int((datetime.now() + timedelta(seconds=seconds)).timestamp())

    def show_exception(self, e: Exception) -> str:
        """
        Formats and returns exception information, including server error responses if present.
        """
        if hasattr(e, 'response') and e.response is not None:
            try:
                error_json = e.response.json()
                message = error_json.get("code", error_json.get("error", "Unknown error"))
                return f"Request failed. Server response: {message}"
            except ValueError:
                return f"Request failed. Server returned non-JSON response: {e.response.text}"
        return f"Request failed. Exception: {e}"

    def shorten_name(self, name: str, limit: int = 255, max_body: int = 190) -> str:
        """
        Shortens a file name (without extension) if it exceeds the length limit.

        :param name: The original file name (no extension)
        :param limit: Maximum allowed characters (default: 255)
        :param max_body: Maximum characters before adding '...'
        :return: A safe file name
        """
        if len(name) <= limit:
            return name
        return name[:max_body].rstrip() + "..."
