from datetime import datetime
from typing import Any, Callable, Dict, List
import paho.mqtt.client as mqtt
import requests
from .const import (
    BOX_PLAY_STATE_BUFFER,
    BOX_PLAY_STATE_CHANNEL,
    BOX_PLAY_STATE_DVR,
    BOX_PLAY_STATE_REPLAY,
    BOX_PLAY_STATE_APP,
    BOX_PLAY_STATE_VOD,
    ONLINE_STANDBY,
    ONLINE_RUNNING,
    MEDIA_KEY_POWER,
    MEDIA_KEY_PLAY_PAUSE,
    MEDIA_KEY_STOP,
    MEDIA_KEY_CHANNEL_UP,
    MEDIA_KEY_CHANNEL_DOWN,
    MEDIA_KEY_ENTER,
    MEDIA_KEY_REWIND,
    MEDIA_KEY_FAST_FORWARD,
    MEDIA_KEY_RECORD,
    RECORDING_TYPE_SEASON,
    RECORDING_TYPE_SHOW
)

import json
from .helpers import make_id
import logging

_logger = logging.getLogger(__name__)

class LGHorizonAuth:
    householdId: str
    accessToken: str
    refreshToken: str
    refreshTokenExpiry: datetime
    username: str
    mqttToken: str = None

    def __init__(self):
        """Initialize a session."""
        pass
    
    def fill(self, auth_json:str)-> None:
        self.householdId = auth_json["householdId"]
        self.accessToken = auth_json["accessToken"]
        self.refreshToken = auth_json["refreshToken"]
        self.refreshTokenExpiry = datetime.fromtimestamp(auth_json["refreshTokenExpiry"])
        self.username = auth_json["username"]
    
    def is_expired(self) -> bool:
        return self.refreshTokenExpiry

class LGHorizonPlayingInfo:
    """Represent current state of a box."""

    channel_id: str = None
    title: str = None
    image: str = None
    source_type: str = None
    paused: bool = False
    channel_title: str = None

    def __init__(self):
        """Initialize the playing info."""
        pass

    def set_paused(self, paused: bool):
        """Set pause state."""
        self.paused = paused

    def set_channel(self, channel_id):
        """Set channel."""
        self.channel_id = channel_id

    def set_title(self, title):
        """Set title."""
        self.title = title

    def set_channel_title(self, title):
        """Set channel title."""
        self.channel_title = title

    def set_image(self, image):
        """Set image."""
        self.image = image

    def set_source_type(self, source_type):
        """Set sourfce type."""
        self.source_type = source_type

    def reset(self):
        self.channel_id = None
        self.title = None
        self.image = None
        self.source_type = None
        self.paused = False
        self.channel_title = None

class LGHorizonChannel:
    """Represent a channel."""

    id: str
    title: str
    stream_image: str
    logo_image: str
    channel_number: str

    def __init__(self, channel_json):
        """Initialize a channel."""
        self.id = channel_json["id"]
        self.title = channel_json["name"]
        self.stream_image = self.get_stream_image(channel_json)
        self.logo_image = channel_json["logo"]["focused"]
        self.channel_number = channel_json["logicalChannelNumber"]
    
    def get_stream_image(self, channel_json)->str:
        image_stream = channel_json["imageStream"]
        if "full" in image_stream:
            return image_stream["full"]
        if "small" in image_stream:
            return image_stream["small"]
        logo = channel_json["logo"]
        if "focus" in logo:
            return logo["focus"]
        return ""

class LGHorizonReplayEvent:

    episodeNumber: int = None
    channelId: str = None
    eventId: str = None
    seasonNumber:int = None
    title:str = None
    episodeName:str = None

    def __init__(self, raw_json:str):
        self.channelId = raw_json["channelId"]
        self.eventId = raw_json["eventId"]
        self.title = raw_json["title"]
        if "episodeName" in raw_json:
            self.episodeName = raw_json["episodeName"]
        if "episodeNumber" in raw_json:
            self.episodeNumber = raw_json["episodeNumber"]
        if "seasonNumber" in raw_json:
            self.seasonNumber = raw_json["seasonNumber"]


class LGHorizonBaseRecording:
    
    id:str = None
    title:str = None
    image:str = None
    type:str = None
    channelId: str = None
    def __init__(self, id:str, title:str, image:str, channelId: str, type:str) -> None:
        self.id = id
        self.title = title
        self.image = image
        self.channelId = channelId
        self.type = type

class LGHorizonRecordingSingle(LGHorizonBaseRecording):
    """Represents a single recording."""
    seasonNumber: int = None
    episodeNumber: int = None

    def __init__(self, recording_json):
        """Init the single recording."""
        LGHorizonBaseRecording.__init__(
            self,
            recording_json["id"],
            recording_json["title"],
            recording_json["poster"]["url"],
            recording_json["channelId"],
            recording_json["type"]  
            )
        if "seasonNumber" in recording_json:
            self.seasonNumber = recording_json["seasonNumber"]
        if "episodeNumber" in recording_json:
            self.episodeNumber = recording_json["episodeNumber"]

class LGHorizonRecordingEpisode:
    """Represents a single recording."""
    
    episodeId: str = None
    episodeTitle:str = None
    seasonNumber: int = None
    episodeNumber: int = None
    showTitle:str = None
    recordingState:str = None
    image:str = None

    def __init__(self, recording_json):
        """Init the single recording."""
        self.episodeId = recording_json["episodeId"]
        self.episodeTitle = recording_json["episodeTitle"]
        self.showTitle = recording_json["showTitle"]
        self.recordingState = recording_json["recordingState"]
        if "seasonNumber" in recording_json:
            self.seasonNumber = recording_json["seasonNumber"]
        if "episodeNumber" in recording_json:
            self.episodeNumber = recording_json["episodeNumber"]
        if "poster" in recording_json:
            self.image = recording_json["poster"]["url"]

class LGHorizonRecordingShow:
    """Represents a single recording."""
    
    episodeId: str = None
    showTitle:str = None
    seasonNumber: int = None
    episodeNumber: int = None
    recordingState:str = None
    image:str = None

    def __init__(self, recording_json):
        """Init the single recording."""
        self.episodeId = recording_json["episodeId"]
        self.showTitle = recording_json["showTitle"]
        self.recordingState = recording_json["recordingState"]
        if "seasonNumber" in recording_json:
            self.seasonNumber = recording_json["seasonNumber"]
        if "episodeNumber" in recording_json:
            self.episodeNumber = recording_json["episodeNumber"]
        if "poster" in recording_json:
            self.image = recording_json["poster"]["url"]


class LGHorizonRecordingListSeasonShow(LGHorizonBaseRecording):
    showId:str = None
    def __init__(self, recording_season_json):
        """Init the single recording."""
        
        LGHorizonBaseRecording.__init__(
            self,
            recording_season_json["id"],
            recording_season_json["title"],
            recording_season_json["poster"]["url"],
            recording_season_json["channelId"],
            recording_season_json["type"]
            )
        if self.type == RECORDING_TYPE_SEASON:
            self.showId = recording_season_json["showId"]
        else:
            self.showId = recording_season_json["id"]

class LGHorizonVod:
    title:str = None
    image: str = None
    def __init__(self, vod_json) -> None:
        self.title = vod_json['title']

class LGHorizonApp:
    title:str = None
    image:str = None
    def __init__(self, app_state_json:str)->None:
        self.title = app_state_json["appName"]
        self.image = app_state_json["logoPath"]
        if not self.image.startswith("http:"):
                self.image = "https:" + self.image

class LGHorizonMqttClient:
    _brokerUrl:str = None
    _mqtt_client :mqtt.Client
    _auth: LGHorizonAuth
    clientId: str = None
    _on_connected_callback: Callable = None
    _on_message_callback: Callable[[str],None] = None

    @property
    def is_connected(self):
        return self._mqtt_client.is_connected

    def __init__(self, auth:LGHorizonAuth, country_settings:Dict[str,Any], on_connected_callback:Callable = None, on_message_callback:Callable[[str],None] = None):
        self._auth = auth
        self._brokerUrl = country_settings["mqtt_url"]
        self.clientId = make_id()
        self._mqtt_client = mqtt.Client(self.clientId, transport="websockets")
        self._mqtt_client.username_pw_set(self._auth.householdId, self._auth.mqttToken)
        self._mqtt_client.tls_set()
        self._mqtt_client.enable_logger(_logger)
        self._mqtt_client.on_connect = self._on_mqtt_connect
        self._on_connected_callback = on_connected_callback
        self._on_message_callback = on_message_callback
    
    def _on_mqtt_connect(self, client, userdata, flags, resultCode):
        if resultCode == 0:
            self._mqtt_client.on_message = self._on_client_message
            self._mqtt_client.subscribe(self._auth.householdId)
            self._mqtt_client.subscribe(self._auth.householdId + "/#")
            self._mqtt_client.subscribe(self._auth.householdId + "/" + self.clientId)
            self._mqtt_client.subscribe(self._auth.householdId + "/+/status")
            self._mqtt_client.subscribe(self._auth.householdId + "/+/networkRecordings")
            self._mqtt_client.subscribe(self._auth.householdId + "/+/networkRecordings/capacity")
            self._mqtt_client.subscribe(self._auth.householdId + "/watchlistService")
            self._mqtt_client.subscribe(self._auth.householdId + "/purchaseService")
            self._mqtt_client.subscribe(self._auth.householdId + "/personalizationService")
            self._mqtt_client.subscribe(self._auth.householdId + "/recordingStatus")
            self._mqtt_client.subscribe(self._auth.householdId + "/recordingStatus/lastUserAction")
            if self._on_connected_callback:
                self._on_connected_callback()
        elif resultCode == 5:
            self._mqtt_client.username_pw_set(self._auth.householdId, self._auth.mqttToken)
            self.connect()
        else:
            _logger.error(f"Cannot connect to MQTT server with resultCode: {resultCode}")
    
    def connect(self) -> None:
        self._mqtt_client.connect(self._brokerUrl, 443)
        self._mqtt_client.loop_start()
    
    def _on_client_message(self, client, userdata, message):
        """Handle messages received by mqtt client."""
        _logger.debug(f"Received MQTT message. Topic: {message.topic}")
        jsonPayload = json.loads(message.payload)
        _logger.debug(f"Message: {jsonPayload}")
        if self._on_message_callback:
            self._on_message_callback(jsonPayload)

    def publish_message(self, topic:str, json_payload:str) -> None:
        self._mqtt_client.publish(topic, json_payload)
        
    def disconnect(self) -> None:
        if self._mqtt_client.is_connected:
            self._mqtt_client.disconnect()

class LGHorizonBox:

    deviceId:str = None
    hashedCPEId:str = None
    deviceFriendlyName:str = None
    state: str = None
    playing_info: LGHorizonPlayingInfo = LGHorizonPlayingInfo()
    manufacturer:str = None
    model: str = None
    
    _mqtt_client:LGHorizonMqttClient
    _change_callback: Callable = None
    _auth: LGHorizonAuth = None
    _channels:Dict[str, LGHorizonChannel] = None
    _message_stamp = None
    
    def __init__(self, box_json:str, platform_type:Dict[str,str], mqtt_client:LGHorizonMqttClient, auth:LGHorizonAuth, channels:Dict[str, LGHorizonChannel]):
        self.deviceId = box_json["deviceId"]
        self.hashedCPEId = box_json["hashedCPEId"]
        self.deviceFriendlyName = box_json["settings"]["deviceFriendlyName"]
        self._mqtt_client = mqtt_client
        self._auth = auth
        self._channels = channels
        if platform_type:
            self.manufacturer = platform_type["manufacturer"]
            self.model = platform_type["model"]
        
    def register_mqtt(self)->None:
        if not self._mqtt_client.is_connected:
            raise Exception("MQTT client not connected.")
        topic = f"{self._auth.householdId}/{self._mqtt_client.clientId}/status"
        payload = {
            "source": self._mqtt_client.clientId,
            "state": ONLINE_RUNNING,
            "deviceType": "HGO",
        }   
        self._mqtt_client.publish_message(topic, json.dumps(payload))
    
    def set_callback(self, change_callback:Callable) -> None:
        self._change_callback = change_callback

    def update_state(self, payload):
        """Register a new settop box."""
        state = payload["state"]
        if self.state == state:
            return
        self.state = state
        if state == ONLINE_STANDBY:
            self.playing_info.reset()
            if self._change_callback:
                self._change_callback(self.deviceId)
        else:
            self._request_settop_box_state()
    
    def update_with_replay_event(self, source_type: str, event:LGHorizonReplayEvent, channel: LGHorizonChannel) -> None:
        self.playing_info.set_source_type(source_type)
        self.playing_info.set_channel(channel.id)
        self.playing_info.set_channel_title(channel.title)
        title = event.title
        if event.episodeName:
            title += f": {event.episodeName}"
        self.playing_info.set_title(title)
        self.playing_info.set_image(channel.stream_image)
        self._trigger_callback()

    def update_with_recording(self, source_type: str, recording:LGHorizonRecordingSingle, channel: LGHorizonChannel) -> None:
        self.playing_info.set_source_type(source_type)
        self.playing_info.set_channel(channel.id)
        self.playing_info.set_channel_title(channel.title)
        self.playing_info.set_title(f"{recording.title}")
        self.playing_info.set_image(recording.image)
        self._trigger_callback()
    
    def update_with_vod(self, source_type: str, vod:LGHorizonVod) -> None:
        self.playing_info.set_source_type(source_type)
        self.playing_info.set_channel(None)
        self.playing_info.set_channel_title(None)
        self.playing_info.set_title(vod.title)
        self.playing_info.set_image(None)
        self._trigger_callback()
    
    def update_with_app(self, source_type: str, app:LGHorizonApp) -> None:
        self.playing_info.set_source_type(source_type)
        self.playing_info.set_channel(None)
        self.playing_info.set_channel_title(None)
        self.playing_info.set_title(app.title)
        self.playing_info.set_image(app.image)
        self._trigger_callback()
        
    def _trigger_callback(self):
        if self._change_callback:
            _logger.debug(f"Callback called from box {self.deviceId}")
            self._change_callback(self.deviceId)

    def turn_on(self) -> None:
        """Turn the settop box on."""
        
        if self.state == ONLINE_STANDBY:
            self.send_key_to_box(MEDIA_KEY_POWER)

    def turn_off(self) -> None:
        """Turn the settop box off."""
        if self.state == ONLINE_RUNNING:
            self.send_key_to_box(MEDIA_KEY_POWER)
            self.playing_info.reset()

    def pause(self) -> None:
        """Pause the given settopbox."""
        if self.state == ONLINE_RUNNING and not self.playing_info.paused:
            self.send_key_to_box(MEDIA_KEY_PLAY_PAUSE)
    
    def play(self) -> None:
        """Resume the settopbox."""
        if self.state == ONLINE_RUNNING and self.playing_info.paused:
            self.send_key_to_box(MEDIA_KEY_PLAY_PAUSE)

    def stop(self) -> None:
        """Stop the settopbox."""
        if self.state == ONLINE_RUNNING:
            self.send_key_to_box(MEDIA_KEY_STOP)

    def next_channel(self):
        """Select the next channel for given settop box."""
        if self.state == ONLINE_RUNNING:
            self.send_key_to_box(MEDIA_KEY_CHANNEL_UP)

    def previous_channel(self) -> None:
        """Select the previous channel for given settop box."""
        if self.state == ONLINE_RUNNING:
            self.send_key_to_box(MEDIA_KEY_CHANNEL_DOWN)

    def press_enter(self) -> None:
        """Press enter on the settop box."""
        if self.state == ONLINE_RUNNING:
            self.send_key_to_box(MEDIA_KEY_ENTER)

    def rewind(self) -> None:
        """Rewind the settop box."""
        if self.state == ONLINE_RUNNING:
            self.send_key_to_box(MEDIA_KEY_REWIND)

    def fast_forward(self) -> None:
        """Fast forward the settop box."""
        if self.state == ONLINE_RUNNING:
            self.send_key_to_box(MEDIA_KEY_FAST_FORWARD)

    def record(self):
        """Record on the settop box."""
        if self.state == ONLINE_RUNNING:
            self.send_key_to_box(MEDIA_KEY_RECORD)

    def is_available(self) -> bool:
        """Return the availability of the settop box."""
        return self.state == ONLINE_RUNNING or self.state == ONLINE_STANDBY

    def set_channel(self, source:str) -> None:
        """Change te channel from the settopbox."""
        channel = [src for src in self._channels.values() if src.title == source][0]
        payload = (
            '{"id":"'
            + make_id(8)
            + '","type":"CPE.pushToTV","source":{"clientId":"'
            + self._mqtt_client.clientId
            + '","friendlyDeviceName":"Home Assistant"},'
            + '"status":{"sourceType":"linear","source":{"channelId":"'
            + channel.id
            + '"},"relativePosition":0,"speed":1}}'
        )

        self._mqtt_client.publish_message(f"{self._auth.householdId}/{self.deviceId}", payload)

    def play_recording(self, recordingId):
        """Play recording."""
        payload = (
            '{"id":"'
            + make_id(8)
            + '","type":"CPE.pushToTV","source":{"clientId":"'
            + self._mqtt_client.clientId
            + '","friendlyDeviceName":"Home Assistant"},'
            + '"status":{"sourceType":"nDVR","source":{"recordingId":"'
            + recordingId
            + '"},"relativePosition":0}}'
        )
        self._mqtt_client.publish_message(f"{self._auth.householdId}/{self.deviceId}", payload)

    def send_key_to_box(self, key: str) -> None:
        """Send emulated (remote) key press to settopbox."""
        payload = (
            '{"type":"CPE.KeyEvent","status":{"w3cKey":"'
            + key
            + '","eventType":"keyDownUp"}}'
        )
        self._mqtt_client.publish_message(f"{self._auth.householdId}/{self.deviceId}", payload)

    def _set_unknown_channel_info(self) -> None:
        """Set unknown channel info."""
        _logger.warning("Couldn't set channel. Channel info set to unknown...")
        self.playing_info.set_source_type(BOX_PLAY_STATE_CHANNEL)
        self.playing_info.set_channel(None)
        self.playing_info.set_title("No information available")
        self.playing_info.set_image(None)
        self.playing_info.set_paused(False)

    def _request_settop_box_state(self) -> None:
        """Send mqtt message to receive state from settop box."""
        topic = f"{self._auth.householdId}/{self.deviceId}"
        payload = {
            "id": make_id(8),
            "type": "CPE.getUiStatus",
            "source": self._mqtt_client.clientId,
        }
        self._mqtt_client.publish_message(topic, json.dumps(payload))

class LGHorizonCustomer:
    customerId:str = None
    hashedCustomerId:str = None 
    countryId: str = None
    cityId: int = 0
    settop_boxes: Dict[str, LGHorizonBox] = None

    def __init__(self, json_payload):
        self.customerId = json_payload["customerId"]
        self.hashedCustomerId = json_payload["hashedCustomerId"]
        self.countryId = json_payload["countryId"]
        self.cityId = json_payload["cityId"]
        if not "assignedDevices" in json_payload:
            return


