#
# Copyright (C) Calum Lind 2017 <calumlind+deluge@gmail.com>
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#

import logging
import os
import uuid
from socket import gaierror, getaddrinfo

from twisted.internet import defer

from deluge.common import get_localhost_auth
from deluge.config import Config
from deluge.configmanager import get_config_dir
from deluge.ui.client import Client, client

log = logging.getLogger(__name__)

DEFAULT_HOST = '127.0.0.1'
DEFAULT_PORT = 58846
LOCALHOST = ('127.0.0.1', 'localhost', '::1')


def default_hostlist():
    """Create a new hosts key for hostlist with a localhost entry"""
    host_id = uuid.uuid4().hex
    username, password = get_localhost_auth()
    return {'hosts': [(host_id, DEFAULT_HOST, DEFAULT_PORT, username, password)]}


def validate_host_info(hostname, port):
    """Checks that hostname and port are valid.

    Args:
        hostname (str): The IP or hostname of the deluge daemon.
        port (int): The port of the deluge daemon.

    Raises:
        ValueError: Host details are not valid with reason.
    """

    try:
        getaddrinfo(hostname, None)
    except gaierror as ex:
        raise ValueError('Host %s: %s', hostname, ex.args[1])

    if not isinstance(port, int):
        raise ValueError('Invalid port. Must be an integer')


def migrate_hostlist(old_filename, new_filename):
    """Check for old hostlist filename and save details to new filename"""
    old_hostlist = get_config_dir(old_filename)
    if os.path.isfile(old_hostlist):
        config_v2 = Config(old_filename, config_dir=get_config_dir())
        config_v2.save(get_config_dir(new_filename))
        del config_v2

        try:
            os.rename(old_hostlist, old_hostlist + '.old')
        except OSError as ex:
            log.exception(ex)

        try:
            os.remove(old_hostlist + '.bak')
        except OSError:
            pass


def migrate_config_2_to_3(config):
    """Mirgrates old hostlist config files to new file version"""
    localclient_username, localclient_password = get_localhost_auth()
    if not localclient_username:
        # Nothing to do here, there's no auth file
        return
    for idx, (__, host, __, username, __) in enumerate(config['hosts'][:]):
        if host in LOCALHOST and not username:
            config['hosts'][idx][3] = localclient_username
            config['hosts'][idx][4] = localclient_password
    return config


def mask_hosts_password(hosts):
    """Replace passwords in hosts list with *'s for log output"""
    if not hosts:
        return hosts

    return [list(host)[:-1] + ['*' * 10] for host in hosts]


class HostList:
    """This class contains methods for adding, removing and looking up hosts in hostlist.conf."""

    def __init__(self):
        migrate_hostlist('hostlist.conf.1.2', 'hostlist.conf')
        self.config = Config(
            'hostlist.conf',
            default_hostlist(),
            config_dir=get_config_dir(),
            file_version=3,
            log_mask_funcs={'hosts': mask_hosts_password},
        )
        self.config.run_converter((1, 2), 3, migrate_config_2_to_3)
        self.config.save()

    def check_info_exists(self, hostname, port, username, skip_host_id=None):
        """Check for existing host entries with the same details.

        Args:
            hostname (str): The IP or hostname of the deluge daemon.
            port (int): The port of the deluge daemon.
            username (str): The username to login to the daemon with.
            skip_host_id (str): A host_id to skip to check if other hosts match details.

        Raises:
            ValueError: Host details already exist.

        """
        for host_entry in self.config['hosts']:
            if (hostname, port, username) == (
                host_entry[1],
                host_entry[2],
                host_entry[3],
            ):
                if skip_host_id is not None and skip_host_id == host_entry[0]:
                    continue
                raise ValueError('Host details already in hostlist')

    def add_host(self, hostname, port, username, password):
        """Add a new host to hostlist.

        Args:
            hostname (str): The IP or hostname of the deluge daemon.
            port (int): The port of the deluge daemon.
            username (str): The username to login to the daemon with.
            password (str): The password to login to the daemon with.

        Returns:
            str: The new host id.
        """
        if (
            not password and not username or username == 'localclient'
        ) and hostname in LOCALHOST:
            username, password = get_localhost_auth()

        validate_host_info(hostname, port)
        self.check_info_exists(hostname, port, username)
        host_id = uuid.uuid4().hex
        self.config['hosts'].append((host_id, hostname, port, username, password))
        self.config.save()
        return host_id

    def get_host_info(self, host_id):
        """Get the host details for host_id.

        Args:
            host_id (str): The host id to get info on.

        Returns:
            list: A list of (host_id, hostname, port, username).

        """
        for host_entry in self.config['hosts']:
            if host_entry[0] == host_id:
                return host_entry[0:4]
        else:
            return []

    def get_hosts_info(self):
        """Get information of all the hosts in the hostlist.

        Returns:
            list of lists: Host information in the format [(host_id, hostname, port, username)].

        """
        return [host_entry[0:4] for host_entry in self.config['hosts']]

    def get_host_status(self, host_id):
        """Gets the current status (online/offline) of the host

        Args:
            host_id (str): The host id to check status of.

        Returns:
            tuple: A tuple of strings (host_id, status, version).

        """
        status_offline = (host_id, 'Offline', '')

        def on_connect(result, c, host_id):
            """Successfully connected to a daemon"""

            def on_info(info, c):
                c.disconnect()
                return host_id, 'Online', info

            def on_info_fail(reason, c):
                c.disconnect()
                return status_offline

            return c.daemon.info().addCallback(on_info, c).addErrback(on_info_fail, c)

        def on_connect_failed(reason, host_id):
            """Connection to daemon failed"""
            log.debug('Host status failed for %s: %s', host_id, reason)
            return status_offline

        try:
            host_id, host, port, user = self.get_host_info(host_id)
        except ValueError:
            log.warning('Problem getting host_id info from hostlist')
            return defer.succeed(status_offline)

        try:
            ips = list({addrinfo[4][0] for addrinfo in getaddrinfo(host, None)})
        except (gaierror, IndexError) as ex:
            log.warning('Unable to resolve host %s to IP: %s', host, ex.args[1])
            return defer.succeed(status_offline)

        host_conn_list = [
            (
                host_ip,
                port,
                'localclient' if not user and host_ip in LOCALHOST else user,
            )
            for host_ip in ips
        ]

        for host_conn_info in host_conn_list:
            if client.connected() and host_conn_info == client.connection_info():
                # Currently connected to host_id daemon.
                def on_info(info, host_id):
                    log.debug('Client connected, query info: %s', info)
                    return host_id, 'Connected', info

                return client.daemon.info().addCallback(on_info, host_id)
            else:
                # Attempt to connect to daemon with host_id details.
                c = Client()
                d = c.connect(host, port, skip_authentication=True)
                d.addCallback(on_connect, c, host_id)
                d.addErrback(on_connect_failed, host_id)
                return d

    def update_host(self, host_id, hostname, port, username, password):
        """Update the supplied host id with new connection details.

        Args:
            host_id (str): The host id to update.
            hostname (str): The new IP or hostname of the deluge daemon.
            port (int): The new port of the deluge daemon.
            username (str): The new username to login to the daemon with.
            password (str): The new password to login to the daemon with.

        """
        validate_host_info(hostname, port)
        self.check_info_exists(hostname, port, username, skip_host_id=host_id)

        if (
            not password and not username or username == 'localclient'
        ) and hostname in LOCALHOST:
            username, password = get_localhost_auth()

        for idx, host_entry in enumerate(self.config['hosts']):
            if host_id == host_entry[0]:
                self.config['hosts'][idx] = host_id, hostname, port, username, password
                self.config.save()
                return True
        return False

    def remove_host(self, host_id):
        """Removes the host entry from hostlist config.

        Args:
            host_id (str): The host id to remove.

        Returns:
            bool: True is successfully removed, False otherwise.

        """
        for host_entry in self.config['hosts']:
            if host_id == host_entry[0]:
                self.config['hosts'].remove(host_entry)
                self.config.save()
                return True
        else:
            return False

    def add_default_host(self):
        self.add_host(DEFAULT_HOST, DEFAULT_PORT, *get_localhost_auth())

    def connect_host(self, host_id):
        """Connect to host daemon"""
        for host_entry in self.config['hosts']:
            if host_entry[0] == host_id:
                __, host, port, username, password = host_entry
                return client.connect(host, port, username, password)

        return defer.fail(Exception('Bad host id'))
