# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, print_function, unicode_literals

#################################################################################################

import json
import socket
from datetime import datetime
from operator import itemgetter
import traceback

import urllib3

from ..helper import LazyLogger

from .credentials import Credentials
from .api import API

#################################################################################################

LOG = LazyLogger(__name__)
CONNECTION_STATE = {
    "Unavailable": 0,
    "ServerSelection": 1,
    "ServerSignIn": 2,
    "SignedIn": 3,
}

#################################################################################################


class ConnectionManager(object):

    user = {}
    server_id = None

    def __init__(self, client):

        LOG.debug("ConnectionManager initializing...")

        self.client = client
        self.config = client.config
        self.credentials = Credentials()

        self.API = API(client)

    def revoke_token(self):

        LOG.info("revoking token")

        self["server"]["AccessToken"] = None
        self.credentials.set_credentials(self.credentials.get())

        self.config.data["auth.token"] = None

    def get_available_servers(self):

        LOG.info("Begin getAvailableServers")

        # Clone the credentials
        credentials = self.credentials.get()
        found_servers = self.process_found_servers(self._server_discovery())

        if (
            not found_servers and not credentials["Servers"]
        ):  # back out right away, no point in continuing
            LOG.info("Found no servers")
            return list()

        servers = list(credentials["Servers"])

        # Merges servers we already knew with newly found ones
        for found_server in found_servers:
            try:
                self.credentials.add_update_server(servers, found_server)
            except KeyError:
                continue

        servers.sort(key=itemgetter("DateLastAccessed"), reverse=True)
        credentials["Servers"] = servers
        self.credentials.set(credentials)

        return servers

    def login(self, server_url, username, password=None):

        if not username:
            raise AttributeError("username cannot be empty")

        if not server_url:
            raise AttributeError("server url cannot be empty")

        data = self.API.login(
            server_url, username, password
        )  # returns empty dict on failure

        if not data:
            LOG.info("Failed to login as `" + username + "`")
            return {}

        LOG.info("Successfully logged in as %s" % (username))
        # TODO Change when moving to database storage of server details
        return self._save_auth_credentials(data)

    def connect_to_address(self, address, options={}):

        if not address:
            return False

        address = self._normalize_address(address)

        try:
            response_url = self.API.check_redirect(address)
            if address != response_url:
                address = response_url
            LOG.info("connectToAddress %s succeeded", address)
            server = {
                "address": address,
            }
            server = self.connect_to_server(server, options)
            if server is False:
                LOG.error("connectToAddress %s failed", address)
                return {"State": CONNECTION_STATE["Unavailable"]}

            return server
        except Exception as error:
            LOG.exception(error)
            LOG.error("connectToAddress %s failed", address)
            return {"State": CONNECTION_STATE["Unavailable"]}

    def connect_to_server(self, server, options={}):

        LOG.info("begin connectToServer")

        try:
            result = self.API.get_public_info(server.get("address"))

            if not result:
                LOG.error("Failed to connect to server: %s" % server.get("address"))
                return {"State": CONNECTION_STATE["Unavailable"]}

            LOG.info(
                "calling onSuccessfulConnection with server %s", server.get("Name")
            )

            self._update_server_info(server, result)
            credentials = self.credentials.get()
            return self._after_connect_validated(
                server, credentials, result, True, options
            )

        except Exception as e:
            LOG.error(traceback.format_exc())
            LOG.error("Failing server connection. ERROR msg: {}".format(e))
            return {"State": CONNECTION_STATE["Unavailable"]}

    def connect(self, options={}):

        LOG.info("Begin connect")

        servers = self.get_available_servers()
        LOG.info("connect has %s servers", len(servers))

        if not (len(servers)):  # No servers provided
            return {"State": ["ServerSelection"]}

        result = self.connect_to_server(servers[0], options)
        LOG.debug("resolving connect with result: %s", result)

        return result

    def jellyfin_token(self):  # Called once monitor.py#163
        return self.get_server_info(self.server_id)["AccessToken"]

    def get_server_info(self, server_id):

        if server_id is None:
            LOG.info("server_id is empty")
            return {}

        servers = self.credentials.get()["Servers"]

        for server in servers:
            if server["Id"] == server_id:
                return server

    def get_server_address(self, server_id):
        return self.get_server_info(server_id or self.server_id).get("address")

    def get_public_users(self):
        return self.client.jellyfin.get_public_users()

    def _server_discovery(self):
        MULTI_GROUP = ("<broadcast>", 7359)
        MESSAGE = b"who is JellyfinServer?"

        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.settimeout(1.0)  # This controls the socket.timeout exception
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)

        LOG.debug("MultiGroup      : %s", str(MULTI_GROUP))
        LOG.debug("Sending UDP Data: %s", MESSAGE)

        servers = []

        try:
            sock.sendto(MESSAGE, MULTI_GROUP)
        except Exception as error:
            LOG.exception(traceback.format_exc())
            LOG.exception(error)
            return servers

        while True:
            data = None
            addr = None
            try:
                data, addr = sock.recvfrom(1024)  # buffer size
                servers.append(json.loads(data))

            except socket.timeout:
                LOG.info("Found Servers: %s", servers)
                return servers

            except json.JSONDecodeError:
                LOG.warning("Unable to decode %r from %r.", data, addr)

            except Exception as e:
                LOG.error(traceback.format_exc())
                LOG.exception("Error trying to find servers: %s", e)
                return servers

    def process_found_servers(self, found_servers):

        servers = []

        for found_server in found_servers:

            server = self._convert_endpoint_address_to_manual_address(found_server)

            info = {
                "Id": found_server["Id"],
                "address": server or found_server["Address"],
                "Name": found_server["Name"],
            }

            servers.append(info)

        return servers

    # TODO: Make IPv6 compatible
    def _convert_endpoint_address_to_manual_address(self, info):

        if info.get("Address") and info.get("EndpointAddress"):
            address = info["EndpointAddress"].split(":")[0]

            # Determine the port, if any
            parts = info["Address"].split(":")
            if len(parts) > 1:
                port_string = parts[len(parts) - 1]

                try:
                    address += ":%s" % int(port_string)
                    return self._normalize_address(address)
                except ValueError:
                    pass

        return None

    def _normalize_address(self, address):
        # TODO: Try HTTPS first, then HTTP if that fails.
        if "://" not in address:
            address = "http://" + address

        # Attempt to correct bad input
        url = urllib3.util.parse_url(address.strip())

        if url.scheme is None:
            url = url._replace(scheme="http")

        if url.scheme == "http" and url.port == 80:
            url = url._replace(port=None)

        if url.scheme == "https" and url.port == 443:
            url = url._replace(port=None)

        return url.url

    def _after_connect_validated(
        self, server, credentials, system_info, verify_authentication, options
    ):
        if options.get("enableAutoLogin") is False:

            self.config.data["auth.user_id"] = server.pop("UserId", None)
            self.config.data["auth.token"] = server.pop("AccessToken", None)

        elif verify_authentication and server.get("AccessToken"):
            system_info = self.API.validate_authentication_token(server)
            if "Status_Code" not in system_info:

                self._update_server_info(server, system_info)
                self.config.data["auth.user_id"] = server["UserId"]
                self.config.data["auth.token"] = server["AccessToken"]
                system_info["Status_Code"] = 200

                return self._after_connect_validated(
                    server, credentials, system_info, False, options
                )

            server["UserId"] = None
            server["AccessToken"] = None
            system_info["State"] = CONNECTION_STATE["Unavailable"]
            return system_info

        self._update_server_info(server, system_info)

        server["DateLastAccessed"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
        self.credentials.add_update_server(credentials["Servers"], server)
        self.credentials.set(credentials)
        self.server_id = server["Id"]

        # Update configs
        self.config.data["auth.server"] = server["address"]
        self.config.data["auth.server-name"] = server["Name"]
        self.config.data["auth.server=id"] = server["Id"]
        self.config.data["auth.ssl"] = options.get("ssl", self.config.data["auth.ssl"])

        # Connected
        return {
            "Servers": [server],
            "State": (
                CONNECTION_STATE["SignedIn"]
                if server.get("AccessToken")
                else CONNECTION_STATE["ServerSignIn"]
            ),
        }

    def login_quick_connect(self, server_url, secret):
        """Exchange a Quick Connect secret for an authenticated session."""
        if not server_url:
            raise AttributeError("server url cannot be empty")
        if not secret:
            raise AttributeError("secret cannot be empty")

        data = self.API.quick_connect_authenticate(server_url, secret)

        if not data:
            LOG.info("Quick Connect authentication failed")
            return {}

        LOG.info("Successfully logged in via Quick Connect as %s", data["User"]["Name"])
        return self._save_auth_credentials(data)

    def _save_auth_credentials(self, data):
        """Persist auth token and user info after a successful login."""
        credentials = self.credentials.get()

        self.config.data["auth.user_id"] = data["User"]["Id"]
        self.config.data["auth.token"] = data["AccessToken"]

        for server in credentials["Servers"]:
            if server["Id"] == data["ServerId"]:
                found_server = server
                break
        else:
            return {}

        found_server["DateLastAccessed"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
        found_server["UserId"] = data["User"]["Id"]
        found_server["AccessToken"] = data["AccessToken"]

        self.credentials.add_update_server(credentials["Servers"], found_server)

        info = {"Id": data["User"]["Id"], "IsSignedInOffline": True}
        self.credentials.add_update_user(found_server, info)

        self.credentials.set_credentials(credentials)

        return data

    def _update_server_info(self, server, system_info):

        if server is None or system_info is None:
            return

        server["Name"] = system_info["ServerName"]
        server["Id"] = system_info["Id"]

        if system_info.get("address"):
            server["address"] = system_info["address"]
