# -*- coding: utf-8 -*-

from enum import Enum

import pytz
import six
from brewtils.errors import ModelError, _deprecate

__all__ = [
    "BaseModel",
    "System",
    "Instance",
    "Command",
    "Parameter",
    "Request",
    "PatchOperation",
    "Choices",
    "LoggingConfig",
    "Event",
    "Events",
    "Queue",
    "Principal",
    "Role",
    "RefreshToken",
    "Job",
    "RequestFile",
    "File",
    "FileChunk",
    "FileStatus",
    "RequestTemplate",
    "DateTrigger",
    "CronTrigger",
    "IntervalTrigger",
    "FileTrigger",
    "Garden",
    "Operation",
    "Resolvable",
]


class Events(Enum):
    BREWVIEW_STARTED = 1
    BREWVIEW_STOPPED = 2
    BARTENDER_STARTED = 3
    BARTENDER_STOPPED = 4
    REQUEST_CREATED = 5
    REQUEST_STARTED = 6
    REQUEST_UPDATED = 22
    REQUEST_COMPLETED = 7
    REQUEST_CANCELED = 42
    INSTANCE_INITIALIZED = 8
    INSTANCE_STARTED = 9
    INSTANCE_UPDATED = 23
    INSTANCE_STOPPED = 10
    SYSTEM_CREATED = 11
    SYSTEM_UPDATED = 12
    SYSTEM_REMOVED = 13
    QUEUE_CLEARED = 14
    ALL_QUEUES_CLEARED = 15
    DB_CREATE = 16
    DB_UPDATE = 17
    DB_DELETE = 18
    GARDEN_CREATED = 19
    GARDEN_UPDATED = 20
    GARDEN_REMOVED = 21
    FILE_CREATED = 24
    GARDEN_STARTED = 25
    GARDEN_STOPPED = 26
    GARDEN_UNREACHABLE = 27
    GARDEN_ERROR = 28
    GARDEN_NOT_CONFIGURED = 29
    GARDEN_SYNC = 30
    ENTRY_STARTED = 31
    ENTRY_STOPPED = 32
    JOB_CREATED = 33
    JOB_DELETED = 34
    JOB_PAUSED = 35
    JOB_RESUMED = 36
    PLUGIN_LOGGER_FILE_CHANGE = 37
    RUNNER_STARTED = 38
    RUNNER_STOPPED = 39
    RUNNER_REMOVED = 40
    JOB_UPDATED = 41

    # Next: 43


class BaseModel(object):
    schema = None


class Command(BaseModel):
    schema = "CommandSchema"

    COMMAND_TYPES = ("ACTION", "INFO", "EPHEMERAL", "ADMIN")
    OUTPUT_TYPES = ("STRING", "JSON", "XML", "HTML", "JS", "CSS")

    def __init__(
        self,
        name=None,
        description=None,
        parameters=None,
        command_type=None,
        output_type=None,
        schema=None,
        form=None,
        template=None,
        icon_name=None,
        hidden=False,
        metadata=None,
    ):
        self.name = name
        self.description = description
        self.parameters = parameters or []
        self.command_type = command_type
        self.output_type = output_type
        self.schema = schema
        self.form = form
        self.template = template
        self.icon_name = icon_name
        self.hidden = hidden
        self.metadata = metadata or {}

    def __str__(self):
        return self.name

    def __repr__(self):
        return "<Command: %s>" % self.name

    def parameter_keys(self):
        """Get a list of all Parameter keys

        Returns:
            list[str]: A list containing each Parameter's key attribute
        """
        return [p.key for p in self.parameters]

    def parameter_keys_by_type(self, desired_type):
        """Get a list of all Parameter keys, filtered by Parameter type

        Args:
            desired_type (str): Parameter type

        Returns:
            list[str]: A list containing matching Parameters' key attribute
        """
        keys = []
        for param in self.parameters:
            key = param.keys_by_type(desired_type)
            if key:
                keys.append(key)
        return keys

    def get_parameter_by_key(self, key):
        """Lookup a Parameter using a given key

        Args:
            key (str): The Parameter key to use

        Returns:
            Parameter (Optional): A Parameter with the given key

            If a Parameter with the given key does not exist None will be returned.
        """
        for parameter in self.parameters:
            if parameter.key == key:
                return parameter

        return None

    def has_different_parameters(self, parameters):
        """Determine if parameters differ from the current parameters

        Args:
            parameters (Sequence[Parameter]): Parameter collection for comparison

        Returns:
            bool: True if the given Parameters differ, False if they are identical
        """
        if len(parameters) != len(self.parameters):
            return True

        for parameter in parameters:
            if parameter.key not in self.parameter_keys():
                return True

            current_param = self.get_parameter_by_key(parameter.key)
            if current_param.is_different(parameter):
                return True

        return False


class Instance(BaseModel):
    schema = "InstanceSchema"

    INSTANCE_STATUSES = {
        "INITIALIZING",
        "RUNNING",
        "PAUSED",
        "STOPPED",
        "DEAD",
        "UNRESPONSIVE",
        "STARTING",
        "STOPPING",
        "UNKNOWN",
    }

    def __init__(
        self,
        name=None,
        description=None,
        id=None,
        status=None,
        status_info=None,
        queue_type=None,
        queue_info=None,
        icon_name=None,
        metadata=None,
    ):
        self.name = name
        self.description = description
        self.id = id
        self.status = status.upper() if status else None
        self.status_info = status_info or {}
        self.queue_type = queue_type
        self.queue_info = queue_info or {}
        self.icon_name = icon_name
        self.metadata = metadata or {}

    def __str__(self):
        return self.name

    def __repr__(self):
        return "<Instance: name=%s, status=%s>" % (self.name, self.status)


class Choices(BaseModel):
    schema = "ChoicesSchema"

    TYPES = ("static", "url", "command")
    DISPLAYS = ("select", "typeahead")

    def __init__(self, type=None, display=None, value=None, strict=None, details=None):
        self.type = type
        self.strict = strict
        self.value = value
        self.display = display
        self.details = details or {}

    def __str__(self):
        return self.value.__str__()

    def __repr__(self):
        return "<Choices: type=%s, display=%s, value=%s>" % (
            self.type,
            self.display,
            self.value,
        )


class Parameter(BaseModel):
    schema = "ParameterSchema"

    TYPES = (
        "String",
        "Integer",
        "Float",
        "Boolean",
        "Any",
        "Dictionary",
        "Date",
        "DateTime",
        "Bytes",
        "Base64",
    )
    FORM_INPUT_TYPES = ("textarea",)

    def __init__(
        self,
        key=None,
        type=None,
        multi=None,
        display_name=None,
        optional=None,
        default=None,
        description=None,
        choices=None,
        parameters=None,
        nullable=None,
        maximum=None,
        minimum=None,
        regex=None,
        form_input_type=None,
        type_info=None,
        is_kwarg=None,
        model=None,
    ):
        self.key = key
        self.type = type
        self.multi = multi
        self.display_name = display_name
        self.optional = optional
        self.default = default
        self.description = description
        self.choices = choices
        self.parameters = parameters or []
        self.nullable = nullable
        self.maximum = maximum
        self.minimum = minimum
        self.regex = regex
        self.form_input_type = form_input_type
        self.type_info = type_info or {}

        # These are special - they aren't part of the Parameter "API" (they aren't in
        # the serialization schema) but we still need them on this model for consistency
        # when creating Clients - https://github.com/beer-garden/beer-garden/issues/777
        self.is_kwarg = is_kwarg
        self.model = model

    def __str__(self):
        return self.key

    def __repr__(self):
        return "<Parameter: key=%s, type=%s, description=%s>" % (
            self.key,
            self.type,
            self.description,
        )

    def keys_by_type(self, desired_type):
        """Gets all keys by the specified type.

        Since parameters can be nested, this method will also return all keys of all
        nested parameters. The return value is a possibly nested list, where the first
        value of each list is going to be a string, while the next value is a list.

        Args:
            desired_type (str): Desired type

        Returns:
            An empty list if the type does not exist, otherwise it will be a list
            containing at least one entry which is a string, each subsequent entry is a
            nested list with the same structure.
        """
        keys = []
        if self.type == desired_type:
            keys.append(self.key)

        if not self.parameters:
            return keys

        for param in self.parameters:
            nested_keys = param.keys_by_type(desired_type)
            if nested_keys:
                if not keys:
                    keys = [self.key]

                keys.append(nested_keys)
        return keys

    def is_different(self, other):
        if not type(other) is type(self):
            return True

        fields_to_compare = [
            "key",
            "type",
            "type_info",
            "multi",
            "optional",
            "default",
            "nullable",
            "maximum",
            "minimum",
            "regex",
        ]
        for field in fields_to_compare:
            if getattr(self, field) != getattr(other, field):
                return True

        if len(self.parameters) != len(other.parameters):
            return True

        parameter_keys = [p.key for p in self.parameters]
        for parameter in other.parameters:
            if parameter.key not in parameter_keys:
                return True

            current_param = list(
                filter((lambda p: p.key == parameter.key), self.parameters)
            )[0]
            if current_param.is_different(parameter):
                return True

        return False


class RequestFile(BaseModel):
    schema = "RequestFileSchema"

    def __init__(self, storage_type=None, filename=None, id=None):
        self.storage_type = storage_type
        self.filename = filename
        self.id = id

    def __str__(self):
        return self.filename

    def __repr__(self):
        return "<RequestFile: filename=%s, storage_type=%s>" % (
            self.filename,
            self.storage_type,
        )


class File(BaseModel):
    schema = "FileSchema"

    def __init__(
        self,
        id=None,
        owner_id=None,
        owner_type=None,
        updated_at=None,
        file_name=None,
        file_size=None,
        chunks=None,
        chunk_size=None,
        owner=None,
        job=None,
        request=None,
    ):
        self.id = id
        self.owner_id = owner_id
        self.owner_type = owner_type
        self.owner = owner
        self.job = job
        self.request = request
        self.updated_at = updated_at
        self.file_name = file_name
        self.file_size = file_size
        self.chunks = chunks
        self.chunk_size = chunk_size

    def __str__(self):
        return self.file_name

    def __repr__(self):
        return "<File: id=%s, file_name=%s, owner_id=%s>" % (
            self.id,
            self.file_name,
            self.owner_id,
        )


class FileChunk(BaseModel):
    schema = "FileChunkSchema"

    def __init__(self, id=None, file_id=None, offset=None, data=None, owner=None):
        self.id = id
        self.file_id = file_id
        self.offset = offset
        self.data = data
        self.owner = owner

    def __str__(self):
        return self.data

    def __repr__(self):
        return "<FileChunk: file_id=%s, offset=%s>" % (self.file_id, self.offset)


class FileStatus(BaseModel):
    schema = "FileStatusSchema"

    def __init__(
        self,
        owner_id=None,
        owner_type=None,
        updated_at=None,
        file_name=None,
        file_size=None,
        chunks=None,
        chunk_size=None,
        chunk_id=None,
        file_id=None,
        offset=None,
        data=None,
        valid=None,
        missing_chunks=None,
        expected_max_size=None,
        size_ok=None,
        expected_number_of_chunks=None,
        number_of_chunks=None,
        chunks_ok=None,
        operation_complete=None,
        message=None,
    ):
        # Top-level file info
        self.file_id = file_id
        self.file_name = file_name
        self.file_size = file_size
        self.updated_at = updated_at
        self.chunk_size = chunk_size
        self.chunks = chunks
        self.owner_id = owner_id
        self.owner_type = owner_type
        # Chunk info
        self.chunk_id = chunk_id
        self.offset = offset
        self.data = data
        # Validation metadata
        self.valid = valid
        self.missing_chunks = missing_chunks
        self.expected_number_of_chunks = expected_number_of_chunks
        self.expected_max_size = expected_max_size
        self.number_of_chunks = number_of_chunks
        self.size_ok = size_ok
        self.chunks_ok = chunks_ok
        self.operation_complete = operation_complete
        self.message = message

    def __str__(self):
        return "%s" % self.__dict__

    def __repr__(self):
        return "<FileStatus: %s>" % self.__dict__


class RequestTemplate(BaseModel):
    schema = "RequestTemplateSchema"

    TEMPLATE_FIELDS = [
        "system",
        "system_version",
        "instance_name",
        "namespace",
        "command",
        "command_type",
        "parameters",
        "comment",
        "metadata",
        "output_type",
    ]

    def __init__(
        self,
        system=None,
        system_version=None,
        instance_name=None,
        namespace=None,
        command=None,
        command_type=None,
        parameters=None,
        comment=None,
        metadata=None,
        output_type=None,
    ):
        self.system = system
        self.system_version = system_version
        self.instance_name = instance_name
        self.namespace = namespace
        self.command = command
        self.command_type = command_type
        self.parameters = parameters
        self.comment = comment
        self.metadata = metadata or {}
        self.output_type = output_type

    def __str__(self):
        return self.command

    def __repr__(self):
        return (
            "<RequestTemplate: command=%s, system=%s, system_version=%s, "
            "instance_name=%s, namespace=%s>"
            % (
                self.command,
                self.system,
                self.system_version,
                self.instance_name,
                self.namespace,
            )
        )


class Request(RequestTemplate):
    schema = "RequestSchema"

    STATUS_LIST = (
        "CREATED",
        "RECEIVED",
        "IN_PROGRESS",
        "CANCELED",
        "SUCCESS",
        "ERROR",
        "INVALID",
    )
    COMPLETED_STATUSES = ("CANCELED", "SUCCESS", "ERROR", "INVALID")
    COMMAND_TYPES = ("ACTION", "INFO", "EPHEMERAL", "ADMIN")
    OUTPUT_TYPES = ("STRING", "JSON", "XML", "HTML", "JS", "CSS")

    def __init__(
        self,
        system=None,
        system_version=None,
        instance_name=None,
        namespace=None,
        command=None,
        id=None,
        parent=None,
        children=None,
        parameters=None,
        comment=None,
        output=None,
        output_type=None,
        status=None,
        command_type=None,
        created_at=None,
        error_class=None,
        metadata=None,
        hidden=None,
        updated_at=None,
        has_parent=None,
        requester=None,
    ):
        super(Request, self).__init__(
            system=system,
            system_version=system_version,
            instance_name=instance_name,
            namespace=namespace,
            command=command,
            command_type=command_type,
            parameters=parameters,
            comment=comment,
            metadata=metadata,
            output_type=output_type,
        )
        self.id = id
        self.parent = parent
        self.children = children
        self.output = output
        self._status = status
        self.hidden = hidden
        self.created_at = created_at
        self.updated_at = updated_at
        self.error_class = error_class
        self.has_parent = has_parent
        self.requester = requester

    @classmethod
    def from_template(cls, template, **kwargs):
        """Create a Request instance from a RequestTemplate

        Args:
            template: The RequestTemplate to use
            **kwargs: Optional overrides to use in place of the template's attributes

        Returns:
            The new Request instance
        """
        request_params = {
            k: kwargs.get(k, getattr(template, k))
            for k in RequestTemplate.TEMPLATE_FIELDS
        }
        return Request(**request_params)

    def __repr__(self):
        return (
            "<Request: command=%s, status=%s, system=%s, system_version=%s, "
            "instance_name=%s, namespace=%s>"
            % (
                self.command,
                self.status,
                self.system,
                self.system_version,
                self.instance_name,
                self.namespace,
            )
        )

    @property
    def status(self):
        return self._status

    @status.setter
    def status(self, value):
        self._status = value

    @property
    def is_ephemeral(self):
        return self.command_type and self.command_type.upper() == "EPHEMERAL"

    @property
    def is_json(self):
        return self.output_type and self.output_type.upper() == "JSON"


class System(BaseModel):
    schema = "SystemSchema"

    def __init__(
        self,
        name=None,
        description=None,
        version=None,
        id=None,
        max_instances=None,
        instances=None,
        commands=None,
        icon_name=None,
        display_name=None,
        metadata=None,
        namespace=None,
        local=None,
        template=None,
    ):
        self.name = name
        self.description = description
        self.version = version
        self.id = id
        self.max_instances = max_instances
        self.instances = instances or []
        self.commands = commands or []
        self.icon_name = icon_name
        self.display_name = display_name
        self.metadata = metadata or {}
        self.namespace = namespace
        self.local = local
        self.template = template

    def __str__(self):
        return "%s:%s-%s" % (self.namespace, self.name, self.version)

    def __repr__(self):
        return "<System: name=%s, version=%s, namespace=%s>" % (
            self.name,
            self.version,
            self.namespace,
        )

    @property
    def instance_names(self):
        return [i.name for i in self.instances]

    def has_instance(self, name):
        """Determine if an instance currently exists in the system

        Args:
            name (str): The instance name

        Returns:
            bool: True if an instance with the given name exists, False otherwise
        """
        return name in self.instance_names

    def get_instance_by_name(self, name, raise_missing=False):
        """Get an instance that currently exists in the system

        Args:
            name (str): The instance name
            raise_missing (bool): If True, raise an exception if an Instance with the
            given name is not found. If False, will return None in that case.

        Returns:
            Instance: The instance if it exists, None otherwise

        Raises:
            ModelError: Instance was not found and raise_missing=True
        """
        for instance in self.instances:
            if instance.name == name:
                return instance

        if raise_missing:
            raise ModelError("Instance not found")

        return None

    def get_instance_by_id(self, id, raise_missing=False):
        """Get an instance that currently exists in the system

        Args:
            id (str): The instance id
            raise_missing (bool): If True, raise an exception if an Instance with the
            given id is not found. If False, will return None in that case.

        Returns:
            Instance: The instance if it exists, None otherwise

        Raises:
            ModelError: Instance was not found and raise_missing=True
        """
        for instance in self.instances:
            if instance.id == id:
                return instance

        if raise_missing:
            raise ModelError("Instance not found")

        return None

    def get_instance(self, name):
        """
        .. deprecated::3.0
           Will be removed in 4.0. Use ``get_instance_by_name`` instead
        """
        _deprecate(
            "Heads up! This method is deprecated, please use get_instance_by_name"
        )
        return self.get_instance_by_name(name)

    def get_command_by_name(self, command_name):
        """Retrieve a particular command from the system

        Args:
            command_name (str): The command name

        Returns:
            Command: The command if it exists, None otherwise
        """
        for command in self.commands:
            if command.name == command_name:
                return command

        return None

    def has_different_commands(self, commands):
        """Check if a set of commands is different than the current commands

        Args:
            commands (Sequence[Command]): Command collection for comparison

        Returns:
            bool: True if the given Commands differ, False if they are identical
        """
        if len(commands) != len(self.commands):
            return True

        for command in commands:
            if command.name not in [c.name for c in self.commands]:
                return True

            current_command = self.get_command_by_name(command.name)

            if current_command.has_different_parameters(command.parameters):
                return True

        return False


class PatchOperation(BaseModel):
    schema = "PatchSchema"

    def __init__(self, operation=None, path=None, value=None):
        self.operation = operation
        self.path = path
        self.value = value

    def __str__(self):
        return "%s, %s, %s" % (self.operation, self.path, self.value)

    def __repr__(self):
        return "<Patch: operation=%s, path=%s, value=%s>" % (
            self.operation,
            self.path,
            self.value,
        )


class LoggingConfig(BaseModel):
    schema = "LoggingConfigSchema"

    LEVELS = ("DEBUG", "INFO", "WARN", "ERROR")
    SUPPORTED_HANDLERS = ("stdout", "file", "logstash")

    DEFAULT_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
    DEFAULT_HANDLER = {
        "class": "logging.StreamHandler",
        "stream": "ext::/sys.stdout",
        "formatter": "default",
    }

    def __init__(self, level=None, handlers=None, formatters=None, loggers=None):
        self.level = level
        self.handlers = handlers
        self.formatters = formatters
        self._loggers = loggers or {}

    @property
    def handler_names(self):
        if self.handlers:
            return set(self.handlers)
        else:
            return None

    @property
    def formatter_names(self):
        if self.formatters:
            return set(self.formatters)
        else:
            return None

    def get_plugin_log_config(self, **kwargs):
        """Get a specific plugin logging configuration.

        It is possible for different systems to have different logging configurations.
        This method will create the correct plugin logging configuration and return it.
        If a specific logger is not found for a system, then the current logging
        configuration will be returned.

        Keyword Args:
            Identifying information for a system (i.e. system_name)

        Returns:
            The logging configuration for this system
        """
        system_name = kwargs.pop("system_name", None)
        specific_logger = self._loggers.get(system_name, {})

        # If there is no specific logger, then we simply return this object
        # otherwise, we need to construct a new LoggingConfig object with
        # the overrides given in the logger.
        if not specific_logger:
            return self

        level = specific_logger.get("level", self.level)
        handlers = self._generate_handlers(specific_logger.get("handlers"))
        formatters = self._generate_formatters(specific_logger.get("formatters", {}))

        return LoggingConfig(level=level, handlers=handlers, formatters=formatters)

    def _generate_handlers(self, specific_handlers):

        # If we are not given an override for handlers, then we will just
        # assume that we want to use all the handlers given in the current
        # configuration.
        if not specific_handlers:
            return self.handlers

        if isinstance(specific_handlers, list):
            handlers = {}
            for handler_name in specific_handlers:
                handlers[handler_name] = self.handlers[handler_name]
        else:
            return specific_handlers

        return handlers

    def _generate_formatters(self, specific_formatters):

        # If we are not given an override for formatters, then we will just
        # assume that we want to use the formatters given in the current
        # configuration
        if not specific_formatters:
            return self.formatters

        # In case no formatter is provided, we always want a default.
        formatters = {"default": {"format": self.DEFAULT_FORMAT}}
        for formatter_name, format_str in six.iteritems(specific_formatters):
            formatters[formatter_name] = {"format": format_str}

        return formatters

    def __str__(self):
        return "%s, %s, %s" % (self.level, self.handler_names, self.formatter_names)

    def __repr__(self):
        return "<LoggingConfig: level=%s, handlers=%s, formatters=%s" % (
            self.level,
            self.handler_names,
            self.formatter_names,
        )


class Event(BaseModel):
    schema = "EventSchema"

    def __init__(
        self,
        name=None,
        namespace=None,
        garden=None,
        metadata=None,
        timestamp=None,
        payload_type=None,
        payload=None,
        error=None,
        error_message=None,
    ):
        self.name = name
        self.namespace = namespace
        self.garden = garden
        self.metadata = metadata or {}
        self.timestamp = timestamp
        self.payload_type = payload_type
        self.payload = payload
        self.error = error
        self.error_message = error_message

    def __str__(self):
        return "%s: %s" % (self.namespace, self.name)

    def __repr__(self):
        return (
            "<Event: namespace=%s, garden=%s, name=%s, timestamp=%s, error=%s, "
            "error_message=%s, metadata=%s, payload_type=%s, payload=%r>"
            % (
                self.namespace,
                self.garden,
                self.name,
                self.timestamp,
                self.error,
                self.error_message,
                self.metadata,
                self.payload_type,
                self.payload,
            )
        )


class Queue(BaseModel):
    schema = "QueueSchema"

    def __init__(
        self,
        name=None,
        system=None,
        version=None,
        instance=None,
        system_id=None,
        display=None,
        size=None,
    ):
        self.name = name
        self.system = system
        self.version = version
        self.instance = instance
        self.system_id = system_id
        self.display = display
        self.size = size

    def __str__(self):
        return "%s: %s" % (self.name, self.size)

    def __repr__(self):
        return "<Queue: name=%s, size=%s>" % (self.name, self.size)


class Principal(BaseModel):
    schema = "PrincipalSchema"

    def __init__(
        self,
        id=None,
        username=None,
        roles=None,
        permissions=None,
        preferences=None,
        metadata=None,
    ):
        self.id = id
        self.username = username
        self.roles = roles
        self.permissions = permissions
        self.preferences = preferences
        self.metadata = metadata

    def __str__(self):
        return "%s" % self.username

    def __repr__(self):
        return "<Principal: username=%s, roles=%s, permissions=%s>" % (
            self.username,
            self.roles,
            self.permissions,
        )


class Role(BaseModel):
    schema = "RoleSchema"

    def __init__(self, id=None, name=None, description=None, permissions=None):
        self.id = id
        self.name = name
        self.description = description
        self.permissions = permissions

    def __str__(self):
        return "%s" % self.name

    def __repr__(self):
        return "<Role: name=%s, permissions=%s>" % (self.name, self.permissions)


class RefreshToken(BaseModel):
    schema = "RefreshTokenSchema"

    def __init__(self, id=None, issued=None, expires=None, payload=None):
        self.id = id
        self.issued = issued
        self.expires = expires
        self.payload = payload or {}

    def __str__(self):
        return "%s" % self.payload

    def __repr__(self):
        return "<RefreshToken: issued=%s, expires=%s, payload=%s>" % (
            self.issued,
            self.expires,
            self.payload,
        )


class Job(BaseModel):
    TRIGGER_TYPES = {"interval", "date", "cron", "file"}
    STATUS_TYPES = {"RUNNING", "PAUSED"}
    schema = "JobSchema"

    def __init__(
        self,
        id=None,
        name=None,
        trigger_type=None,
        trigger=None,
        request_template=None,
        misfire_grace_time=None,
        coalesce=None,
        next_run_time=None,
        success_count=None,
        error_count=None,
        status=None,
        max_instances=None,
        timeout=None,
    ):
        self.id = id
        self.name = name
        self.trigger_type = trigger_type
        self.trigger = trigger
        self.request_template = request_template
        self.misfire_grace_time = misfire_grace_time
        self.coalesce = coalesce
        self.next_run_time = next_run_time
        self.success_count = success_count
        self.error_count = error_count
        self.status = status
        self.max_instances = max_instances
        self.timeout = timeout

    def __str__(self):
        return "%s: %s" % (self.name, self.id)

    def __repr__(self):
        return "<Job: name=%s, id=%s>" % (self.name, self.id)


class DateTrigger(BaseModel):
    schema = "DateTriggerSchema"

    def __init__(self, run_date=None, timezone=None):
        self.run_date = run_date
        self.timezone = timezone

    def __str__(self):
        return repr(self)

    def __repr__(self):
        return "<DateTrigger: run_date=%s>" % self.run_date

    @property
    def scheduler_attributes(self):
        return ["run_date", "timezone"]

    @property
    def scheduler_kwargs(self):
        tz = pytz.timezone(self.timezone)

        return {"timezone": tz, "run_date": tz.localize(self.run_date)}


class IntervalTrigger(BaseModel):
    schema = "IntervalTriggerSchema"

    def __init__(
        self,
        weeks=None,
        days=None,
        hours=None,
        minutes=None,
        seconds=None,
        start_date=None,
        end_date=None,
        timezone=None,
        jitter=None,
        reschedule_on_finish=None,
    ):
        self.weeks = weeks
        self.days = days
        self.hours = hours
        self.minutes = minutes
        self.seconds = seconds
        self.start_date = start_date
        self.end_date = end_date
        self.timezone = timezone
        self.jitter = jitter
        self.reschedule_on_finish = reschedule_on_finish

    def __str__(self):
        return repr(self)

    def __repr__(self):
        return (
            "<IntervalTrigger: weeks=%d, days=%d, hours=%d, "
            "minutes=%d, seconds=%d>"
            % (self.weeks, self.days, self.hours, self.minutes, self.seconds)
        )

    @property
    def scheduler_attributes(self):
        return [
            "weeks",
            "days",
            "hours",
            "minutes",
            "seconds",
            "start_date",
            "end_date",
            "timezone",
            "jitter",
            "reschedule_on_finish",
        ]

    @property
    def scheduler_kwargs(self):
        tz = pytz.timezone(self.timezone)

        kwargs = {key: getattr(self, key) for key in self.scheduler_attributes}
        kwargs.update(
            {
                "timezone": tz,
                "start_date": tz.localize(self.start_date) if self.start_date else None,
                "end_date": tz.localize(self.end_date) if self.end_date else None,
            }
        )

        return kwargs


class CronTrigger(BaseModel):
    schema = "CronTriggerSchema"

    def __init__(
        self,
        year=None,
        month=None,
        day=None,
        week=None,
        day_of_week=None,
        hour=None,
        minute=None,
        second=None,
        start_date=None,
        end_date=None,
        timezone=None,
        jitter=None,
    ):
        self.year = year
        self.month = month
        self.day = day
        self.week = week
        self.day_of_week = day_of_week
        self.hour = hour
        self.minute = minute
        self.second = second
        self.start_date = start_date
        self.end_date = end_date
        self.timezone = timezone
        self.jitter = jitter

    def __str__(self):
        return repr(self)

    def __repr__(self):
        return "<CronTrigger: %s %s %s %s %s>" % (
            self.minute,
            self.hour,
            self.day,
            self.month,
            self.day,
        )

    @property
    def scheduler_attributes(self):
        return [
            "year",
            "month",
            "day",
            "week",
            "day_of_week",
            "hour",
            "minute",
            "second",
            "start_date",
            "end_date",
            "timezone",
            "jitter",
        ]

    @property
    def scheduler_kwargs(self):
        tz = pytz.timezone(self.timezone)

        kwargs = {key: getattr(self, key) for key in self.scheduler_attributes}
        kwargs.update(
            {
                "timezone": tz,
                "start_date": tz.localize(self.start_date) if self.start_date else None,
                "end_date": tz.localize(self.end_date) if self.end_date else None,
            }
        )

        return kwargs


class FileTrigger(BaseModel):
    schema = "FileTriggerSchema"

    def __init__(self, pattern=None, path=None, recursive=None, callbacks=None):
        self.pattern = pattern
        self.path = path
        self.recursive = recursive
        self.callbacks = callbacks

    def __str__(self):
        return repr(self)

    def __repr__(self):
        return "<FileTrigger: %s %s %s %s>" % (
            self.pattern,
            self.path,
            self.recursive,
            self.callbacks,
        )

    @property
    def scheduler_attributes(self):
        return ["pattern", "path", "recursive", "callbacks"]

    @property
    def scheduler_kwargs(self):
        kwargs = {key: getattr(self, key) for key in self.scheduler_attributes}
        kwargs.update(
            {
                "pattern": self.pattern,
                "path": self.path,
                "recursive": self.recursive,
                "callbacks": self.callbacks,
            }
        )

        return kwargs


class Garden(BaseModel):
    schema = "GardenSchema"

    GARDEN_STATUSES = {
        "INITIALIZING",
        "RUNNING",
        "BLOCKED",
        "STOPPED",
        "NOT_CONFIGURED",
        "UNREACHABLE",
        "ERROR",
        "UNKNOWN",
    }

    def __init__(
        self,
        id=None,
        name=None,
        status=None,
        status_info=None,
        namespaces=None,
        systems=None,
        connection_type=None,
        connection_params=None,
    ):
        self.id = id
        self.name = name
        self.status = status.upper() if status else None
        self.status_info = status_info or {}
        self.namespaces = namespaces or []
        self.systems = systems or []

        self.connection_type = connection_type
        self.connection_params = connection_params

    def __str__(self):
        return "%s" % self.name

    def __repr__(self):
        return "<Garden: garden_name=%s, status=%s>" % (self.name, self.status)


class Operation(BaseModel):
    schema = "OperationSchema"

    def __init__(
        self,
        model=None,
        model_type=None,
        args=None,
        kwargs=None,
        target_garden_name=None,
        source_garden_name=None,
        operation_type=None,
    ):
        self.model = model
        self.model_type = model_type
        self.args = args or []
        self.kwargs = kwargs or {}
        self.target_garden_name = target_garden_name
        self.source_garden_name = source_garden_name
        self.operation_type = operation_type

    def __str__(self):
        return "%s" % self.operation_type

    def __repr__(self):
        return (
            "<Operation: operation_type=%s, source_garden_name=%s, "
            "target_garden_name=%s, model_type=%s, model=%s, args=%s, kwargs=%s>"
            % (
                self.operation_type,
                self.source_garden_name,
                self.target_garden_name,
                self.model_type,
                self.model,
                self.args,
                self.kwargs,
            )
        )


class Runner(BaseModel):
    schema = "RunnerSchema"

    def __init__(
        self,
        id=None,
        name=None,
        path=None,
        instance_id=None,
        stopped=None,
        dead=None,
        restart=None,
    ):
        self.id = id
        self.name = name
        self.path = path
        self.instance_id = instance_id
        self.stopped = stopped
        self.dead = dead
        self.restart = restart

    def __str__(self):
        return "%s" % self.name

    def __repr__(self):
        return (
            "<Runner: id=%s, name=%s, path=%s, instance_id=%s, stopped=%s, dead=%s, "
            "restart=%s>"
            % (
                self.id,
                self.name,
                self.path,
                self.instance_id,
                self.stopped,
                self.dead,
                self.restart,
            )
        )


class Resolvable(BaseModel):
    schema = "ResolvableSchema"

    # Resolvable parameter types
    TYPES = ("Base64", "Bytes")

    def __init__(self, id=None, type=None, storage=None, details=None):
        self.id = id
        self.type = type
        self.storage = storage
        self.details = details or {}

    def __str__(self):
        return "%s: %s %s" % (self.id, self.type, self.storage)

    def __repr__(self):
        return "<Resolvable: id=%s, type=%s, storage=%s, details=%s>" % (
            self.id,
            self.type,
            self.storage,
            self.details,
        )
