# Exceptions can be put here to get them out of the way of the main flow of things,
# and because once in a while we may want them to be shared or to have shared parents.

import io

from .misc_utils import full_object_name, full_class_name, capitalize1, NamedObject, get_error_message
from .lang_utils import must_be_one_of


class KnownBugError(AssertionError):

    # One way or another, the initialization protocol will set these.
    jira_ticket = None
    bug_name = None

    def set_jira_ticket(self, jira_ticket):
        self.jira_ticket = jira_ticket
        self.bug_name = "a bug" if jira_ticket is None else "Bug %s" % jira_ticket

    def __init__(self, message):
        super(KnownBugError, self).__init__(message)
        if self.bug_name is None:
            self.set_jira_ticket(self.jira_ticket)


class UnfixedBugError(KnownBugError):
    pass


class WrongErrorSeen(UnfixedBugError):

    def __init__(self, *, expected_class, error_seen, jira_ticket=None):
        self.set_jira_ticket(jira_ticket)
        self.expected_class = expected_class
        self.error_seen = error_seen
        super().__init__("%s did not occur where expected."
                         " An error of class %s was thrown where one of class %s was expected."
                         " The bug may have been fixed or it may be manifesting in an unexpected way: %s"
                         % (capitalize1(self.bug_name),
                            full_class_name(error_seen),
                            full_object_name(expected_class),
                            error_seen))


class ExpectedErrorNotSeen(UnfixedBugError):

    def __init__(self, *, jira_ticket=None):
        self.set_jira_ticket(jira_ticket)
        super().__init__("%s did not occur where expected. It may have been fixed." % capitalize1(self.bug_name))


class FixedBugError(KnownBugError):
    pass


class WrongErrorSeenAfterFix(FixedBugError):

    def __init__(self, *, expected_class, error_seen, jira_ticket=None):
        self.set_jira_ticket(jira_ticket)
        self.expected_class = expected_class
        self.error_seen = error_seen
        super().__init__("%s did not occur,"
                         " but an error of class %s was thrown where one of class %s was expected to be fixed."
                         " This may be another bug showing through or a regression of some sort: %s"
                         % (capitalize1(self.bug_name),
                            full_class_name(error_seen),
                            full_object_name(expected_class),
                            error_seen))


class UnexpectedErrorAfterFix(FixedBugError):

    def __init__(self, *, expected_class, error_seen, jira_ticket=None):
        self.set_jira_ticket(jira_ticket)
        self.expected_class = expected_class
        self.error_seen = error_seen
        super().__init__("An error of class %s was thrown where we expected one of class %s was fixed."
                         " This may be a regression in the fix for %s: %s"
                         % (full_class_name(error_seen), full_object_name(expected_class), self.bug_name, error_seen))


class ConfigurationError(ValueError):
    pass


class SynonymousEnvironmentVariablesMismatched(ConfigurationError):

    def __init__(self, var1, val1, var2, val2):
        self.var1 = var1
        self.val1 = val1
        self.var2 = var2
        self.val2 = val2
        super().__init__("The environment variables {var1} and {var2} are synonyms but have inconsistent values:"
                         " If you supply values for both, they must be the same value."
                         " You supplied: {var1}={val1!r} {var2}={val2!r}"
                         .format(var1=var1, val1=val1, var2=var2, val2=val2))


class InferredBucketConflict(ConfigurationError):

    def __init__(self, *, kind, specified, inferred):
        self.kind = kind
        self.specified = specified
        self.inferred = inferred
        super().__init__("Specified {kind} bucket, {specified},"
                         " and {kind} bucket inferred from health page, {inferred}, do not match."
                         .format(kind=kind, specified=specified, inferred=inferred))


class CannotInferEnvFromNoGlobalEnvs(ConfigurationError):

    def __init__(self, *, global_bucket):
        self.global_bucket = global_bucket
        super().__init__("No envs were found in the global env bucket, {global_bucket}, so no env can be inferred."
                         .format(global_bucket=global_bucket))


class CannotInferEnvFromManyGlobalEnvs(ConfigurationError):

    def __init__(self, *, global_bucket, keys):
        self.global_bucket = global_bucket
        self.keys = keys
        super().__init__("Too many keys were found in the global env bucket, {global_bucket},"
                         " for a particular env to be inferred: {keys}"
                         .format(global_bucket=global_bucket, keys=keys))


class MissingGlobalEnv(ConfigurationError):

    def __init__(self, *, global_bucket, keys, env):
        self.global_bucket = global_bucket
        self.keys = keys
        self.env = env
        super().__init__("No matches for env {env} in bucket {global_bucket}; keys: {keys}"
                         .format(global_bucket=global_bucket, keys=keys, env=env))


class GlobalBucketAccessError(ConfigurationError):

    def __init__(self, *, global_bucket, status):
        self.global_bucket = global_bucket
        self.status = status
        super().__init__("Could not access global env bucket {global_bucket}: status: {status}"
                         .format(global_bucket=global_bucket, status=status))


class MissingFoursightBucketTable(ConfigurationError):
    pass


class IncompleteFoursightBucketTable(ConfigurationError):
    pass


class InvalidParameterError(ValueError):

    SUPPRESSED = NamedObject('suppressed')
    QUOTE_OPTIONS = True
    VALIDITY_WORD = "valid"
    VALID_OPTIONS = None

    def compute_valid_options(self):
        return self.VALID_OPTIONS

    def _defaulted_options(self, options):
        # The options argument takes precedence if both are defined
        options = self.compute_valid_options() if options is None else options
        if options is None:
            options = []
        return options

    def __init__(self, parameter=None, value=SUPPRESSED, options=None, message=None):
        options = self._defaulted_options(options)
        self.parameter = parameter
        self.value = value
        self.options = options
        self.message = message

        if message is None:
            value_part = ""
            if value is not self.SUPPRESSED:
                if self.QUOTE_OPTIONS:
                    value = repr(str(value))
                value_part = f", {value},"
            valid = self.VALIDITY_WORD
            restriction = must_be_one_of(options, possible=valid, kind='value', quote=self.QUOTE_OPTIONS)
            parameter_part = ""
            if parameter is not None:
                parameter_part = f" of {parameter}"
            message = f"The value{parameter_part}{value_part} was not {valid}. {restriction}"

        super().__init__(message)


class EnvUtilsLoadError(ValueError):

    def __init__(self, msg, *, env_bucket, config_key, encapsulated_error):
        self.env_bucket = env_bucket
        self.config_key = config_key
        self.encapsulated_error = encapsulated_error
        super().__init__(f"{msg} (env_bucket={env_bucket}, config={config_key})")


class AppKeyMissing(RuntimeError):

    def __init__(self, context, key_manager):
        self.context = context
        self.key_manager = key_manager
        super().__init__(f"Missing credential in file {key_manager.keys_file} for {context}.")


class AppEnvKeyMissing(AppKeyMissing):

    def __init__(self, env, key_manager):
        self.env = env
        super().__init__(context=f"{key_manager.APP_NAME} environment {env}",
                         key_manager=key_manager)


class AppServerKeyMissing(AppKeyMissing):

    def __init__(self, server, key_manager):
        self.server = server
        super().__init__(context=f"{key_manager.APP_NAME} server {server}",
                         key_manager=key_manager)


class NotUsingBeanstalksAnyMore(Exception):
    pass


class NotBeanstalkEnvironment(Exception):

    def __init__(self, message=None, *, envname):
        self.envname = envname
        super().__init__(message or f"Not a beanstalk environment: {envname}")


class BeanstalkOperationNotImplemented(NotImplementedError):
    def __init__(self, operation, message=None):
        self.operation = operation
        super().__init__(message
                         or f"Attempt to use an obsolete beanstalk operation {operation} in a container environment.")


class LegacyDispatchDisabled(Exception):
    def __init__(self, operation, call_args=None, call_kwargs=None, mode='dispatch', message=None):
        self.operation = operation
        self.call_args = call_args
        self.call_kwargs = call_kwargs
        self.mode = mode
        super().__init__(message
                         or f"Attempt to use legacy operation {operation}"
                            f" with args={call_args} kwargs={call_kwargs} mode={mode}.")


class MultiError(Exception):

    def __init__(self, *errors, flatten=True):
        self.errors = []
        for error in errors:
            if flatten and isinstance(error, MultiError):
                self.errors.extend(error.errors)
            else:
                self.errors.append(error)
        s = io.StringIO()
        s.write("Multiple errors:")
        for error in self.errors:
            s.write(f"\n  {get_error_message(error)}")
        self.message = s.getvalue()

    def __str__(self):
        return self.message

    def __repr__(self):
        return f"{self.__class__.__name__}({','.join(map(repr, self.errors))})"
