from __future__ import annotations

import urllib.parse
import webbrowser
from contextlib import suppress
from functools import cached_property
from itertools import islice, zip_longest
from typing import TYPE_CHECKING, Any, TypeVar

from beets import ui
from beets.logging import getLogger
from beetsplug._utils.requests import RequestHandler
from beetsplug.tidal.session import TidalSession

if TYPE_CHECKING:
    from collections.abc import Iterable

    from .api_types import (
        AlbumDocument,
        Document,
        SearchDocument,
        TrackDocument,
    )

    T = TypeVar("T")

log = getLogger("beets.tidal")


API_BASE = "https://openapi.tidal.com/v2"
MAX_FILTER_SIZE = 20


def _batched(iterable: Iterable[T], n: int) -> Iterable[list[T]]:
    # FIXME: Replace with itertools.batched once
    # we upgrade to python > 3.12
    it = iter(iterable)
    while batch := list(islice(it, n)):
        yield batch


class TidalAPI(RequestHandler):
    def __init__(self, client_id: str, token_path: str) -> None:
        self.client_id = client_id
        self.token_path = token_path

    @cached_property
    def session(self) -> TidalSession:
        return TidalSession(self.client_id, self.token_path)

    def search_results(
        self,
        query: str,
        *,
        explicit_filter: str = "INCLUDE",
        include: list[str] | None = None,
        country_code: str = "US",
    ) -> SearchDocument:
        """Search results for a query.

        https://tidal-music.github.io/tidal-api-reference/#/searchResults
        """
        params = {
            "explicitFilter": explicit_filter,
            "countryCode": country_code,
            "include": include or [],
        }

        return self.get_json(
            f"{API_BASE}/searchResults/{urllib.parse.quote(query)}",
            params=params,
        )

    def get_tracks(
        self,
        ids: list[str] | None = None,
        isrcs: list[str] | None = None,
        include: list[str] | None = None,
        country_code: str = "US",
    ) -> TrackDocument:
        """Fetch tracks resolving pagination and included items.

        https://tidal-music.github.io/tidal-api-reference/#/tracks/get_tracks
        """
        ids = ids or []
        isrcs = isrcs or []

        # Tidal allows at max 20 filters per request. This needs a bit of extra
        # logic sadly.
        doc: TrackDocument = {"data": [], "included": []}
        for id_batch, isrc_batch in zip_longest(
            _batched(ids, MAX_FILTER_SIZE),
            _batched(isrcs, MAX_FILTER_SIZE),
            fillvalue=(),
        ):
            params: dict[str, Any] = {"countryCode": country_code}
            if id_batch:
                params["filter[id]"] = id_batch
            if isrc_batch:
                params["filter[isrc]"] = isrc_batch

            doc = self.merge_multiresource_pagination(
                doc,
                self.get_paginated(
                    f"{API_BASE}/tracks", include, params=params
                ),
            )

        return doc

    def get_albums(
        self,
        ids: list[str] | None = None,
        barcode_ids: list[str] | None = None,
        include: list[str] | None = None,
        country_code: str = "US",
    ) -> AlbumDocument:
        """Fetch Albums resolving pagination and included items.

        https://tidal-music.github.io/tidal-api-reference/#/albums/get_albums
        """
        ids = ids or []
        barcode_ids = barcode_ids or []

        # Tidal allows at max 20 filters per request. This needs a bit of extra
        # logic sadly.
        doc: AlbumDocument = {"data": [], "included": []}
        for id_batch, barcode_batch in zip_longest(
            _batched(ids, MAX_FILTER_SIZE),
            _batched(barcode_ids, MAX_FILTER_SIZE),
            fillvalue=(),
        ):
            params: dict[str, Any] = {"countryCode": country_code}
            if id_batch:
                params["filter[id]"] = id_batch
            if barcode_batch:
                params["filter[barcodeId]"] = barcode_batch

            doc = self.merge_multiresource_pagination(
                doc,
                self.get_paginated(
                    f"{API_BASE}/albums", include, params=params
                ),
            )

        return doc

    def ui_authenticate_flow(self) -> None:
        """Interactive first-time authentication (PKCE flow).

        1. Visit generated URL
        2. Paste full redirect URL (with ?code=...)
        3. Token auto-saved for future use
        """
        auth_url, _ = self.session.authorization_url(
            "https://login.tidal.com/authorize"
        )
        ui.print_(f"Visit: {auth_url}")
        with suppress(webbrowser.Error):
            webbrowser.open(auth_url)
        redirect_url = ui.input_("Paste redirected URL: ")
        self.session.fetch_token(
            "https://auth.tidal.com/v1/oauth2/token",
            authorization_response=redirect_url,
            include_client_id=True,
        )
        self.session.save_token(self.session.token)
        ui.print_(f"Saved tidal token in {self.session.token_path}")

    @staticmethod
    def merge_multiresource_pagination(
        a: Document[list[T]], b: Document[list[T]]
    ) -> Document[list[T]]:
        """
        Merge of b into a, following JSON:API spec rules.

        - Appends data arrays
        - Deduplicates included by (type, id)
        - Updates links (b overrides a)
        """
        a["included"] = a.get("included", [])
        a["links"] = a.get("links", {})

        a["data"].extend(b["data"])

        # Merge included with deduplication
        seen = {(item["type"], item["id"]) for item in a["included"]}
        for item in b.get("included", []):
            key = (item["type"], item["id"])
            if key not in seen:
                seen.add(key)
                a["included"].append(item)

        # Update pagination links (final state wins)
        a["links"] = b.get("links", {})
        return a

    def get_paginated(
        self,
        url: str,
        include: list[str] | str | None = None,
        params: dict[str, Any] | None = None,
        **kwargs,
    ) -> Document[list[Any]]:
        """
        Perform a GET request to the Tidal API with pagination resolution.
        """
        include = include or []
        params = params or {}

        doc: Document[list[Any]] = {
            "data": [],
            "included": [],
            "links": {"next": url},
        }

        while next_ := doc.get("links", {}).get("next"):
            page_doc = self.get_json(
                url=next_, params={**params, "include": include}, **kwargs
            )
            doc = self.merge_multiresource_pagination(doc, page_doc)

        # Dedupe include
        doc["included"] = list(
            {
                (item["type"], item["id"]): item
                for item in doc.get("included", [])
            }.values()
        )
        return doc
