from __future__ import annotations

import hashlib
import json
import re
from collections.abc import Generator
from urllib.parse import urljoin, urlparse
from typing import Any

import click
from click import Context
from requests import Request
from envied.core.manifests import HLS
from envied.core.search_result import SearchResult
from envied.core.service import Service
from envied.core.titles import Episode, Movie, Movies, Series
from envied.core.tracks import Chapters, Subtitle, Tracks
from envied.core.utils.xml import load_xml


class SBS(Service):
    """
    \b
    Service code for SBS ondemand streaming service (https://www.sbs.com.au/ondemand/).

    \b
    Version: 1.0.1
    Author: stabbedbybrick
    Authorization: None
    Geofence: AU (API and downloads)
    Robustness:
      AES: 720p, AAC2.0

    \b
    Tips:
        - Input should be complete URL:
          SERIES: https://www.sbs.com.au/ondemand/tv-series/reckless
          EPISODE: https://www.sbs.com.au/ondemand/tv-series/reckless/season-1/reckless-s1-ep1/2459384899653
          MOVIE: https://www.sbs.com.au/ondemand/movie/silence/1363535939614
          SPORT: https://www.sbs.com.au/ondemand/sports-series/australian-championship-2025/football-australian-championship-2025/australian-championship-2025-s2025-ep40/2457638979614

    \b
    Notes:
        - SBS uses transport streams for HLS, meaning the video and audio are a part of the same stream.
          As a result only videos are listed as tracks, but the audio will be included as well.

    """

    GEOFENCE = ("au",)

    @staticmethod
    @click.command(name="SBS", short_help="https://www.sbs.com.au/ondemand/", help=__doc__)
    @click.argument("title", type=str)
    @click.pass_context
    def cli(ctx: Context, **kwargs: Any) -> SBS:
        return SBS(ctx, **kwargs)

    def __init__(self, ctx: Context, title: str):
        self.title = title
        super().__init__(ctx)

        self.session.headers.update(self.config["headers"])

    def search(self) -> Generator[SearchResult, None, None]:
        params = {
            "q": self.title.strip(),
        }

        results = self._request("GET", "https://content-search.pr.sbsod.com/catalogue", params=params)["items"]

        for result in results:
            if result.get("entityType") in ("PAGE"):
                continue

            label = result.get("entityType")
            slug = result.get("slug")
            title = result.get("title")
            description = result.get("description")
            yield SearchResult(
                id_=f"https://www.sbs.com.au/ondemand/{label}/{slug}",
                title=title,
                description=description,
                label=label,
                url=f"https://www.sbs.com.au/ondemand/{label}/{slug}",
            )


    def get_titles(self) -> Movies | Series:
        regex = re.compile(
            r"^https://www.sbs.com.au/ondemand/"
            r"(?P<entity>tv-series|tv-program|sports-series|movie|watch)"
            r"(?:/|/.*/)"
            r"(?P<id>[^/]+)/?$"
        )

        match = regex.search(self.title)
        if not match:
            raise ValueError(f"Invalid URL input: {self.title}")

        entity_type, entity_id = (match.group(i) for i in ("entity", "id"))

        if entity_type in ("movie", "tv-program") and entity_id.isdigit():
            movie = self._movie(entity_id)
            return Movies(movie)
        
        elif entity_id.isdigit():
            episode = self._episode(entity_id)
            return Series(episode)

        elif entity_type in ("tv-series", "sports-series"):
            episodes = self._series(urlparse(self.title).path)
            return Series(episodes)

    def get_tracks(self, title: Movie | Episode) -> Tracks:
        smil = self._request("GET", f"/api/v3/video_smil?id={title.id}")

        body = load_xml(smil).find("body").find("seq")
        section = body.find("par") or body

        manifest = next((x.get("src") for x in section.findall("video")), None)
        subtitles = [(x.get("src"), x.get("lang"), x.get("type")) for x in section.findall("textstream")]
        
        tracks = HLS.from_url(manifest, self.session).to_tracks(title.language)

        if subtitles:
            for url, lang, type in subtitles:
                if "ttaf+xml" in type:
                    continue
                codec = type.split("/")[-1]
                tracks.add(
                    Subtitle(
                        id_=hashlib.md5(url.encode()).hexdigest()[0:6],
                        url=url,
                        codec=Subtitle.Codec.from_mime(codec),
                        language=lang,
                        sdh="_CC" in url,
                    )
                )

        return tracks

    def get_chapters(self, title: Movie | Episode) -> Chapters:
        return Chapters()

    # Service specific

    def _series(self, path: str) -> Episode:
        if "ondemand" in path:
            path = path.split("ondemand")[1]

        metadata = self._request("GET", f"https://catalogue.pr.sbsod.com{path}")

        seasons = metadata.get("seasons")
        if not seasons:
            raise ValueError(f"Failed to find seasons for title: {path}")
        
        episodes = []
        for season in seasons:
            for episode in season.get("episodes"):
                episodes.append(
                    Episode(
                        id_=episode.get("mpxMediaID"),
                        service=self.__class__,
                        title=episode.get("seriesTitle"),
                        season=int(episode.get("seasonNumber", 0)),
                        number=int(episode.get("episodeNumber", 0)),
                        name=episode.get("title"),
                        year=episode.get("releaseYear"),
                        language=metadata.get("localeID") or "en",
                        data=episode,
                    )
                )

        return episodes

    def _movie(self, entity_id: str) -> Movie:
        metadata = self._request("GET", f"https://catalogue.pr.sbsod.com/mpx-media/{entity_id}")

        return [
            Movie(
                id_=metadata.get("mpxMediaID"),
                service=self.__class__,
                name=metadata.get("title") or metadata.get("cdpTitle"),
                year=metadata.get("releaseYear"),
                language=metadata.get("localeID") or "en",
                data=metadata,
            )
        ]

    def _episode(self, entity_id: str) -> Episode:
        metadata = self._request("GET", f"https://catalogue.pr.sbsod.com/mpx-media/{entity_id}")

        return [
            Episode(
                id_=metadata.get("mpxMediaID"),
                service=self.__class__,
                title=metadata.get("seriesTitle"),
                season=int(metadata.get("seasonNumber", 0)),
                number=int(metadata.get("episodeNumber", 0)),
                name=metadata.get("title") or metadata.get("cdpTitle"),
                year=metadata.get("releaseYear"),
                language=metadata.get("localeID") or "en",
                data=metadata,
            )
        ]

    def _request(self, method: str, endpoint: str, **kwargs: Any) -> Any[dict | str]:
        url = urljoin(self.config["endpoints"]["base_url"], endpoint)

        prep = self.session.prepare_request(Request(method, url, **kwargs))
        response = self.session.send(prep)

        if response.status_code != 200:
            raise ConnectionError(f"{response.text}")

        try:
            return json.loads(response.content)

        except json.JSONDecodeError:
            return response.text

        except ValueError as e:
            raise ValueError(f"Failed to parse JSON: {response.text}") from e

