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

import keyring

from mini_buildd import config, util, net, events

LOG = logging.getLogger(__name__)


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()):
        util.systemcert_workaround()

        self.endpoint = endpoint if isinstance(endpoint, net.ClientEndpoint) else 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.debug(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.debug(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.debug(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, timeout=None):
        """
        Run API call (interactive)

        .. attention:: **compat** (``python < 3.7``): Not using Assignment Expressions (PEP-0572) in interactive loop (see ``_get_action()``)
        """
        LOG.debug(f"Client.api(): {command}")

        result = None
        confirm_next_call = False
        while result is None:
            try:
                http_args = command.http_args(with_confirm=self.auto_confirm or confirm_next_call)
                confirm_next_call = False
                url = self.endpoint.geturl(path=config.URIS["api"]["view"].join(command.name() + "/"), query=http_args)
                LOG.debug(f"{self.endpoint}: Calling API URL: {url}")
                result = json.load(self.endpoint.urlopen(url, timeout=timeout))
            except urllib.error.HTTPError as e:
                # Non-200 HTTP responses: Handle retries interactively and create proper exceptions based o rfc7807 result
                try:
                    rfc7807 = util.Rfc7807.from_json(json.loads(e.read().decode(config.CHAR_ENCODING)))
                except BaseException:
                    rfc7807 = None

                if e.getcode() == 401 and self.interactive:
                    action = self.input(f"FAILED: {rfc7807}\n<=> (l)og in, (c)onfirm next [{confirm_next_call}] or (C)onfirm all calls [{self.auto_confirm}] or retry (any other input)? ")
                    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
                else:
                    if rfc7807 is None:
                        raise
                    raise util.HTTPError(rfc7807.status, rfc7807.detail)
            except urllib.error.URLError as e:
                # Other urllib errors: Make exception message 'public' to give users a hint
                raise util.HTTPUnavailable(detail=str(e))

        return result

    def ievents(self, after=None, types=None, distribution=None, source=None, version=None, minimal_version=None, exit_on=None, fail_on=None):
        with contextlib.closing(self.endpoint.http_connect()) as conn:
            def ihttp_events():
                while True:
                    args = {"after": after.isoformat()} if after is not None else {}
                    conn.request("GET", config.URIS["events"]["attach"].join() + "?" + urllib.parse.urlencode(args))
                    response = conn.getresponse()
                    if response.status != 200:
                        raise util.HTTPError(response.status, response.reason)
                    yield events.Event.from_json(json.load(response))

            yield from events.ifilter(ihttp_events, types=types, distribution=distribution, source=source, version=version, minimal_version=minimal_version, exit_on=exit_on, fail_on=fail_on)

    def event(self, **kwargs):
        return next(self.ievents(**kwargs, exit_on=list(events.Type)))
