import json
import os
import random
import re
from enum import Enum
from importlib.metadata import PackageNotFoundError, version
from pathlib import Path
from urllib.error import URLError
from urllib.request import Request, urlopen

import certifi
from niquests import Session
from packaging.version import parse as parse_version

from .env import initialize_app_env
from .logger import get_logger

VERSION = None

try:
    VERSION = version("aniworld")
except PackageNotFoundError:
    VERSION = None

CA_CERT_BUNDLE = certifi.where()
os.environ.setdefault("SSL_CERT_FILE", CA_CERT_BUNDLE)
os.environ.setdefault("REQUESTS_CA_BUNDLE", CA_CERT_BUNDLE)
os.environ.setdefault("CURL_CA_BUNDLE", CA_CERT_BUNDLE)


def get_latest_version():
    """Fetch the newest available version from PyPI."""
    try:
        logger.debug("Checking latest version on PyPI...")
        request = Request(
            "https://pypi.org/pypi/aniworld/json",
            headers={"User-Agent": DEFAULT_USER_AGENT},
        )
        with urlopen(request, timeout=5) as response:
            payload = json.load(response)
        latest_version = payload["info"]["version"]
        logger.debug(f"Latest PyPI version is {latest_version}")
        return latest_version
    except (URLError, TimeoutError, OSError, ValueError, KeyError) as exc:
        logger.debug(f"Could not fetch latest version from PyPI: {exc}")
        return None


def is_newest_version() -> bool:
    """Checks if the installed version is the newest available on PyPI."""
    if not VERSION:
        return False

    latest_version = get_latest_version()
    if not latest_version:
        return False

    return parse_version(VERSION) >= parse_version(latest_version)


# Resolve the app directory and load its .env file whenever config is imported.
ANIWORLD_CONFIG_DIR = initialize_app_env(
    Path(__file__).resolve().parent / ".env.example",
    Path.home() / ".aniworld",
)

logger = get_logger(__name__)

NAMING_TEMPLATE = os.getenv(
    "ANIWORLD_NAMING_TEMPLATE",
    "{title} ({year}) [imdbid-{imdbid}]/Season {season}/{title} S{season}E{episode}.mkv",
)

# Video codec configuration
VIDEO_CODEC = os.getenv("ANIWORLD_VIDEO_CODEC", "copy")

# Simple codec mapping using ffmpeg defaults
VIDEO_CODEC_MAP = {
    "copy": "copy",
    "h264": "libx264",
    "h265": "libx265",
    "av1": "libsvtav1",
}

ACTION_METHODS = {
    "Download": "download",
    "Watch": "watch",
    "Syncplay": "syncplay",
}


def get_video_codec():
    """Get and validate video codec from environment variable."""
    codec = VIDEO_CODEC
    if codec not in VIDEO_CODEC_MAP:
        logger.warning(
            f"Invalid video codec '{codec}', falling back to 'copy'. Valid options: {list(VIDEO_CODEC_MAP.keys())}"
        )
        return "copy"
    return VIDEO_CODEC_MAP[codec]


# NIQUESTS


def _get_random_user_agent() -> str:
    fallback = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
    jsonl_path = Path(__file__).parent / "browsers.jsonl"

    if not jsonl_path.exists():
        return fallback

    valid_agents = []
    try:
        # Stream the file to avoid loading all 10,000 lines into memory
        with open(jsonl_path, "r", encoding="utf-8") as f:
            for line in f:
                # Fast string pre-filter before expensive JSON parsing
                if '"Windows"' in line or '"Mac OS X"' in line:
                    data = json.loads(line)
                    if data.get("os") in ("Windows", "Mac OS X"):
                        ua = data.get("useragent")
                        if ua:
                            valid_agents.append(ua)

        if valid_agents:
            return random.choice(valid_agents)
    except Exception:
        pass

    return fallback


DEFAULT_USER_AGENT = _get_random_user_agent()

LULUVDO_USER_AGENT = (
    "Mozilla/5.0 (Android 15; Mobile; rv:132.0) Gecko/132.0 Firefox/132.0"
)

# TODO:
# This is so fucking annoying because using GLOBAL_SESSION anywhere in the
# codebase ends up importing basically every module, so even a simple fetch
# takes 20–30 seconds just to start...
#
# I already made a lazy-loading wrapper that defers all the __init__.py imports
# in another branch, but for now I just have to sit through the import time
# every run, even though the actual fetch only takes about a second
GLOBAL_SESSION = Session(
    resolver=["doh+cloudflare://"],
    disable_http3=True,
    multiplexed=False,
    headers={
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Sec-Fetch-Site": "none",
        "Sec-Fetch-Dest": "document",
        "Accept-Language": "en-US,en;q=0.9",
        "Sec-Fetch-Mode": "navigate",
        "User-Agent": DEFAULT_USER_AGENT,
        "Accept-Encoding": "gzip, deflate, br",
        "Referer": "https://aniworld.to/search",
        "Priority": "u=0, i",
    },
)
GLOBAL_SESSION.verify = CA_CERT_BUNDLE

logger.debug("Config initialized successfully")
logger.debug(
    "If this shit seems to hang, just wait. It's currently loading all modules, which can take up to ~30 seconds, but only once per startup. This is being worked on and won't take nearly as long in the future."
)

# -----------------------------
# Provider Stuff
# -----------------------------
SUPPORTED_PROVIDERS = (
    "VOE",
    "MegaKino",
    "Vidmoly",
    "Vidoza",
    "Doodstream",
    "Filemoon",
    # "LoadX",
    # "Luluvdo",
    # "Streamtape",
)


def parse_provider_order(value, allowed_providers=None):
    allowed = tuple(dict.fromkeys(allowed_providers or SUPPORTED_PROVIDERS))
    if not allowed:
        return ()

    if not value:
        return allowed

    ordered = []
    seen = set()

    if isinstance(value, (list, tuple, set)):
        raw_values = value
    else:
        raw_values = str(value).split(",")

    for raw_provider in raw_values:
        provider = raw_provider.strip()
        if provider and provider in allowed and provider not in seen:
            ordered.append(provider)
            seen.add(provider)

    for provider in allowed:
        if provider not in seen:
            ordered.append(provider)

    return tuple(ordered)


def get_provider_fallback_order(allowed_providers=None):
    return parse_provider_order(
        os.getenv("ANIWORLD_PROVIDER_FALLBACK_ORDER", ""),
        allowed_providers=allowed_providers,
    )


def build_provider_attempt_order(
    selected_provider, available_providers, fallback_order=None
):
    available = tuple(
        dict.fromkeys(
            str(provider).strip()
            for provider in available_providers
            if str(provider).strip()
        )
    )
    if not available:
        return (selected_provider,) if selected_provider else ()

    ordered = []
    seen = set()

    if selected_provider and selected_provider in available:
        ordered.append(selected_provider)
        seen.add(selected_provider)

    if fallback_order is None:
        fallback_order = get_provider_fallback_order(allowed_providers=available)

    for provider in parse_provider_order(fallback_order, allowed_providers=available):
        if provider not in seen:
            ordered.append(provider)
            seen.add(provider)

    return tuple(ordered)


PROVIDER_HEADERS_D = {
    "Vidmoly": {"Referer": "https://vidmoly.biz"},
    # Doodstream signs the direct link against the requesting client, so the
    # download must reuse the same User-Agent the extractor sent.
    "Doodstream": {
        "User-Agent": DEFAULT_USER_AGENT,
        "Referer": "https://dood.li/",
    },
    "VOE": {
        "User-Agent": DEFAULT_USER_AGENT,
        "Accept": "*/*",
        "Accept-Language": "en-US,en;q=0.5",
        "Accept-Encoding": "gzip, deflate",
        "Connection": "keep-alive",
        "Referer": "https://voe.sx/",
        "Origin": "https://voe.sx",
    },
    "LoadX": {"Accept": "*/*"},
    "Cineby": {"Referer": "https://www.cineby.at/"},
    "Filemoon": {"User-Agent": DEFAULT_USER_AGENT, "Referer": "https://filemoon.to"},
    "Luluvdo": {
        "User-Agent": LULUVDO_USER_AGENT,
        "Accept-Language": "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
        "Origin": "https://luluvdo.com",
        "Referer": "https://luluvdo.com/",
    },
}

PROVIDER_HEADERS_W = {
    "Vidmoly": {"Referer": "https://vidmoly.biz"},
    # Doodstream signs the direct link against the requesting client, so the
    # download must reuse the same User-Agent the extractor sent.
    "Doodstream": {
        "User-Agent": DEFAULT_USER_AGENT,
        "Referer": "https://dood.li/",
    },
    "VOE": {
        "User-Agent": DEFAULT_USER_AGENT,
        "Accept": "*/*",
        "Accept-Language": "en-US,en;q=0.5",
        "Accept-Encoding": "gzip, deflate",
        "Connection": "keep-alive",
        "Referer": "https://voe.sx/",
        "Origin": "https://voe.sx",
    },
    "LoadX": {"Accept": "*/*"},
    "Cineby": {"Referer": "https://www.cineby.at/"},
    "Filemoon": {"User-Agent": DEFAULT_USER_AGENT, "Referer": "https://filemoon.to"},
    "Luluvdo": {
        "User-Agent": LULUVDO_USER_AGENT,
        "Accept-Language": "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
        "Origin": "https://luluvdo.com",
        "Referer": "https://luluvdo.com/",
    },
}


# -----------------------------
# Language Stuff
# -----------------------------
class Audio(Enum):
    """
    Available audio language options:

        - JAPANESE: Japanese dubbed audio
        - GERMAN:   German dubbed audio
        - ENGLISH:  English dubbed audio

    Required source for each option:

        Japanese Dub -> Source: German Sub, English Sub
        German Dub   -> Source: German Dub
        English Dub  -> Source: English Dub
    """

    JAPANESE = "Japanese"
    GERMAN = "German"
    ENGLISH = "English"


class Subtitles(Enum):
    """
    Available subtitle language options:

        - NONE:    No subtitles
        - GERMAN:  German subtitles
        - ENGLISH: English subtitles

    Required source for each option:

        German Sub   -> Source: German Sub
        English Sub  -> Source: English Sub
    """

    NONE = "None"
    GERMAN = "German"
    ENGLISH = "English"


# Map site-specific language keys to semantic meaning
LANG_KEY_MAP = {
    "1": (Audio.GERMAN, Subtitles.NONE),  # German Dub
    "2": (Audio.JAPANESE, Subtitles.ENGLISH),  # English Sub
    "3": (Audio.JAPANESE, Subtitles.GERMAN),  # German Sub
    "4": (Audio.ENGLISH, Subtitles.NONE),  # English Dub
}

LANG_LABELS = {
    "1": "German Dub",
    "2": "English Sub",
    "3": "German Sub",
    "4": "English Dub",
}

LANG_CODE_MAP = {
    Audio.ENGLISH: "eng",
    Audio.GERMAN: "deu",
    Audio.JAPANESE: "jpn",
    Subtitles.ENGLISH: "eng",
    Subtitles.GERMAN: "deu",
    Subtitles.NONE: None,
}


INVERSE_LANG_KEY_MAP = {v: k for k, v in LANG_KEY_MAP.items()}
INVERSE_LANG_LABELS = {v: k for k, v in LANG_LABELS.items()}

# -----------------------------
# Patterns
# -----------------------------


ANIWORLD_SERIES_PATTERN = re.compile(
    r"^https?://(www\.)?aniworld\.to/anime/stream/[a-zA-Z0-9\-]+/?$", re.IGNORECASE
)

# series slug + (/staffel-N or /filme)
ANIWORLD_SEASON_PATTERN = re.compile(
    r"^https?://(www\.)?aniworld\.to/anime/stream/"
    r"[a-zA-Z0-9\-]+/"
    r"(staffel-\d+|filme)"
    r"/?$",
    re.IGNORECASE,
)

ANIWORLD_EPISODE_PATTERN = re.compile(
    r"^https?://(www\.)?aniworld\.to/anime/stream/"
    r"[a-zA-Z0-9\-]+/"  # series slug
    r"(staffel-\d+/episode-\d+|"  # season/episode
    r"filme/film-\d+)"  # movie/film
    r"/?$",
    re.IGNORECASE,
)

HANIME_TV_SERIES_PATTERN = re.compile(
    r"^https?://(?:www\.)?hanime\.tv/"
    r"(?:videos/hentai|hentai/video|playlists/[0-9a-z]+/video)/"
    r"[A-Za-z0-9\-]+/?$",
    re.IGNORECASE,
)

# serienstream.to went down at times; serienstream.cx and 186.2.175.5 are mirrors.

# Reachable hosts, in preference order. The IP is a last resort and needs a Host
# header (see models/s_to/http.py) because it serves the same site.
STO_DOMAINS = ["serienstream.to", "serienstream.cx"]
STO_IP = "186.2.175.5"

# Retired hosts that are still recognised and rewritten onto an active host, but
# never requested themselves. Keeps old links and bookmarks working.
STO_LEGACY_DOMAINS = ["s.to"]

STO_ALL_HOSTS = [*STO_DOMAINS, STO_IP, *STO_LEGACY_DOMAINS]

_STO_HOST = r"(?:www\.)?(?:" + "|".join(re.escape(h) for h in STO_ALL_HOSTS) + r")"

STO_HOST_RE = re.compile(r"^(https?://)" + _STO_HOST + r"(?=[:/?#]|$)", re.IGNORECASE)


def is_sto_host(url):
    """True if the URL points at any known serienstream host."""
    return bool(STO_HOST_RE.match(str(url or "")))


SERIENSTREAM_SERIES_PATTERN = re.compile(
    rf"^https?://{_STO_HOST}/serie/[a-zA-Z0-9\-]+/?$", re.IGNORECASE
)

SERIENSTREAM_SEASON_PATTERN = re.compile(
    rf"^https?://{_STO_HOST}/serie/"
    r"[a-zA-Z0-9\-]+/"
    r"staffel-\d+"
    r"/?$",
    re.IGNORECASE,
)

SERIENSTREAM_EPISODE_PATTERN = re.compile(
    rf"^https?://{_STO_HOST}/serie/"
    r"[a-zA-Z0-9\-]+/"
    r"staffel-\d+/episode-\d+"
    r"/?$",
    re.IGNORECASE,
)

MEGAKINO_SERIES_PATTERN = re.compile(
    r"^https?://(?:www\.)?megakino[\w-]*\.[^/]+/(?:action|films|serials)/[^?#]+(?:\.html)?/?(?:#mkep=\d+)?$",
    re.IGNORECASE,
)

MANGA_FIRE_SERIES_PATTERN = re.compile(
    r"^https?://(?:www\.)?mangafire\.to/title/[a-zA-Z0-9]+(?:-[a-zA-Z0-9\-]+)?/?$",
    re.IGNORECASE,
)

MANGA_FIRE_CHAPTER_PATTERN = re.compile(
    r"^https?://(?:www\.)?mangafire\.to/title/[a-zA-Z0-9]+(?:-[a-zA-Z0-9\-]+)?/chapter/[0-9]+(?:\.[0-9]+)?/?$",
    re.IGNORECASE,
)

FILMPALAST_SERIES_PATTERN = re.compile(
    r"^https?://(?:www\.)?filmpalast\.[^/]+/stream/[^/?#]+/?$",
    re.IGNORECASE,
)

# The trailing (?:\?[^#]*)? lets per-episode URLs (…?s=1&e=2) resolve too.
KINOX_SERIES_PATTERN = re.compile(
    r"^https?://(?:www\.)?kinox[\w.-]*\.[^/]+/Stream/[^/?#]+?(?:\.html)?(?:\?[^#]*)?/?$",
    re.IGNORECASE,
)

_CINEBY_HOST = r"(?:www\.)?cineby\.(?:at|app|[a-z]{2,4})"

# /movie/<id> or /tv/<id> (optionally ?s=N for a specific TV season)
CINEBY_SERIES_PATTERN = re.compile(
    rf"^https?://{_CINEBY_HOST}/(?:movie|tv)/\d+(?:\?[^#]*)?/?$",
    re.IGNORECASE,
)

# /movie/<id> or /tv/<id>/<season>/<episode>
CINEBY_EPISODE_PATTERN = re.compile(
    rf"^https?://{_CINEBY_HOST}/(?:movie/\d+|tv/\d+/\d+/\d+)(?:\?[^#]*)?/?$",
    re.IGNORECASE,
)

_BS_HOST = r"(?:www\.)?(?:burning-series\.(?:io|net)|burningseries\.(?:ac|cx)|bs\.cine\.to|bs\.to)"

BURNINGSERIES_SERIES_PATTERN = re.compile(
    rf"^https?://{_BS_HOST}/serie/[a-zA-Z0-9\-]+(?:\?[^#]*)?/?$",
    re.IGNORECASE,
)

# /serie/<slug>/<season>[/<lang>]
BURNINGSERIES_SEASON_PATTERN = re.compile(
    rf"^https?://{_BS_HOST}/serie/[a-zA-Z0-9\-]+/\d+(?:/[a-z]{{2}})?/?$",
    re.IGNORECASE,
)

# /serie/<slug>/<season>/<episode-slug>/<lang>
BURNINGSERIES_EPISODE_PATTERN = re.compile(
    rf"^https?://{_BS_HOST}/serie/[a-zA-Z0-9\-]+/\d+/[^/]+/[a-z]{{2}}/?$",
    re.IGNORECASE,
)

# -----------------------------
# Directories
# -----------------------------

# TODO: add many other directories and use them throughout the app

# Determine mpv scripts directory
# On Linux/macOS: ~/.config/mpv/scripts
# On Windows: %APPDATA%\mpv\scripts
if os.name == "nt":
    MPV_CONFIG_DIR = Path(os.getenv("APPDATA")) / "mpv"
else:
    MPV_CONFIG_DIR = Path.home() / ".config" / "mpv"

MPV_SCRIPTS_DIR = MPV_CONFIG_DIR / "scripts"
