import sys
import logging
import http.client
import json
import urllib.error
import getpass
import contextlib

import keyring

import mini_buildd.misc
import mini_buildd.net

LOG = logging.getLogger(__name__)

#: On error, API commands fail with some HTTP error. For further perusal, more information should
#: be available via special HTTP headers starting with this prefix.
HTTP_MESSAGE_PREFIX = "X-Mini-Buildd-Message"

#: Monkey-patch python's maxheader limitation. The default (100) is sometimes too low.
http.client._MAXHEADERS = 5000  # pylint: disable=protected-access


class Client():
    def input(self, prompt):
        return input(f"{self.endpoint}: {prompt}") if self.interactive else None

    def getpass(self, prompt):
        return getpass.getpass(f"{self.endpoint}: {prompt}") if self.interactive else None

    def __init__(self, endpoint, auto_confirm=False, auto_save_passwords=False, interactive=sys.stdout.isatty() and sys.stdin.isatty()):
        self.endpoint = endpoint if isinstance(endpoint, mini_buildd.net.ClientEndpoint) else mini_buildd.net.ClientEndpoint(endpoint, protocol="http")
        self.keyring = keyring.get_keyring()
        self.interactive = interactive
        self.auto_confirm = auto_confirm
        self.auto_save_passwords = auto_save_passwords

        # Setup login/logout handling. Auto-login now if the endpoint was given w/ user
        if self.endpoint.get_user():
            self.login()

        LOG.info(f"{self.endpoint}: Client initialized: Keyring backend: {self.keyring.__class__.__qualname__}, "
                 f"auto_confirm={self.auto_confirm}, auto_save_passwords={self.auto_save_passwords}, interactive={self.interactive}")

        # Extra: status caching
        self._status = None

    def login(self):
        # Retrieve password
        user = self.endpoint.get_user()
        password = self.keyring.get_password(self.endpoint.geturl(), user)
        if password:
            LOG.info(f"{self.endpoint}: Password retrieved for '{user}'")
            new_password = False
        else:
            password = self.getpass(f"Password for '{user}': ")
            new_password = True

        # Login
        self.endpoint.login(password)
        LOG.info(f"{self.endpoint}: User '{user}' logged in")

        # Logged in: Optionally save credentials
        if new_password and (self.auto_save_passwords or self.input(f"Save password for '{user}' to {self.keyring.__class__.__qualname__}: (Y)es, (N)o? ").upper() == "Y"):
            self.keyring.set_password(self.endpoint.geturl(), user, password)
        password = ""  # Maybe safer

        return self

    def try_login(self):
        try:
            self.login()
        except Exception as e:
            LOG.error(f"{self.endpoint}: Login with user '{self.endpoint.get_user()}' failed: {e}")

    def api(self, command, args=None):
        result = None
        confirm_next_call = False
        while result is None:
            try:
                http_args = {"command": command}
                http_args.update({"confirm": command} if (self.auto_confirm or confirm_next_call) else {})
                confirm_next_call = False
                http_args.update(args or {})

                url = self.endpoint.geturl(path=mini_buildd.config.HTTP_PATH_API, query=http_args)
                LOG.info(f"{self.endpoint}: Calling API URL: {url}")
                result = json.load(self.endpoint.urlopen(url))
            except urllib.error.HTTPError as e:
                # Default to HTTP exception as error message; overwrite with the last daemon message.
                # There should always be exactly one daemon message in the HTTP header.
                http_msg = f"API call failed with HTTP Status {e.getcode()}: {e}"
                daemon_msgs = [f"Server says: {mini_buildd.misc.b642u(msg)}" for msg in [v for k, v in list(e.headers.items()) if k.startswith(mini_buildd.client.HTTP_MESSAGE_PREFIX)]]
                error_msg = daemon_msgs[-1] or http_msg

                if e.getcode() == 401 and self.interactive:
                    LOG.error(f"{self.endpoint}: {error_msg}")

                    # note: Would be so much nicer w/ assign. expr. (pep-0572), but we need to be compatible with buster/py 3.7
                    def _get_action():
                        return self.input(f"{error_msg}\n<=> (l)og in, (c)onfirm next [{confirm_next_call}] or (C)onfirm all calls [{self.auto_confirm}] or (r)etry? ")
                    action = _get_action()
                    while action != "r":
                        if action == "l":
                            self.endpoint.set_user(self.input(f"Log in as [{self.endpoint.get_user()}]: ") or self.endpoint.get_user())
                            self.try_login()
                        elif action == "c":
                            confirm_next_call = True
                        elif action == "C":
                            self.auto_confirm = True
                        action = _get_action()
                else:
                    raise Exception(error_msg)

        return result

    def events_iter(self, since=None, types=None, package=None, version=None, minimal_version=None, distribution=None, stop=False):
        with contextlib.closing(self.endpoint.http_connect()) as conn:
            def http_events_iter():
                while True:
                    args = {"since": since.isoformat()} if since is not None else {}
                    conn.request("GET", "/" + mini_buildd.config.HTTP_PATH_EVENTS + "?" + urllib.parse.urlencode(args))
                    response = conn.getresponse()
                    if response.status != 200:
                        raise Exception(f"HTTP Error {response.status}: {response.reason}")
                    yield mini_buildd.events.Event.from_json(json.load(response))

            yield from mini_buildd.events.filter_iter(http_events_iter, types=types, package=package, version=version, minimal_version=minimal_version, distribution=distribution, stop=stop)

    def event(self, **kwargs):
        return next(self.events_iter(**kwargs, stop=True))
