# -*- coding: utf-8 -*-
#
# This file is part of REANA.
# Copyright (C) 2018, 2019, 2020, 2021, 2022 CERN.
#
# REANA is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""Models for REANA Components."""

from __future__ import absolute_import

import enum
import math
import logging
import uuid
from datetime import datetime
from functools import reduce

from reana_commons.config import (
    MQ_MAX_PRIORITY,
    REANA_MAX_CONCURRENT_BATCH_WORKFLOWS,
    REANA_RUNTIME_KUBERNETES_KEEP_ALIVE_JOBS_WITH_STATUSES,
)
from reana_commons.utils import get_disk_usage
from sqlalchemy import (
    BigInteger,
    Boolean,
    Column,
    DateTime,
    Enum,
    Float,
    ForeignKey,
    String,
    Text,
    UniqueConstraint,
    event,
    func,
    or_,
)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship
from sqlalchemy_utils import EncryptedType, JSONType, UUIDType
from sqlalchemy_utils.models import Timestamp
from sqlalchemy_utils.types.encrypted.encrypted_type import AesEngine
from sqlalchemy.dialects.postgresql import ARRAY

from reana_db.config import (
    DB_SECRET_KEY,
    DEFAULT_QUOTA_LIMITS,
    DEFAULT_QUOTA_RESOURCES,
    WORKFLOW_TERMINATION_QUOTA_UPDATE_POLICY,
)
from reana_db.utils import (
    build_workspace_path,
    store_workflow_disk_quota,
    update_users_disk_quota,
    update_workflow_cpu_quota,
)


Base = declarative_base()


def generate_uuid():
    """Generate new uuid."""
    return str(uuid.uuid4())


class QuotaBase:
    """Quota base functionality."""

    def _get_quota_by_type(self, resource_type):
        """Aggregate quota usage by resource type."""

        def _get_health_status(usage, limit):
            """Calculate quota health status."""
            health = QuotaHealth.healthy
            if limit:
                percentage = usage / limit * 100
                if percentage >= 60:
                    if percentage >= 85:
                        health = QuotaHealth.critical
                    else:
                        health = QuotaHealth.warning
            return health.name

        quota_usage = 0
        quota_limit = 0
        unit = None

        for resource in self.resources:
            if resource.resource.type_ == resource_type:
                # make sure that all resources of the same type use the same units
                if unit and unit != resource.resource.unit:
                    raise Exception(
                        "Error while calculating quota usage. Not all "
                        "resources of resource type {} use "
                        "the same units.".format(resource_type)
                    )
                unit = resource.resource.unit
                quota_usage += resource.quota_used
                if hasattr(resource, "quota_limit"):
                    quota_limit += resource.quota_limit

        usage_dict = {
            "usage": {
                "raw": quota_usage,
                "human_readable": ResourceUnit.human_readable_unit(unit, quota_usage),
            }
        }
        if quota_limit:
            usage_dict["limit"] = {
                "raw": quota_limit,
                "human_readable": ResourceUnit.human_readable_unit(unit, quota_limit),
            }
            usage_dict["health"] = _get_health_status(quota_usage, quota_limit)

        return usage_dict

    def get_quota_usage(self):
        """Get quota usage information."""
        used_resource_types = set(res.resource.type_ for res in self.resources)
        return {
            resource_type.name: self._get_quota_by_type(resource_type)
            for resource_type in used_resource_types
        }


class User(Base, Timestamp, QuotaBase):
    """User table."""

    __tablename__ = "user_"
    __table_args__ = {"schema": "__reana"}

    id_ = Column(UUIDType, primary_key=True, unique=True, default=generate_uuid)
    email = Column(String(length=255), unique=True, primary_key=True)
    full_name = Column(String(length=255))
    username = Column(String(length=255))
    tokens = relationship("UserToken", backref="user_", lazy="dynamic")
    workflows = relationship("Workflow", backref="user_", lazy="dynamic")
    audit_logs = relationship("AuditLog", backref="user_")

    def __init__(self, access_token=None, **kwargs):
        """Initialize user model."""
        for k, v in kwargs.items():
            setattr(self, k, v)
        if access_token:
            self.access_token = access_token
        self.initialize_user_quota_limits()

    @hybrid_property
    def active_token(self):
        """REANA active access token object."""
        return self.tokens.filter_by(
            status=UserTokenStatus.active, type_=UserTokenType.reana
        ).one_or_none()

    @hybrid_property
    def access_token(self):
        """REANA active access token value."""
        return self.active_token.token if self.active_token else None

    @access_token.setter
    def access_token(self, value):
        """REANA access token setter."""
        from .database import Session

        if self.tokens.count() and self.active_token:
            raise Exception("User {} has already an active access token.".format(self))
        if (
            self.tokens.count()
            and self.access_token_status == UserTokenStatus.requested.name
        ):
            self.latest_access_token.status = UserTokenStatus.active
            self.latest_access_token.token = value
        else:
            user_token = UserToken(
                user_=self,
                token=value,
                status=UserTokenStatus.active,
                type_=UserTokenType.reana,
            )
            Session.add(user_token)

    @hybrid_property
    def latest_access_token(self):
        """REANA most recent access token."""
        latest_reana_token = (
            self.tokens.filter_by(type_=UserTokenType.reana).order_by(
                UserToken.created.desc()
            )
        ).first()
        return latest_reana_token or None

    @hybrid_property
    def access_token_status(self):
        """REANA most recent access token status."""
        return (
            self.latest_access_token.status.name if self.latest_access_token else None
        )

    def get_user_workspace(self):
        """Build user's workspace directory path.

        :return: Path to the user's workspace directory.
        """
        return build_workspace_path(self.id_)

    def request_access_token(self):
        """Create user token and mark it as requested."""
        from .database import Session

        if self.tokens.count() and self.active_token:
            raise Exception("User {} has already an active access token.".format(self))
        if (
            self.tokens.count()
            and self.access_token_status == UserTokenStatus.requested.name
        ):
            raise Exception(
                "User {} has already requested an access" " token.".format(self)
            )
        user_token = UserToken(
            user_=self,
            token=None,
            status=UserTokenStatus.requested,
            type_=UserTokenType.reana,
        )
        Session.add(user_token)
        Session.commit()

    def log_action(self, action, details=None):
        """Create audit log entry for the user.

        :param action: Type of action.
        :type action: AuditLogAction
        :param details: JSON field containing action details.
        """
        from .database import Session

        audit_log = AuditLog(user_id=self.id_, action=action, details=details)
        Session.add(audit_log)
        Session.commit()
        return audit_log

    def initialize_user_quota_limits(self):
        """Initialize user quota limits."""
        resources = Resource.query.all()
        for resource in resources:
            self.resources.append(
                UserResource(
                    user_id=self.id_,
                    resource_id=resource.id_,
                    quota_limit=DEFAULT_QUOTA_LIMITS[resource.type_.name],
                    quota_used=0,
                )
            )

    def has_exceeded_quota(self):
        """Get whether user has exceeded the quota of any resource."""
        return any(
            r.quota_limit != 0 and r.quota_used >= r.quota_limit for r in self.resources
        )

    def get_workflow_overload_priority(self):
        """Get priority factor based on the number of current workflows ``running``."""
        max_concurrent_workflows = REANA_MAX_CONCURRENT_BATCH_WORKFLOWS
        running_count = self.workflows.filter(
            or_(
                Workflow.status == RunStatus.pending,
                Workflow.status == RunStatus.running,
            )
        ).count()
        if running_count > max_concurrent_workflows:
            return 0.1
        # we reduce the 10% (* 0.9) to avoid getting a 0 multiplier factor when
        # `running_count == `max_concurrent_workflows`, thus taking into
        # account workflow complexity when workflows are requeued.
        priority = round(1 - (running_count * 0.9) / max_concurrent_workflows, 2)
        return priority

    def __repr__(self):
        """User string representation."""
        return "<User %r>" % self.id_


class UserTokenStatus(enum.Enum):
    """Enumeration of possible user token statuses."""

    requested = 0
    active = 1
    revoked = 2


class UserTokenType(enum.Enum):
    """Enumeration of possible user token types."""

    reana = 0


class UserToken(Base, Timestamp):
    """User tokens table."""

    __tablename__ = "user_token"
    __table_args__ = {"schema": "__reana"}

    id_ = Column(UUIDType, primary_key=True, default=generate_uuid)
    token = Column(
        EncryptedType(String(length=255), DB_SECRET_KEY, AesEngine, "pkcs5"),
        unique=True,
    )
    status = Column(Enum(UserTokenStatus))
    user_id = Column(UUIDType, ForeignKey("__reana.user_.id_"), nullable=False)
    type_ = Column(Enum(UserTokenType), nullable=False)


class CleanUpDependingOnStatusMixin:
    """Mixin to determine whether to clean up jobs for REANA status enums."""

    @classmethod
    def should_cleanup_job(cls, job_status):
        """Determine if a job/workflow should be cleaned up depending on its status."""
        job_status_name = (
            job_status.name if isinstance(job_status, enum.Enum) else job_status
        )
        keep_on_status_set = set(REANA_RUNTIME_KUBERNETES_KEEP_ALIVE_JOBS_WITH_STATUSES)
        all_statuses_list = [s.name for s in cls]
        if not keep_on_status_set.issubset(all_statuses_list):
            logging.warning(
                "The configuration variable REANA_RUNTIME_KUBERNETES_KEEP_ALIVE_JOBS_WITH_STATUSES contains "
                "unknown statuses {} which will be ignored, possibly causing jobs not to be cleaned up.".format(
                    keep_on_status_set.difference(all_statuses_list)
                )
            )
        return not (job_status_name in keep_on_status_set)


class RunStatus(CleanUpDependingOnStatusMixin, enum.Enum):
    """Enumeration of possible run statuses."""

    created = 0
    running = 1
    finished = 2
    failed = 3
    deleted = 4
    stopped = 5
    queued = 6
    pending = 7


ALLOWED_WORKFLOW_STATUS_TRANSITIONS = [
    # Created
    (RunStatus.created, RunStatus.deleted),
    (RunStatus.created, RunStatus.queued),
    # Queued
    (RunStatus.queued, RunStatus.deleted),
    (RunStatus.queued, RunStatus.pending),
    # Pending
    (RunStatus.pending, RunStatus.running),
    (RunStatus.pending, RunStatus.deleted),
    # Running
    (RunStatus.running, RunStatus.failed),
    (RunStatus.running, RunStatus.finished),
    (RunStatus.running, RunStatus.stopped),
    (RunStatus.running, RunStatus.running),
    # Stopped
    (RunStatus.stopped, RunStatus.deleted),
    # Failed
    (RunStatus.failed, RunStatus.deleted),
    (RunStatus.failed, RunStatus.running),
    # Finished
    (RunStatus.finished, RunStatus.deleted),
    (RunStatus.finished, RunStatus.running),
]


class JobStatus(CleanUpDependingOnStatusMixin, enum.Enum):
    """Enumeration of possible job statuses."""

    created = 0
    running = 1
    finished = 2
    failed = 3
    stopped = 4
    queued = 5


class WorkflowSession(Base):
    """Workflow Session table."""

    __tablename__ = "workflow_session"
    __table_args__ = {"schema": "__reana"}

    workflow_id = Column(UUIDType, ForeignKey("__reana.workflow.id_"), nullable=True)
    session_id = Column(
        UUIDType, ForeignKey("__reana.interactive_session.id_"), primary_key=True
    )

    def __repr__(self):
        """Workflow Session string representation."""
        return "<WorkflowSession {} {}>".format(self.session_id, self.workflow_id)


class InteractiveSessionType(enum.Enum):
    """Enumeration of interactive session types."""

    jupyter = 0


class InteractiveSession(Base, Timestamp, QuotaBase):
    """Interactive Session table."""

    __tablename__ = "interactive_session"
    id_ = Column(UUIDType, primary_key=True, unique=True, default=generate_uuid)
    name = Column(String(255))
    path = Column(Text)  # path to access the interactive session
    status = Column(Enum(RunStatus), nullable=False, default=RunStatus.created)
    owner_id = Column(UUIDType, ForeignKey("__reana.user_.id_"))
    type_ = Column(
        Enum(InteractiveSessionType),
        nullable=False,
        default=InteractiveSessionType.jupyter,
    )

    __table_args__ = (
        UniqueConstraint("name", "path", name="_interactive_session_uc"),
        {"schema": "__reana"},
    )

    def __repr__(self):
        """Interactive Session string representation."""
        return "<InteractiveSession %r>" % self.name


class Workflow(Base, Timestamp, QuotaBase):
    """Workflow table."""

    __tablename__ = "workflow"

    id_ = Column(UUIDType, primary_key=True)
    name = Column(String(255))
    status = Column(Enum(RunStatus), default=RunStatus.created)
    owner_id = Column(UUIDType, ForeignKey("__reana.user_.id_"))
    reana_specification = Column(JSONType)
    input_parameters = Column(JSONType)
    operational_options = Column(JSONType)
    complexity = Column(ARRAY(BigInteger(), dimensions=2), default=[])
    type_ = Column(String(30))
    logs = Column(String)
    run_started_at = Column(DateTime)
    run_finished_at = Column(DateTime)
    run_stopped_at = Column(DateTime)
    _run_number = Column("run_number", Float)
    job_progress = Column(JSONType, default=dict)
    workspace_path = Column(String)
    restart = Column(Boolean, default=False)
    # job_progress = {
    #  jobs_total = {total: job_number}
    #  jobs_running = {job_ids: [], total: c}
    #  jobs_finished = {job_ids: [], total: c}
    #  jobs_failed = {job_ids: [], total: c}}
    engine_specific = Column(JSONType)
    git_ref = Column(String(40))
    git_repo = Column(String(255))
    git_provider = Column(String(255))
    owner = relationship("User", backref="workflow")

    sessions = relationship(
        "InteractiveSession",
        secondary="__reana.workflow_session",
        lazy="dynamic",
        backref="workflow",
        cascade="all, delete",
    )

    __table_args__ = (
        UniqueConstraint(
            "name", "owner_id", "run_number", name="_user_workflow_run_uc"
        ),
        {"schema": "__reana"},
    )

    def __init__(
        self,
        id_,
        name,
        owner_id,
        reana_specification,
        type_,
        logs="",
        input_parameters={},
        operational_options={},
        status=RunStatus.created,
        complexity=[],
        git_ref="",
        git_repo=None,
        git_provider=None,
        workspace_path=None,
        restart=False,
        run_number=None,
    ):
        """Initialize workflow model."""
        self.id_ = id_
        self.name = name
        self.status = status
        self.owner_id = owner_id
        self.reana_specification = reana_specification
        self.input_parameters = input_parameters
        self.operational_options = operational_options
        self.complexity = complexity
        self.type_ = type_
        self.logs = logs or ""
        self.git_ref = git_ref
        self.git_repo = git_repo
        self.git_provider = git_provider
        self.restart = restart
        self._run_number = self.assign_run_number(run_number)
        self.workspace_path = workspace_path or build_workspace_path(
            self.owner_id, self.id_
        )

    def __repr__(self):
        """Workflow string representation."""
        return "<Workflow %r>" % self.id_

    @hybrid_property
    def run_number(self):
        """Property of run_number."""
        if self._run_number.is_integer():
            return int(self._run_number)
        return self._run_number

    @run_number.expression
    def run_number(cls):
        return func.abs(cls._run_number)

    def assign_run_number(self, run_number):
        """Assing run number."""
        from .database import Session

        if run_number:
            last_workflow = (
                Session.query(Workflow)
                .filter(
                    Workflow.name == self.name,
                    Workflow.run_number >= int(run_number),
                    Workflow.run_number < int(run_number) + 1,
                    Workflow.owner_id == self.owner_id,
                )
                .order_by(Workflow.run_number.desc())
                .first()
            )
        else:
            last_workflow = (
                Session.query(Workflow)
                .filter_by(name=self.name, restart=False, owner_id=self.owner_id)
                .order_by(Workflow.run_number.desc())
                .first()
            )
        if last_workflow and self.restart:
            return round(last_workflow.run_number + 0.1, 1)
        else:
            if not last_workflow:
                return 1
            else:
                return last_workflow.run_number + 1

    def get_input_parameters(self):
        """Return workflow parameters."""
        return self.reana_specification.get("inputs", {}).get("parameters", {})

    def get_specification(self):
        """Return workflow specification."""
        return self.reana_specification["workflow"].get("specification", {})

    def get_owner_access_token(self):
        """Return workflow owner access token."""
        from .database import Session

        db_session = Session.object_session(self)
        owner = db_session.query(User).filter_by(id_=self.owner_id).first()
        return owner.access_token

    def get_full_workflow_name(self):
        """Return full workflow name including run number."""
        return "{}.{}".format(self.name, str(self.run_number))

    def get_workspace_disk_usage(self, summarize=False, search=None):
        """Retrieve disk usage information of a workspace."""
        from functools import partial

        if not summarize:
            # size break down per directory so we can't query DB (`r-client du`)
            return get_disk_usage(
                self.workspace_path,
                summarize,
                search,
                to_human_readable_units=partial(
                    ResourceUnit.human_readable_unit, ResourceUnit.bytes_
                ),
            )

        disk_usage = self.get_quota_usage().get("disk", {}).get("usage", {})
        if not disk_usage:
            # recalculate disk workflow resource
            workflow_resource = store_workflow_disk_quota(self)
            disk_usage = dict(
                raw=workflow_resource.quota_used,
                to_human_readable_units=ResourceUnit.human_readable_unit(
                    ResourceUnit.bytes_, workflow_resource.quota_used
                ),
            )

        return [{"name": "", "size": disk_usage}]

    def get_priority(self, cluster_memory):
        """Workflow priority when scheduling it.

        Takes into account both the workflow complexity and the number of workflows
        ``running`` at a certain time.
        """
        return round(
            self.owner.get_workflow_overload_priority()
            * self.get_complexity_priority(cluster_memory)
        )

    def get_complexity_priority(self, total_cluster_memory):
        """Calculate workflow priority based on its complexity."""
        if not self.complexity:
            return 0
        wf_memory = int(
            reduce(lambda sum, item: sum + item[0] * item[1], self.complexity, 0)
        )
        if not total_cluster_memory or wf_memory > total_cluster_memory:
            return 0
        return int(round(1 - wf_memory / total_cluster_memory, 2) * MQ_MAX_PRIORITY)

    @staticmethod
    def update_workflow_status(
        db_session, workflow_uuid, status, new_logs="", message=None
    ):
        """Update database workflow status.

        :param workflow_uuid: UUID which represents the workflow.
        :param status: String that represents the workflow status.
        :param new_logs: New logs from workflow execution.
        :param message: Unused.
        """
        try:
            workflow = db_session.query(Workflow).filter_by(id_=workflow_uuid).first()

            if not workflow:
                raise Exception(
                    "Workflow {0} doesn't exist in database.".format(workflow_uuid)
                )
            if status:
                workflow.status = status
            if new_logs:
                workflow.logs = (workflow.logs or "") + new_logs + "\n"
            db_session.commit()
        except Exception as e:
            raise e

    def can_transition_to(self, next_status):
        """Whether the provided workflow can transition to the next status."""
        current_transition = (self.status, next_status)
        return current_transition in ALLOWED_WORKFLOW_STATUS_TRANSITIONS

    def update_workflow_timestamp(self, new_status):
        """Update workflow timestamps according to new status."""
        from .database import Session

        if new_status in [
            RunStatus.finished,
            RunStatus.failed,
        ]:
            self.run_finished_at = datetime.now()
        elif new_status in [RunStatus.stopped]:
            self.run_stopped_at = datetime.now()
        elif new_status in [RunStatus.running]:
            self.run_started_at = datetime.now()
        Session.commit()


@event.listens_for(Workflow.status, "set")
def workflow_status_change_listener(workflow, new_status, old_status, initiator):
    """Workflow status change listener."""

    def _update_disk_quota(workflow):
        if ResourceType.disk.name not in WORKFLOW_TERMINATION_QUOTA_UPDATE_POLICY:
            return

        try:
            update_users_disk_quota(user=workflow.owner)
            store_workflow_disk_quota(workflow)
        except Exception as e:
            logging.error(f"Failed to update disk quota: \n{e}\nContinuing...")

    def _update_cpu_quota(workflow):
        if ResourceType.cpu.name not in WORKFLOW_TERMINATION_QUOTA_UPDATE_POLICY:
            return

        try:
            update_workflow_cpu_quota(workflow=workflow)
        except Exception as e:
            logging.error(f"Failed to update cpu quota: \n{e}\nContinuing...")

    workflow.update_workflow_timestamp(new_status)
    if new_status in [
        RunStatus.finished,
        RunStatus.failed,
        RunStatus.stopped,
    ]:
        _update_cpu_quota(workflow)
        _update_disk_quota(workflow)
    elif new_status in [RunStatus.deleted]:
        _update_disk_quota(workflow)

    return new_status


class Job(Base, Timestamp):
    """Job table."""

    __tablename__ = "job"
    __table_args__ = {"schema": "__reana"}

    id_ = Column(UUIDType, primary_key=True, default=generate_uuid)
    backend_job_id = Column(String(256))
    workflow_uuid = Column(UUIDType)
    status = Column(Enum(JobStatus), default=JobStatus.created)
    compute_backend = Column(String(30))
    cvmfs_mounts = Column(Text)
    shared_file_system = Column(Boolean)
    docker_img = Column(String(256))
    cmd = Column(JSONType)
    env_vars = Column(JSONType)
    deleted = Column(Boolean)
    logs = Column(String, nullable=True)
    started_at = Column(DateTime)
    finished_at = Column(DateTime)
    prettified_cmd = Column(JSONType)
    job_name = Column(Text)


@event.listens_for(Job.status, "set")
def job_status_change_listener(job, new_status, old_status, initiator):
    """Job status change listener."""
    if new_status != old_status:
        from .database import Session

        if new_status in [
            JobStatus.finished,
            JobStatus.failed,
        ]:
            job.finished_at = datetime.now()
        elif new_status in [JobStatus.running]:
            job.started_at = datetime.now()

        Session.commit()


class JobCache(Base, Timestamp):
    """Job Cache table."""

    __tablename__ = "job_cache"
    __table_args__ = {"schema": "__reana"}

    id_ = Column(UUIDType, primary_key=True, default=generate_uuid)
    job_id = Column(UUIDType, ForeignKey("__reana.job.id_"), primary_key=True)
    parameters = Column(String(1024))
    result_path = Column(String(1024))
    workspace_hash = Column(String(1024))
    access_times = Column(JSONType)


class AuditLogAction(enum.Enum):
    """Enumeration of audit log actions."""

    request_token = 0
    grant_token = 1
    revoke_token = 2


class AuditLog(Base, Timestamp):
    """Audit log table."""

    __tablename__ = "audit_log"
    __table_args__ = {"schema": "__reana"}

    id_ = Column(UUIDType, primary_key=True, default=generate_uuid)
    user_id = Column(UUIDType, ForeignKey("__reana.user_.id_"), nullable=False)
    action = Column(Enum(AuditLogAction), nullable=False)
    details = Column(JSONType)

    def __repr__(self):
        """Audit log string representation."""
        return "<AuditLog {} {}>".format(self.id_, self.action)


class ResourceType(enum.Enum):
    """Enumeration of resource types."""

    cpu = 0
    disk = 1


class ResourceUnit(enum.Enum):
    """Enumeration of resource usage units."""

    bytes_ = 0
    milliseconds = 1

    @staticmethod
    def _human_readable_milliseconds(milliseconds):
        """Convert milliseconds usage to human readable string."""
        hours, minutes, seconds = (
            milliseconds // (1000 * 60 * 60),
            (milliseconds // (1000 * 60)) % 60,
            (milliseconds // 1000) % 60,
        )

        human_readable_milliseconds = ""
        for value, unit in [(hours, "h"), (minutes, "m"), (seconds, "s")]:
            if value >= 1:
                human_readable_milliseconds += "{value}{unit} ".format(
                    value=value, unit=unit
                )

        return human_readable_milliseconds.strip() or "0s"

    @staticmethod
    def _human_readable_bytes(bytes_):
        """Convert bytes usage to human readable string."""
        if bytes_ == 0:
            return "0 Bytes"
        units = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
        digits = 2
        k = 1024
        unit_index = int(math.floor(math.log(bytes_) / math.log(k)))

        converted_value = round(bytes_ / math.pow(k, unit_index), digits)
        return "{converted_value} {converted_unit}".format(
            converted_value=int(converted_value)
            if converted_value.is_integer()
            else converted_value,
            converted_unit=units[unit_index],
        )

    @staticmethod
    def human_readable_unit(unit, value):
        """Convert passed value in units to human readable string."""
        convert_to_human_readable = {
            ResourceUnit.bytes_: ResourceUnit._human_readable_bytes,
            ResourceUnit.milliseconds: ResourceUnit._human_readable_milliseconds,
        }
        return convert_to_human_readable[unit](value)


class Resource(Base, Timestamp):
    """Resource table."""

    __tablename__ = "resource"
    __table_args__ = {"schema": "__reana"}

    id_ = Column(UUIDType, primary_key=True, default=generate_uuid)
    name = Column(String(1024), unique=True, nullable=False)
    type_ = Column(Enum(ResourceType), nullable=False)
    unit = Column(Enum(ResourceUnit), nullable=False)
    title = Column(String(1024))

    def __repr__(self):
        """Resource string representation."""
        return "<Resource {}>".format(self.id_)

    @staticmethod
    def initialise_default_resources():
        """Initialise default Resources."""
        from reana_db.database import Session

        existing_resources = [r.name for r in Resource.query.all()]
        default_resources = []
        resource_type_to_unit = {
            ResourceType.cpu: ResourceUnit.milliseconds,
            ResourceType.disk: ResourceUnit.bytes_,
        }
        for type_, name in DEFAULT_QUOTA_RESOURCES.items():
            if name not in existing_resources:
                default_resources.append(
                    Resource(
                        name=name,
                        type_=ResourceType[type_],
                        unit=resource_type_to_unit[ResourceType[type_]],
                        title="Default {} resource.".format(type_),
                    )
                )

        if default_resources:
            Session.add_all(default_resources)
            Session.commit()

        return default_resources


class UserResource(Base, Timestamp):
    """User Resource table."""

    __tablename__ = "user_resource"
    __table_args__ = {"schema": "__reana"}

    user_id = Column(UUIDType, ForeignKey("__reana.user_.id_"), primary_key=True)
    resource_id = Column(UUIDType, ForeignKey("__reana.resource.id_"), primary_key=True)
    quota_limit = Column(BigInteger())
    quota_used = Column(BigInteger())
    user = relationship("User", backref="resources")
    resource = relationship("Resource", backref="user_resource")

    def __repr__(self):
        """User Resource string representation."""
        return "<UserResource {} {}>".format(self.user_id, self.resource_id)


class WorkflowResource(Base, Timestamp):
    """Workflow Resource table."""

    __tablename__ = "workflow_resource"
    __table_args__ = {"schema": "__reana"}

    workflow_id = Column(UUIDType, ForeignKey("__reana.workflow.id_"), primary_key=True)
    resource_id = Column(UUIDType, ForeignKey("__reana.resource.id_"), primary_key=True)
    quota_used = Column(BigInteger())
    workflow = relationship("Workflow", backref="resources")
    resource = relationship("Resource", backref="workflow_resources")

    def __repr__(self):
        """Workflow Resource string representation."""
        return "<WorkflowResource {} {}>".format(self.workflow_id, self.resource_id)


class InteractiveSessionResource(Base, Timestamp):
    """Interactive Session Resource table."""

    __tablename__ = "interactive_session_resource"
    __table_args__ = {"schema": "__reana"}

    session_id = Column(
        UUIDType, ForeignKey("__reana.interactive_session.id_"), primary_key=True
    )
    resource_id = Column(UUIDType, ForeignKey("__reana.resource.id_"), primary_key=True)
    quota_used = Column(BigInteger())
    interactive_session = relationship("InteractiveSession", backref="resources")
    resource = relationship("Resource", backref="interactive_session_resources")

    def __repr__(self):
        """Interactive Session Resource string representation."""
        return "<InteractiveSessionResource {} {}>".format(
            self.session_id, self.resource_id
        )


class QuotaHealth(enum.Enum):
    """Enumeration of quota health statuses."""

    healthy = 0
    warning = 1
    critical = 2
