import asyncio
import functools

import spotipy
from spotipy.oauth2 import CacheFileHandler

from .. import fhem, utils
from .. import generic


class spotify(generic.FhemModule):
    def __init__(self, logger):
        super().__init__(logger)
        # Spotipy Spotify instance
        self.spotipy = None
        # Spotipy PKCE authenticator instance
        self.spotipy_pkce = None
        # Available Spotify Connect devices
        self.available_devices = {}
        self._last_data = {}
        self.spotipy_scope = (
            ""
            # Spotify scopes: https://developer.spotify.com/documentation/general/guides/scopes/
            # Listening history
            + "user-read-recently-played "
            + "user-top-read "
            + "user-read-playback-position "
            # Spotify connect
            + "user-read-playback-state "
            + "user-modify-playback-state "
            + "user-read-currently-playing "
            # Playback
            + "streaming "
            # Playlist
            + "playlist-read-private "
            + "playlist-read-collaborative "
            # Follow
            + "user-follow-read "
            # Library
            + "user-library-read "
            # User
            + "user-read-email "
            + "user-read-private"
        )
        self.loggedin = False
        attr_config = {
            "update_status_interval": {
                "default": 60,
                "format": "int",
                "help": "Change interval, default is 60s.",
            },
            "update_devices_interval": {
                "default": 300,
                "format": "int",
                "help": "Change interval, default is 300s.",
            },
            "default_device": {"default": None, "help": "Set default device ID."},
            "spotify_connect": {
                "default": "off",
                "options": "on,off",
                "help": "Activate FHEM Connect Player within FHEM Web. Just open Spotify app and stream to FHEM Web Player.",
            },
        }
        self.set_attr_config(attr_config)

        self.set_config = {
            "play": {
                "args": ["uri", "deviceid"],
                "function": "set_command",
                "params": {
                    "uri": {"default": None, "optional": False},
                    "deviceid": {"default": None, "optional": True},
                },
            },
            "pause": {
                "args": ["deviceid"],
                "function": "set_command",
                "params": {"deviceid": {"default": None, "optional": True}},
            },
            "next_track": {
                "args": ["deviceid"],
                "function": "set_command",
                "params": {"deviceid": {"default": None, "optional": True}},
            },
            "previous_track": {
                "args": ["deviceid"],
                "function": "set_command",
                "params": {"deviceid": {"default": None, "optional": True}},
            },
            "authcode": {"args": ["code"]},
            "update_devices": {},
            "status": {"function": "set_command"},
            "shuffle": {
                "args": ["onoff"],
                "params": {
                    "onoff": {"default": True, "optional": True},
                },
                "options": "on,off",
                "function": "set_command",
            },
            "volume": {
                "args": ["vol"],
                "params": {
                    "vol": {"optional": False},
                },
                "options": "slider,0,1,100",
                "function": "set_command",
            },
            "transfer_playback": {
                "args": ["deviceid"],
                "function": "set_command",
            },
        }
        self.set_set_config(self.set_config)
        return

    # FHEM FUNCTION
    async def Define(self, hash, args, argsh):
        await super().Define(hash, args, argsh)
        await fhem.readingsBeginUpdate(hash)
        await fhem.readingsBulkUpdateIfChanged(hash, "state", "disconnected")
        await fhem.readingsEndUpdate(hash, 1)
        await self.set_auth_url()
        self.create_async_task(self.connect_spotipy())

    async def FW_detailFn(self, hash, args, argsh):
        spotify_script = ""
        if self._attr_spotify_connect == "on":
            spotify_script = """
                <script src="https://sdk.scdn.co/spotify-player.js"></script>
                <script>
                    window.onSpotifyWebPlaybackSDKReady = () => {
                    const token = '###SPOTIFY_ACCESS_TOKEN###';
                    const player = new Spotify.Player({
                        name: 'FHEM Web Player',
                        getOAuthToken: cb => { cb(token); }
                    });

                    // Error handling
                    player.addListener('initialization_error', ({ message }) => { console.error(message); });
                    player.addListener('authentication_error', ({ message }) => { console.error(message); });
                    player.addListener('account_error', ({ message }) => { console.error(message); });
                    player.addListener('playback_error', ({ message }) => { console.error(message); });

                    // Playback status updates
                    player.addListener('player_state_changed', state => { 
                        console.log(state);
                        var div = document.getElementsByClassName("makeSelect")[0];
                        var dev = div.getAttribute("dev");
                        FW_cmd(FW_root + '?cmd=set ' + dev + ' status&XHR=1');
                    });

                    // Ready
                    player.addListener('ready', ({ device_id }) => {
                        console.log('Ready with Device ID', device_id);
                        $('<div id="idSpotifyConnect" class="makeTable help"></div>')
                            .insertBefore("div.makeSelect");
                        $("div#idSpotifyConnect").html("Spotify Connect active with device id: " + device_id + "<br>");
                    });

                    // Not Ready
                    player.addListener('not_ready', ({ device_id }) => {
                        console.log('Device ID has gone offline', device_id);
                    });

                    // Connect to the player!
                    player.connect();
                    };
                </script>
            """
            access_token = self.spotipy_pkce.get_cached_token()
            if access_token is not None:
                spotify_script = spotify_script.replace(
                    "###SPOTIFY_ACCESS_TOKEN###", access_token["access_token"]
                )
        detailfn = await super().FW_detailFn(hash, args, argsh)
        return spotify_script + str(detailfn)

    async def set_auth_url(self):
        # not sure if this is useful...
        x = "e92855a009"
        y = "e74eb69ba6609d3bfd7d"
        z = 90 + 6
        handler = CacheFileHandler(cache_path=f".{self.hash['NAME']}_spotify_token")
        self.spotipy_pkce = spotipy.oauth2.SpotifyPKCE(
            f"{x}{y}{str(z)}",
            "https://europe-west1-fhem-ga-connector.cloudfunctions.net/codelanding/start",
            scope=self.spotipy_scope,
            cache_handler=handler,
        )
        url = self.spotipy_pkce.get_authorize_url()
        url = (
            '<html><a href="'
            + url
            + '" target="_blank">Connect Spotify account (new window/tab)</a><br></html>'
        )
        await fhem.readingsSingleUpdate(self.hash, "login", url, 1)

    async def set_authcode(self, hash, params):
        code = params["code"]
        self.create_async_task(self.handle_authcode(code))

    async def handle_authcode(self, code):
        access_token = await utils.run_blocking(
            functools.partial(
                self.spotipy_pkce.get_access_token, code=code, check_cache=False
            )
        )
        await self.connect_spotipy()

    async def connect_spotipy(self):
        if self.spotipy_pkce.get_cached_token() is None:
            await fhem.readingsSingleUpdate(self.hash, "state", "login required", 1)
            return

        if self.spotipy is None:
            self.spotipy = spotipy.Spotify(auth_manager=self.spotipy_pkce)
            user = await utils.run_blocking(
                functools.partial(self.spotipy.current_user)
            )
            if user is not None:
                self.loggedin = True
                await fhem.readingsSingleUpdate(
                    self.hash,
                    "user",
                    user["display_name"] + " (" + user["email"] + ")",
                    1,
                )
                await fhem.readingsSingleUpdate(self.hash, "state", "connected", 1)
                await self.update_devices()
                self.create_async_task(self.update_playback_loop())
                self.create_async_task(self.update_devices_loop())

    async def update_playback_loop(self):
        while True:
            try:
                await self.update_playback()
            except Exception:
                self.logger.exception("Failed to update playback")
            await asyncio.sleep(self._attr_update_status_interval)

    async def update_devices_loop(self):
        while True:
            try:
                await self.update_devices()
            except Exception:
                self.logger.exception("Failed to update devices")
            await asyncio.sleep(self._attr_update_devices_interval)

    async def set_update_devices(self, hash, params):
        self.create_async_task(self.update_devices())

    async def update_devices(self):
        if not self.loggedin:
            return
        # Query spotify for active devices
        devices_available = await utils.run_blocking(
            functools.partial(self.spotipy.devices)
        )

        self.available_devices = {}
        await fhem.CommandDeleteReading(self.hash, f"{self.hash['NAME']} device_.*")
        # update device list
        await fhem.readingsBeginUpdate(self.hash)
        for device in devices_available["devices"]:
            await fhem.readingsBulkUpdateIfChanged(
                self.hash, "device_" + device["id"], device["name"]
            )
            self.available_devices[device["id"]] = device
            if self._attr_default_device is None:
                self._attr_default_device = device["id"]
                await fhem.readingsBulkUpdateIfChanged(
                    self.hash,
                    "default_device",
                    device["name"] + " (" + device["id"] + ")",
                )
        await fhem.readingsEndUpdate(self.hash, 1)

    async def set_attr_default_device(self, hash):
        if self._attr_default_device in self.available_devices:
            await fhem.readingsSingleUpdateIfChanged(
                self.hash,
                "default_device",
                self._attr_default_device
                + " ("
                + self.available_devices[self._attr_default_device]["name"]
                + ")",
                1,
            )

    async def set_command(self, hash, params):
        if not self.loggedin:
            return "Please login first"

        if params["cmd"] == "pause":
            utils.run_blocking_task(
                functools.partial(self.spotipy.pause_playback, params["deviceid"])
            )
        elif params["cmd"] == "play":
            if "track" in params["uri"]:
                utils.run_blocking_task(
                    functools.partial(
                        self.spotipy.start_playback,
                        device_id=params["deviceid"],
                        uris=[params["uri"]],
                    )
                )
            else:
                utils.run_blocking_task(
                    functools.partial(
                        self.spotipy.start_playback,
                        device_id=params["deviceid"],
                        context_uri=params["uri"],
                    )
                )
        elif params["cmd"] == "next_track":
            utils.run_blocking_task(
                functools.partial(self.spotipy.next_track, params["deviceid"])
            )
        elif params["cmd"] == "previous_track":
            utils.run_blocking_task(
                functools.partial(self.spotipy.previous_track, params["deviceid"])
            )
        elif params["cmd"] == "status":
            self.create_async_task(self.update_playback())
        elif params["cmd"] == "shuffle":
            utils.run_blocking_task(
                functools.partial(self.spotipy.shuffle, params["onoff"] == "on")
            )
        elif params["cmd"] == "volume":
            utils.run_blocking_task(
                functools.partial(self.spotipy.volume, params["vol"])
            )
        elif params["cmd"] == "transfer_playback":
            utils.run_blocking_task(
                functools.partial(self.spotipy.transfer_playback, params["deviceid"])
            )

    async def update_playback(self):
        try:
            status = await utils.run_blocking(
                functools.partial(self.spotipy.current_playback)
            )
            flat_status = utils.flatten_json(status)
            if self._last_data:
                del_readings = set(self._last_data) - set(flat_status)
            else:
                del_readings = {}
                await fhem.CommandDeleteReading(
                    self.hash, self.hash["NAME"] + " current_.*"
                )

            await fhem.readingsBeginUpdate(self.hash)
            for status_name in flat_status:
                reading = "current_" + status_name
                if "available_markets" in status_name:
                    continue
                if "device_volume_percent" == status_name:
                    reading = "volume"
                await fhem.readingsBulkUpdateIfChanged(
                    self.hash, reading, flat_status[status_name]
                )
            await fhem.readingsEndUpdate(self.hash, 1)

            # delete old readings which were not updated
            for del_reading in del_readings:
                await fhem.CommandDeleteReading(
                    self.hash, self.hash["NAME"] + " current_" + del_reading
                )
            self._last_current = flat_status
        except Exception:
            self.logger.exception("Failed to update status")
