import logging
import os
import re
import tempfile
import time
from pathlib import Path

import ffmpy
import music_tag
import requests
from librespot.audio.decoders import AudioQuality, VorbisOnlyAudioQuality
from librespot.core import Session
from librespot.metadata import TrackId

from ..models import SpotifyTrackMetadata
from ..models.track_metadata import TrackMetadata
from ..utils import format_seconds


class SpotifyClient:
    PLAYLISTS_URL = "https://api.spotify.com/v1/playlists"
    TRACKS_URL = "https://api.spotify.com/v1/tracks"
    EPISODE_INFO_URL = "https://api.spotify.com/v1/episodes"
    SHOWS_URL = "https://api.spotify.com/v1/shows"

    def __init__(self, username, password, target_directory):
        self.username = username
        self.password = password
        self.session = self._get_spotify_session(target_directory)
        self._logger = logging.getLogger(__name__)
        self.download_root = os.path.join(target_directory, "download")
        os.makedirs(self.download_root, exist_ok=True)

    def download_tracks(self, download_root: str, download_url: str) -> list[SpotifyTrackMetadata]:
        """Download all tracks from a Spotify URL.

        Args:
            download_root: The path to the data directory.
            download_url: The URL to download from.

        Returns:
            A list of metadata for the downloaded tracks.
        """
        url_type, spotify_id = self.parse_url(download_url)
        if url_type == "playlist":
            tracks_metadata = self.get_playlist_tracks_metadata(spotify_id)
        return tracks_metadata

    def _get_spotify_session(self, target_directory):
        """Initialize the Spotify session.

        Args:
            target_directory: The path to the data directory.
        """
        cred_location = os.path.join(target_directory, "credentials.json")
        if Path(cred_location).is_file():
            try:
                conf = Session.Configuration.Builder().set_store_credentials(False).build()
                return Session.Builder(conf).stored_file(cred_location).create()
            except RuntimeError:
                pass
        else:
            conf = Session.Configuration.Builder().set_stored_credential_file(cred_location).build()
            return Session.Builder(conf).user_pass(self.username, self.password).create()

    def _get_auth_header(self):
        """Get the authorization header for the Spotify API.

        Returns:
            dict: The authorization header.
        """
        token = self.session.tokens().get_token(
            "user-read-email",
            "playlist-read-private",
            "user-library-read",
            "user-follow-read",
        )
        header = {
            "Authorization": f"Bearer {token.access_token}",
            "Content-Type": "application/json",
            "app-platform": "WebPlayer",
        }
        return header

    def _make_request(self, url, params=None):
        """Make a request to the Spotify API.

        Args:
            url: The URL to make the request to.
            params: The parameters to include in the request.

        Returns:
            dict: The response from the request.

        Raises:
            ValueError: If the response from the URL is invalid or if parsing the response fails.
        """
        header = self._get_auth_header()
        response = requests.get(url, headers=header, params=params)
        response.raise_for_status()
        return response.json()

    def parse_url(self, search_input: str) -> tuple[str, str]:
        """Parse a Spotify URL or URI and return the ID and object type.

        Args:
            search_input: The Spotify URL or URI to parse.

        Returns:
            dict[str, str]: A dict with the Id and type of the object.

        Example:
            >>> regex_input_for_urls('https://open.spotify.com/track/7ouMYWpwJ422jRcDASZB7P?si=abc123')
            >>> "track", "7ouMYWpwJ422jRcDASZB7P"

        Raises:
            ValueError: If no ID is found in the input.
        """
        # Regular expression to match Spotify URLs and URIs
        pattern = re.compile(
            r"(?:spotify:(?P<type>\w+):(?P<id>\w+))|(?:https://open.spotify.com/(?P<type2>\w+)/(?P<id2>\w+))"
        )
        match = pattern.search(search_input)

        if match:
            object_type = match.group("type") or match.group("type2")
            object_id = match.group("id") or match.group("id2")
            return object_type, object_id

        raise ValueError("No ID found in the input.")

    def get_playlist_tracks_metadata(self, playlist_id: str) -> list[SpotifyTrackMetadata]:
        """Retrieve all tracks from a specific Spotify playlist.

        This function fetches all tracks from a playlist identified by its ID from Spotify.
        It handles pagination to retrieve all tracks by making multiple requests if necessary.

        Args:
            playlist_id: The ID of the playlist as a string.

        Returns:
            A list of tracks (list[dict]), where each track is represented as a dictionary containing its metadata.

        Raises:
            ValueError: If the response from the URL is invalid or if parsing the response fails.
        """
        tracks = []
        offset = 0
        limit = 100

        while True:
            resp = self._make_request(
                url=f"{self.PLAYLISTS_URL}/{playlist_id}/tracks",
                params={"limit": limit, "offset": offset},
            )
            offset += limit
            tracks.extend(resp["items"])
            if len(resp["items"]) < limit:
                break
        tracks_metadata = []
        for track in tracks:
            track_metadata = self.get_track_metadata(track["track"]["id"])
            tracks_metadata.append(track_metadata)
        return tracks_metadata

    def get_track_metadata(self, track_id) -> SpotifyTrackMetadata:
        """Retrieve metadata for a specific Spotify track.

        Args:
            track_id: The ID of the track as a string.

        Returns:
            dict: Metadata for the track.

        Raises:
            ValueError: If the response from the URL is invalid or if parsing the response fails.
        """
        try:
            response = self._make_request(
                url=f"{self.TRACKS_URL}",
                params={"ids": track_id, "market": "from_token"},
            )
            track = response["tracks"][0]
            artists = []
            for data in track["artists"]:
                artists.append(data["name"])
            track_metadata = SpotifyTrackMetadata(
                download_root=self.download_root,
                artists=artists,
                album_name=track["album"]["name"],
                name=track["name"],
                image_url=track["album"]["images"][0]["url"],
                release_year=track["album"]["release_date"].split("-")[0],
                disc_number=track["disc_number"],
                track_number=track["track_number"],
                id=track["id"],
                is_playable=track["is_playable"],
                duration_ms=track["duration_ms"],
            )
        except Exception as e:
            raise ValueError(f"Failed to parse TRACKS_URL [ '{track_id}' ] response: {str(e)}")
        return track_metadata

    def process_track(self, track_metadata: SpotifyTrackMetadata) -> SpotifyTrackMetadata:
        """Process a track from Spotify.

        Determine the target location and download the track to the target location, if
        it does not already exist and is playable.

        Args:
            track_id: The id of the track to download.

        Returns:
            bytes: The downloaded track.
        """
        try:
            logging.info(f"Processing [ '{track_metadata.name}' ] ...")
            if os.path.exists(track_metadata.download_path):
                self._logger.info("... track already exists => Skipping download.")
                return track_metadata

            if not track_metadata.is_playable:
                self._logger.warning("... track is not playable => Skipping download.")
                return track_metadata
            self.download_track(track_metadata)
            return track_metadata

        except Exception as e:
            self._logger.error(f"... failed to download track [ '{track_metadata.name}' ]: {str(e)}")
            return track_metadata

    def download_track(
        self,
        track_metadata: SpotifyTrackMetadata,
    ):
        self._logger.info("... downloading")
        download_tempfile = tempfile.NamedTemporaryFile(delete=False)
        librespot_track = TrackId.from_base62(track_metadata.id)
        stream = self.session.content_feeder().load(
            librespot_track, VorbisOnlyAudioQuality(AudioQuality.HIGH), False, None
        )
        total_size = stream.input_stream.size
        time_start = time.time()
        downloaded = 0
        b = 0
        while b < 5:
            data = stream.input_stream.stream().read(20000)
            download_tempfile.write(data)
            downloaded += len(data)
            b += 1 if data == b"" else 0
            if True:  # if args.ban_protection:
                delta_real = time.time() - time_start
                delta_want = (downloaded / total_size) * (track_metadata.duration_ms / 5000)
                if delta_want > delta_real:
                    time.sleep(delta_want - delta_real)
        time_downloaded = time.time()
        logging.info(f"... finished download in {format_seconds(int(time_downloaded - time_start))}!")
        self._convert_to_mp3(download_tempfile.name, track_metadata.download_path)
        self._set_metadata(track_metadata.download_path, track_metadata)

    def _set_metadata(self, file_path, track_metadata: TrackMetadata) -> None:
        tags = music_tag.load_file(file_path)
        tags["albumartist"] = track_metadata.artists[0]
        tags["artist"] = ", ".join(track_metadata.artists)
        tags["tracktitle"] = track_metadata.name
        tags["album"] = track_metadata.album_name
        tags["year"] = track_metadata.release_year
        tags["discnumber"] = track_metadata.disc_number
        tags["tracknumber"] = track_metadata.track_number
        try:
            if track_metadata.image_url:
                image_url = track_metadata.image_url
                img = requests.get(image_url).content
                tags["artwork"] = img
        except Exception as e:
            logging.error(f"Failed to download image: {str(e)}")
        tags.save()
        return

    def _convert_to_mp3(self, temp_filename, filename) -> None:
        """Convert raw audio into a playable file.

        This function uses ffmpy to convert a raw audio file into a playable format.
        It sets the codec to 'libmp3lame' and the bitrate to '160k' unless the codec is 'copy'.
        The function handles errors related to the ffmpeg executable not being found.

        Args:
            temp_filename: The path to the temporary raw audio file as a string.
            filename: The path to the output playable audio file as a string.

        Returns:
            None
        """
        self._logger.info("... converting Track Audio Format")

        output_params = ["-c:a", "libmp3lame", "-b:a", "160k"]

        try:
            ff_m = ffmpy.FFmpeg(
                global_options=["-y", "-hide_banner", "-loglevel error"],
                inputs={temp_filename: None},
                outputs={filename: output_params},
            )
            ff_m.run()

        except ffmpy.FFExecutableNotFoundError:
            logging.warning("Skipping conversion - ffmpeg not found!")
        except ffmpy.FFRuntimeError as e:
            logging.error(f"Failed to convert file: {str(e)}")
