"""
$description Live local TV channels and video on-demand service. OTT service from FilmOn.
$url filmon.com
$type live, vod
$notes Some VODs are mp4 which may not stream, use -o to download
"""

from __future__ import annotations

import re
import time
from typing import TYPE_CHECKING
from urllib.parse import urlparse, urlunparse

from streamlink.exceptions import PluginError, StreamError
from streamlink.logger import getLogger
from streamlink.plugin import Plugin, pluginmatcher
from streamlink.plugin.api import validate
from streamlink.session.http import TLSSecLevel1Adapter
from streamlink.stream.hls import HLSStream, HLSStreamReader, HLSStreamWorker
from streamlink.stream.http import HTTPStream


if TYPE_CHECKING:
    from collections.abc import Iterator


log = getLogger(__name__)


class FilmOnHLSStreamWorker(HLSStreamWorker):
    stream: FilmOnHLS

    def _fetch_playlist(self):
        try:
            return super()._fetch_playlist()
        except StreamError as err:
            if hasattr(err, "err") and err.err.response.status_code in (403, 502):  # type: ignore
                self.stream.watch_timeout = 0
                self.playlist_reload_time = 0
                log.debug(f"Force-reloading the channel playlist on error: {err}")
            raise err


class FilmOnHLSStreamReader(HLSStreamReader):
    __worker__ = FilmOnHLSStreamWorker


class FilmOnHLS(HLSStream):
    __shortname__ = "hls-filmon"
    __reader__ = FilmOnHLSStreamReader

    def __init__(self, session, url: str, api: FilmOnAPI, channel=None, vod_id=None, quality="high", **args):
        if channel is None and vod_id is None:
            raise PluginError("Channel or vod_id must be set")

        super().__init__(session, url, **args)
        self.api = api
        self.channel = channel
        self.vod_id = vod_id
        self.quality = quality
        self._url = url
        self.watch_timeout = 0.0
        self._first_netloc = ""

    def _get_stream_data(self) -> Iterator[tuple[str, str, int]]:
        if self.channel:
            log.debug(f"Reloading FilmOn channel playlist: {self.channel}")
            yield from self.api.channel(self.channel)
        elif self.vod_id:
            log.debug(f"Reloading FilmOn VOD playlist: {self.vod_id}")
            yield from self.api.vod(self.vod_id)

    @property
    def url(self) -> str:
        if time.time() <= self.watch_timeout:
            return self._url

        # If the watch timeout has passed then refresh the playlist from the API
        for quality, url, timeout in self._get_stream_data():
            if quality == self.quality:
                self.watch_timeout = time.time() + timeout

                if not self.channel:
                    self._url = url
                else:
                    parsed = urlparse(url)
                    if not self._first_netloc:
                        # save the first netloc
                        self._first_netloc = parsed.netloc
                    # always use the first saved netloc
                    self._url = parsed._replace(netloc=self._first_netloc).geturl()

                return self._url

        raise TypeError("Stream has expired and cannot be translated to a URL")


class FilmOnAPI:
    channel_url = "https://www.filmon.com/ajax/getChannelInfo"
    vod_url = "https://vms-admin.filmon.com/api/video/movie?id={0}"

    ATTEMPTS = 5
    TIMEOUT = 0.75

    stream_schema = validate.all(
        {
            "quality": str,
            "url": validate.url(),
            "watch-timeout": int,
        },
        validate.union_get("quality", "url", "watch-timeout"),
    )

    def __init__(self, session):
        self.session = session

    def channel(self, channel) -> list[tuple[str, str, int]]:
        num = 1
        while True:
            # retry for 50X errors or validation errors at the same time
            try:
                return self.session.http.post(
                    self.channel_url,
                    data={
                        "channel_id": channel,
                        "quality": "low",
                    },
                    headers={"X-Requested-With": "XMLHttpRequest"},
                    schema=validate.Schema(
                        validate.parse_json(),
                        {"streams": [self.stream_schema]},
                        validate.get("streams"),
                    ),
                )
            except PluginError:
                log.debug(f"Received invalid or non-JSON data, attempt {num}/{self.ATTEMPTS}")
                if num >= self.ATTEMPTS:
                    raise
                num = num + 1
                time.sleep(self.TIMEOUT)

    def vod(self, vod_id) -> list[tuple[str, str, int]]:
        return self.session.http.get(
            self.vod_url.format(vod_id),
            schema=validate.Schema(
                validate.parse_json(),
                {"response": {"streams": {str: self.stream_schema}}},
                validate.get(("response", "streams")),
                validate.transform(lambda d: d.values()),
            ),
        )


@pluginmatcher(
    re.compile(
        r"""
            https?://(?:www\.)?filmon\.(?:tv|com)/
            (?:
                (?:
                    index/popout\?
                    |
                    (?:tv/)?channel/(?:export\?)?
                    |
                    tv/(?!channel/)
                    |
                    channel/
                    |
                    (?P<is_group>group/)
                )(?:channel_id=)?(?P<channel>[-_\w]+)
                |
                vod/view/(?P<vod_id>[^/?&]+)
            )
        """,
        re.VERBOSE,
    ),
)
class Filmon(Plugin):
    quality_weights = {
        "high": 720,
        "low": 480,
    }

    TIME_CHANNEL = 60 * 60 * 24 * 365

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        parsed = urlparse(self.url)
        if parsed.path.startswith("/channel/"):
            self.url = urlunparse(parsed._replace(path=parsed.path.replace("/channel/", "/tv/")))
        self.api = FilmOnAPI(self.session)

        adapter = TLSSecLevel1Adapter()
        self.session.http.mount("https://filmon.com", adapter)
        self.session.http.mount("https://www.filmon.com", adapter)
        self.session.http.mount("https://vms-admin.filmon.com/", adapter)

        self.session.options.set("hls-playlist-reload-time", "segment")

    @classmethod
    def stream_weight(cls, stream: str) -> tuple[float, str]:
        weight = cls.quality_weights.get(stream)
        if weight:
            return weight, "filmon"

        return super().stream_weight(stream)

    def _get_streams(self):
        channel = self.match.group("channel")
        vod_id = self.match.group("vod_id")
        is_group = self.match.group("is_group")

        # get cookies
        self.session.http.get(self.url)

        if vod_id:
            for quality, url, _timeout in self.api.vod(vod_id):
                if url.endswith(".m3u8"):
                    streams = HLSStream.parse_variant_playlist(self.session, url)
                    if streams:
                        yield from streams.items()
                        return
                    yield quality, HLSStream(self.session, url)
                elif url.endswith(".mp4"):
                    yield quality, HTTPStream(self.session, url)
        else:
            if not channel or channel.isdigit():
                id_ = channel
            else:
                id_ = self.cache.get(channel)
                if id_ is not None:
                    log.debug(f"Found cached channel ID: {id_}")
                else:
                    id_ = self.session.http.get(
                        self.url,
                        schema=validate.Schema(
                            re.compile(r"""channel_id\s*=\s*(?P<q>['"]?)(?P<value>\d+)(?P=q)"""),
                            validate.any(None, validate.get("value")),
                        ),
                    )
                    log.debug(f"Found channel ID: {id_}")
                    # do not cache a group url
                    if id_ and not is_group:
                        self.cache.set(channel, id_, expires=self.TIME_CHANNEL)

            if id_ is None:
                raise PluginError(f"Unable to find channel ID: {channel}")

            try:
                for quality, url, _timeout in self.api.channel(id_):
                    yield quality, FilmOnHLS(self.session, url, self.api, channel=id_, quality=quality)
            except Exception:
                if channel and not channel.isdigit():
                    self.cache.set(channel, None, expires=0)
                    log.debug(f"Reset cached channel: {channel}")
                raise


__plugin__ = Filmon
