import sys
import traceback
from getpass import getpass

import collections
import jsonpickle
import requests
from functools import wraps
from klon_core.responses import invalid_request, NonJsonResponse


class TaskRunner:
    def on_start(self):
        """
        Called before the process is started.
        """
        pass

    def on_exit(self, task_result):
        """
        Called when the task is exited.

        :param task_result: the task's result
        :type task_result: TaskResult
        """
        if not task_result:
            task_result.print_stack_trace()

    def request(self, url, data=None, headers=None, stream=False, allow_connect_error=False):
        def call():
            resp = requests.post(url=url, data=data, headers=headers or dict(), stream=stream)
            if 'content-type' in resp.headers and resp.headers['content-type'].startswith("application/json"):
                return jsonpickle.decode(resp.text)
            return NonJsonResponse(resp)

        if allow_connect_error:
            try:
                return call()
            except (requests.exceptions.MissingSchema, requests.exceptions.ConnectionError):
                return invalid_request()

        else:
            return call()

    def exit(self):
        raise _TaskExitException()

    def prompt(self, msg):
        return prompt(msg)

    def prompt_pw(self, msg):
        return prompt_pw(msg)


class TaskResult:
    def __init__(self, error=None):
        self.error = error

    def __bool__(self):
        return self.error is None or self.user_exited()

    def user_exited(self):
        return isinstance(self.error, _TaskExitException) or \
               isinstance(self.error, KeyboardInterrupt)

    def got_network_error(self):
        return isinstance(self.error, requests.exceptions.MissingSchema) or \
               isinstance(self.error, requests.exceptions.ConnectionError)

    def got_error(self):
        return self.error is not None

    def print_stack_trace(self):
        trace = traceback.format_exception(etype=type(self.error), value=self.error, tb=self.error.__traceback__)
        print("".join(trace), file=sys.stderr, flush=True)


class _TaskExitException(Exception):
    pass


def task(f):
    @wraps(f)
    def decorated(runner, *args, **kwargs):
        try:
            runner.on_start()
            f(runner, *args, **kwargs)
            result = TaskResult()
        except Exception as e:
            result = TaskResult(e)
        runner.on_exit(result)
        return result
    return decorated


def prompt(msg):
    return input(msg).strip()


def prompt_pw(msg):
    return getpass(msg)


class Form:
    def __bool__(self):
        for obj in self.__dict__.values():
            if (isinstance(obj, FormValidator) or isinstance(obj, FormChoice)) and not obj:
                return False
        return True


class FormFilling:
    def __init__(self, input_func, forms):
        self._input_func = input_func
        self._forms = forms

    def _poll(self):
        self._input_func(self._forms)

    def _done(self):
        if isinstance(self._forms, collections.Iterable):
            return all([bool(form) for form in self._forms])
        return bool(self._forms)

    def _validate(self, validator_func, *args, **kwargs):
        for form in self._forms if isinstance(self._forms, collections.Iterable) else [self._forms]:
            for obj in form.__dict__.values():
                if isinstance(obj, FormValidator):
                    obj._reset()
            if validator_func is not None:
                try:
                    if isinstance(self._forms, collections.Iterable):
                        validator_func(form, *args, **kwargs)
                    else:
                        validator_func(*args, **kwargs)
                except _ValidationException:
                    pass

    def __call__(self, validator_func=None):
        def decorated(*args, **kwargs):
            while True:
                self._poll()
                self._validate(validator_func, *args, **kwargs)
                if self._done():
                    self._poll()
                    break

        decorated()
        return decorated


def fill_out_form(input_func, forms):
    FormFilling(input_func, forms)()


class FormParameter:
    def __init__(self, form):
        self._input = None
        self.is_static = False
        self._was_set = False
        self._validators = []
        self._form = form

    def set(self, value):
        self._input = value
        self._was_set = True
        self.is_static = False

    def set_static(self, value):
        self.set(value)
        self._was_set = True
        self.is_static = True

    def get(self):
        return self._input

    def _name(self):
        for k, v in self._form.__dict__.items():
            if v is self:
                return k

    def _add_validator(self, v):
        self._validators.append(v)

    def __bool__(self):
        return self._was_set and all(bool(v) for v in self._validators)


class FormValidator:
    def __init__(self, first_param, *rest_params):
        self._fails = False
        self._params = [first_param] + list(rest_params)
        for param in self._params:
            param._add_validator(self)
        self._form = first_param._form

    def fail(self):
        self._fails = True
        for param in self._params:
            if param.is_static:
                raise InvalidStaticParameterException(
                    validator_name=self._name(), param_name=param._name(), param_value=param.get()
                )
        raise _ValidationException()

    def _name(self):
        for k, v in self._form.__dict__.items():
            if v is self:
                return k

    def _reset(self):
        self._fails = False

    def __bool__(self):
        return not self._fails


class _ValidationException(Exception):
    pass


class InvalidStaticParameterException(Exception):
    def __init__(self, validator_name, param_name, param_value):
        super().__init__("form validator '{}' failed static set parameter '{}' with value '{}'"
                         .format(validator_name, param_name, param_value))
        self.validator_name = validator_name
        self.param_name = param_name


class FormCheckBox:
    def __init__(self, is_selected, is_active=True):
        self.is_active = is_active
        self._is_selected = is_selected

    def set(self, is_selected):
        if self.is_active:
            self._is_selected = is_selected

    def toggle(self):
        if self.is_active:
            self._is_selected = not self._is_selected

    def __bool__(self):
        return self._is_selected


class FormChoice:
    def __init__(self, default_option, *options):
        self._options = list(options) + [default_option]
        for opt in self._options:
            opt._set_choice(self)
        default_option.select()

    def _select(self, opt):
        for o in self._options:
            if o is not opt:
                o.selected = False

    def get(self):
        (choice,) = [opt for opt in self._options if opt.selected]
        return choice


class FormOption:
    def __init__(self):
        self.selected = False

    def select(self):
        self.selected = True
        self._choice._select(self)

    def _set_choice(self, choice):
        self._choice = choice
