import uuid

from typing import Dict, List, Optional

from hestia.datetime_typing import AwareDT

from django.conf import settings
from django.contrib.postgres.fields import JSONField
from django.db import models
from django.utils import timezone
from django.utils.functional import cached_property

import auditor
import compiler

from constants.cloning_strategies import CloningStrategy
from db.models.abstract.backend import BackendModel
from db.models.abstract.datarefs import DataReferenceModel
from db.models.abstract.deleted import DeletedModel
from db.models.abstract.describable import DescribableModel
from db.models.abstract.diff import DiffModel
from db.models.abstract.is_managed import IsManagedModel
from db.models.abstract.job import TensorboardJobMixin
from db.models.abstract.nameable import NameableModel
from db.models.abstract.outputs import OutputsModel
from db.models.abstract.persistence import PersistenceModel
from db.models.abstract.readme import ReadmeModel
from db.models.abstract.run import RunTimeModel
from db.models.abstract.sub_paths import SubPathModel
from db.models.abstract.tag import TagModel
from db.models.abstract.unique_name import UniqueNameMixin
from db.models.charts import ChartViewModel
from db.models.statuses import LastStatusMixin, StatusModel
from db.models.unique_names import EXPERIMENT_UNIQUE_NAME_FORMAT
from db.redis.heartbeat import RedisHeartBeat
from events.registry.experiment import (
    EXPERIMENT_COPIED,
    EXPERIMENT_NEW_METRIC,
    EXPERIMENT_RESTARTED,
    EXPERIMENT_RESUMED
)
from libs.paths.experiments import get_experiment_subpath
from lifecycles.experiments import ExperimentLifeCycle
from lifecycles.jobs import JobLifeCycle
from schemas import PodResourcesConfig, TaskType, kinds


class Experiment(DiffModel,
                 RunTimeModel,
                 BackendModel,
                 IsManagedModel,
                 NameableModel,
                 DataReferenceModel,
                 OutputsModel,
                 PersistenceModel,
                 SubPathModel,
                 DescribableModel,
                 ReadmeModel,
                 TagModel,
                 DeletedModel,
                 UniqueNameMixin,
                 LastStatusMixin,
                 TensorboardJobMixin):
    """A model that represents experiments."""
    STATUSES = ExperimentLifeCycle

    uuid = models.UUIDField(
        default=uuid.uuid4,
        editable=False,
        unique=True,
        null=False)
    project = models.ForeignKey(
        'db.Project',
        on_delete=models.CASCADE,
        related_name='experiments')
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='+')
    build_job = models.ForeignKey(
        'db.BuildJob',
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        related_name='experiments')
    framework = models.CharField(
        max_length=16,
        blank=True,
        null=True)
    experiment_group = models.ForeignKey(
        'db.ExperimentGroup',
        on_delete=models.CASCADE,
        blank=True,
        null=True,
        related_name='experiments',
        help_text='The experiment group that generate this experiment.')
    params = JSONField(
        blank=True,
        null=True,
        help_text='The parameters used for this experiment.')
    content = models.TextField(
        null=True,
        blank=True,
        help_text='The yaml content of the polyaxonfile/specification.')
    run_env = JSONField(
        blank=True,
        null=True,
        help_text='The run environment of the experiment.')
    original_experiment = models.ForeignKey(
        'self',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='clones',
        help_text='The original experiment that was cloned from.')
    cloning_strategy = models.CharField(
        max_length=16,
        blank=True,
        null=True,
        choices=CloningStrategy.CHOICES)
    status = models.OneToOneField(
        'db.ExperimentStatus',
        related_name='+',
        blank=True,
        null=True,
        editable=True,
        on_delete=models.SET_NULL)
    last_metric = JSONField(
        blank=True,
        null=True,
        default=dict)
    code_reference = models.ForeignKey(
        'db.CodeReference',
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        related_name='+')

    class Meta:
        app_label = 'db'
        unique_together = (('project', 'name'),)
        indexes = [
            models.Index(fields=['name']),
        ]

    @cached_property
    def unique_name(self) -> str:
        if self.experiment_group:
            parent_name = self.experiment_group.unique_name
        else:
            parent_name = self.project.unique_name
        return EXPERIMENT_UNIQUE_NAME_FORMAT.format(parent_name=parent_name, id=self.id)

    @property
    def subpath(self) -> str:
        return get_experiment_subpath(experiment_name=self.unique_name)

    @cached_property
    def specification(self) -> Optional['ExperimentSpecification']:
        return compiler.compile(kind=kinds.EXPERIMENT, values=self.content)

    @property
    def has_specification(self) -> bool:
        return self.content is not None

    @cached_property
    def is_distributed(self) -> bool:
        if self.has_specification and self.specification.cluster_def is not None:
            return self.specification.cluster_def[1]
        return False

    @property
    def default_job_role(self) -> str:
        if self.is_distributed:
            return self.jobs.order_by('created_at').only('role').first().role
        return TaskType.MASTER

    @cached_property
    def secret_refs(self) -> Optional[List[str]]:
        if not self.specification:
            return None
        return self.specification.secret_refs

    @cached_property
    def config_map_refs(self) -> Optional[List[str]]:
        if not self.specification:
            return None
        return self.specification.config_map_refs

    @cached_property
    def resources(self) -> Optional[PodResourcesConfig]:
        if not self.specification:
            return None
        return self.specification.total_resources

    @cached_property
    def max_restarts(self) -> Optional[int]:
        if not self.specification:
            return None
        return self.specification.max_restarts

    @property
    def last_job_statuses(self) -> List[str]:
        """The last constants of the job in this experiment."""
        statuses = []
        for status in self.jobs.values_list('status__status', flat=True):
            if status is not None:
                statuses.append(status)
        return statuses

    @property
    def has_running_jobs(self) -> bool:
        """"Return a boolean indicating if the experiment has any running jobs"""
        return self.jobs.exclude(status__status__in=ExperimentLifeCycle.DONE_STATUS).exists()

    @property
    def calculated_status(self) -> str:
        master_status = self.jobs.order_by('created_at').first().last_status
        calculated_status = master_status if JobLifeCycle.is_done(master_status) else None
        if calculated_status is None:
            calculated_status = ExperimentLifeCycle.jobs_status(self.last_job_statuses)
        if calculated_status is None:
            return self.last_status
        return calculated_status

    @property
    def is_clone(self) -> bool:
        return self.original_experiment is not None

    @property
    def original_unique_name(self) -> Optional[str]:
        return self.original_experiment.unique_name if self.original_experiment else None

    @property
    def is_restart(self) -> bool:
        return self.is_clone and self.cloning_strategy == CloningStrategy.RESTART

    @property
    def is_resume(self) -> bool:
        return self.is_clone and self.cloning_strategy == CloningStrategy.RESUME

    @property
    def is_copy(self) -> bool:
        return self.is_clone and self.cloning_strategy == CloningStrategy.COPY

    @property
    def is_independent(self) -> bool:
        """If the experiment belongs to a experiment_group or is independently created."""
        return self.experiment_group is None

    def update_status(self) -> bool:
        current_status = self.last_status
        calculated_status = self.calculated_status
        if calculated_status != current_status:
            if calculated_status == ExperimentLifeCycle.UNSCHEDULABLE:
                # Add details augmentation if the it's UNSCHEDULABLE
                self.set_status(ExperimentLifeCycle.WARNING,
                                message='One of the jobs is unschedulable.')
            elif calculated_status == ExperimentLifeCycle.STOPPED:
                # Add details augmentation if the it's UNSCHEDULABLE
                self.set_status(ExperimentLifeCycle.STOPPED,
                                message='One of the jobs was stopped by the system.')
            else:
                # Add new status to the experiment
                self.set_status(calculated_status)
            return True
        return False

    def last_status_before(self, status_date: AwareDT = None) -> Optional[str]:
        if not status_date:
            return self.last_status
        status = ExperimentStatus.objects.filter(
            experiment=self,
            created_at__lte=status_date).last()
        return status.status if status else None

    def set_status(self,
                   status: str,
                   created_at: AwareDT = None,
                   message: str = None,
                   traceback: Dict = None,
                   **kwargs):
        if status in ExperimentLifeCycle.HEARTBEAT_STATUS:
            RedisHeartBeat.experiment_ping(self.id)
        last_status = self.last_status_before(status_date=created_at)
        if ExperimentLifeCycle.can_transition(status_from=last_status, status_to=status):
            params = {'created_at': created_at} if created_at else {}
            ExperimentStatus.objects.create(experiment=self,
                                            status=status,
                                            message=message,
                                            traceback=traceback,
                                            **params)

    def set_metric(self, metric):
        last_metric = self.last_metric
        last_metric.update(metric)
        self.last_metric = last_metric
        self.save(update_fields=['last_metric'])
        auditor.record(event_type=EXPERIMENT_NEW_METRIC, instance=self)

    def _clone(self,
               cloning_strategy: str,
               event_type: str,
               user=None,
               description: str = None,
               content=None,
               params: dict = None,
               code_reference=None,
               update_code_reference: bool = False,
               experiment_group_id=None) -> 'Experiment':
        if not code_reference and not update_code_reference:
            code_reference = self.code_reference
        instance = Experiment.objects.create(
            project_id=self.project_id,
            user=user or self.user,
            experiment_group_id=experiment_group_id or self.experiment_group_id,
            description=description or self.description,
            content=content or self.content,
            params=params or self.params,
            original_experiment=self,
            cloning_strategy=cloning_strategy,
            code_reference=code_reference)
        auditor.record(event_type=event_type, instance=instance)
        return instance

    def resume(self,
               user=None,
               description: str = None,
               content=None,
               params: Dict = None,
               code_reference=None,
               update_code_reference: bool = False,
               experiment_group_id=None) -> 'Experiment':
        # TODO: We need to check if this instance was stopped after it was created
        # TODO: If that's the case and no updates are passed we just resume this very same instance
        # If the current instance is a resume of an original than we need to resume the original
        if self.is_resume:
            return self.original_experiment.resume(
                user=user,
                description=description,
                content=content,
                params=params,
                code_reference=code_reference,
                update_code_reference=update_code_reference,
                experiment_group_id=experiment_group_id or self.experiment_group_id)

        # Resume normal workflow
        return self._clone(cloning_strategy=CloningStrategy.RESUME,
                           event_type=EXPERIMENT_RESUMED,
                           user=user,
                           description=description,
                           content=content,
                           params=params,
                           code_reference=code_reference,
                           update_code_reference=update_code_reference,
                           experiment_group_id=experiment_group_id or self.experiment_group_id)

    def restart(self,
                user=None,
                description: str = None,
                content=None,
                params: Dict = None,
                code_reference=None,
                update_code_reference: bool = False,
                experiment_group=None) -> 'Experiment':
        return self._clone(cloning_strategy=CloningStrategy.RESTART,
                           event_type=EXPERIMENT_RESTARTED,
                           user=user,
                           description=description,
                           content=content,
                           params=params,
                           code_reference=code_reference,
                           update_code_reference=update_code_reference,
                           experiment_group_id=experiment_group.id if experiment_group else None)

    def copy(self,
             user=None,
             description: str = None,
             content=None,
             params: Dict = None,
             code_reference=None,
             update_code_reference: bool = False,
             experiment_group=None) -> 'Experiment':
        return self._clone(cloning_strategy=CloningStrategy.COPY,
                           event_type=EXPERIMENT_COPIED,
                           user=user,
                           description=description,
                           content=content,
                           params=params,
                           code_reference=code_reference,
                           update_code_reference=update_code_reference,
                           experiment_group_id=experiment_group.id if experiment_group else None)


class ExperimentStatus(StatusModel):
    """A model that represents an experiment status at certain time."""
    STATUSES = ExperimentLifeCycle

    experiment = models.ForeignKey(
        'db.Experiment',
        on_delete=models.CASCADE,
        related_name='statuses')
    status = models.CharField(
        max_length=64,
        blank=True,
        null=True,
        default=STATUSES.CREATED,
        choices=STATUSES.CHOICES)

    class Meta:
        app_label = 'db'
        verbose_name_plural = 'Experiment Statuses'
        ordering = ['created_at']

    def __str__(self) -> str:
        return '{} <{}>'.format(self.experiment.unique_name, self.status)


class ExperimentMetric(models.Model):
    """A model that represents an experiment metric at certain time."""
    experiment = models.ForeignKey(
        'db.Experiment',
        on_delete=models.CASCADE,
        related_name='metrics')
    created_at = models.DateTimeField(default=timezone.now)
    values = JSONField()

    def __str__(self) -> str:
        return '{} <{}>'.format(self.experiment.unique_name, self.created_at)

    class Meta:
        app_label = 'db'
        ordering = ['created_at']


class ExperimentChartView(ChartViewModel):
    """A model that represents an experiment chart view."""
    experiment = models.ForeignKey(
        'db.Experiment',
        on_delete=models.CASCADE,
        related_name='chart_views')

    class Meta:
        app_label = 'db'
        verbose_name_plural = 'Experiment Chart Views'
        ordering = ['created_at']

import uuid

from typing import Dict, List, Optional

from hestia.datetime_typing import AwareDT

from django.conf import settings
from django.contrib.postgres.fields import JSONField
from django.db import models
from django.utils import timezone
from django.utils.functional import cached_property

import auditor
import compiler

from constants.cloning_strategies import CloningStrategy
from db.models.abstract.backend import BackendModel
from db.models.abstract.datarefs import DataReferenceModel
from db.models.abstract.deleted import DeletedModel
from db.models.abstract.describable import DescribableModel
from db.models.abstract.diff import DiffModel
from db.models.abstract.is_managed import IsManagedModel
from db.models.abstract.job import TensorboardJobMixin
from db.models.abstract.nameable import NameableModel
from db.models.abstract.outputs import OutputsModel
from db.models.abstract.persistence import PersistenceModel
from db.models.abstract.readme import ReadmeModel
from db.models.abstract.run import RunTimeModel
from db.models.abstract.sub_paths import SubPathModel
from db.models.abstract.tag import TagModel
from db.models.abstract.unique_name import UniqueNameMixin
from db.models.charts import ChartViewModel
from db.models.statuses import LastStatusMixin, StatusModel
from db.models.unique_names import EXPERIMENT_UNIQUE_NAME_FORMAT
from db.redis.heartbeat import RedisHeartBeat
from events.registry.experiment import (
    EXPERIMENT_COPIED,
    EXPERIMENT_NEW_METRIC,
    EXPERIMENT_RESTARTED,
    EXPERIMENT_RESUMED
)
from libs.paths.experiments import get_experiment_subpath
from lifecycles.experiments import ExperimentLifeCycle
from lifecycles.jobs import JobLifeCycle
from schemas import PodResourcesConfig, TaskType, kinds


class Experiment(DiffModel,
                 RunTimeModel,
                 BackendModel,
                 IsManagedModel,
                 NameableModel,
                 DataReferenceModel,
                 OutputsModel,
                 PersistenceModel,
                 SubPathModel,
                 DescribableModel,
                 ReadmeModel,
                 TagModel,
                 DeletedModel,
                 UniqueNameMixin,
                 LastStatusMixin,
                 TensorboardJobMixin):
    """A model that represents experiments."""
    STATUSES = ExperimentLifeCycle

    uuid = models.UUIDField(
        default=uuid.uuid4,
        editable=False,
        unique=True,
        null=False)
    project = models.ForeignKey(
        'db.Project',
        on_delete=models.CASCADE,
        related_name='experiments')
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='+')
    build_job = models.ForeignKey(
        'db.BuildJob',
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        related_name='experiments')
    framework = models.CharField(
        max_length=16,
        blank=True,
        null=True)
    experiment_group = models.ForeignKey(
        'db.ExperimentGroup',
        on_delete=models.CASCADE,
        blank=True,
        null=True,
        related_name='experiments',
        help_text='The experiment group that generate this experiment.')
    params = JSONField(
        blank=True,
        null=True,
        help_text='The parameters used for this experiment.')
    content = models.TextField(
        null=True,
        blank=True,
        help_text='The yaml content of the polyaxonfile/specification.')
    run_env = JSONField(
        blank=True,
        null=True,
        help_text='The run environment of the experiment.')
    original_experiment = models.ForeignKey(
        'self',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='clones',
        help_text='The original experiment that was cloned from.')
    cloning_strategy = models.CharField(
        max_length=16,
        blank=True,
        null=True,
        choices=CloningStrategy.CHOICES)
    status = models.OneToOneField(
        'db.ExperimentStatus',
        related_name='+',
        blank=True,
        null=True,
        editable=True,
        on_delete=models.SET_NULL)
    last_metric = JSONField(
        blank=True,
        null=True,
        default=dict)
    code_reference = models.ForeignKey(
        'db.CodeReference',
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        related_name='+')

    class Meta:
        app_label = 'db'
        unique_together = (('project', 'name'),)
        indexes = [
            models.Index(fields=['name']),
        ]

    @cached_property
    def unique_name(self) -> str:
        if self.experiment_group:
            parent_name = self.experiment_group.unique_name
        else:
            parent_name = self.project.unique_name
        return EXPERIMENT_UNIQUE_NAME_FORMAT.format(parent_name=parent_name, id=self.id)

    @property
    def subpath(self) -> str:
        return get_experiment_subpath(experiment_name=self.unique_name)

    @cached_property
    def specification(self) -> Optional['ExperimentSpecification']:
        return compiler.compile(kind=kinds.EXPERIMENT, values=self.content)

    @property
    def has_specification(self) -> bool:
        return self.content is not None

    @cached_property
    def is_distributed(self) -> bool:
        if self.has_specification and self.specification.cluster_def is not None:
            return self.specification.cluster_def[1]
        return False

    @property
    def default_job_role(self) -> str:
        if self.is_distributed:
            return self.jobs.order_by('created_at').only('role').first().role
        return TaskType.MASTER

    @cached_property
    def secret_refs(self) -> Optional[List[str]]:
        if not self.specification:
            return None
        return self.specification.secret_refs

    @cached_property
    def config_map_refs(self) -> Optional[List[str]]:
        if not self.specification:
            return None
        return self.specification.config_map_refs

    @cached_property
    def resources(self) -> Optional[PodResourcesConfig]:
        if not self.specification:
            return None
        return self.specification.total_resources

    @cached_property
    def max_restarts(self) -> Optional[int]:
        if not self.specification:
            return None
        return self.specification.max_restarts

    @property
    def last_job_statuses(self) -> List[str]:
        """The last constants of the job in this experiment."""
        statuses = []
        for status in self.jobs.values_list('status__status', flat=True):
            if status is not None:
                statuses.append(status)
        return statuses

    @property
    def has_running_jobs(self) -> bool:
        """"Return a boolean indicating if the experiment has any running jobs"""
        return self.jobs.exclude(status__status__in=ExperimentLifeCycle.DONE_STATUS).exists()

    @property
    def calculated_status(self) -> str:
        master_status = self.jobs.order_by('created_at').first().last_status
        calculated_status = master_status if JobLifeCycle.is_done(master_status) else None
        if calculated_status is None:
            calculated_status = ExperimentLifeCycle.jobs_status(self.last_job_statuses)
        if calculated_status is None:
            return self.last_status
        return calculated_status

    @property
    def is_clone(self) -> bool:
        return self.original_experiment is not None

    @property
    def original_unique_name(self) -> Optional[str]:
        return self.original_experiment.unique_name if self.original_experiment else None

    @property
    def is_restart(self) -> bool:
        return self.is_clone and self.cloning_strategy == CloningStrategy.RESTART

    @property
    def is_resume(self) -> bool:
        return self.is_clone and self.cloning_strategy == CloningStrategy.RESUME

    @property
    def is_copy(self) -> bool:
        return self.is_clone and self.cloning_strategy == CloningStrategy.COPY

    @property
    def is_independent(self) -> bool:
        """If the experiment belongs to a experiment_group or is independently created."""
        return self.experiment_group is None

    def update_status(self) -> bool:
        current_status = self.last_status
        calculated_status = self.calculated_status
        if calculated_status != current_status:
            if calculated_status == ExperimentLifeCycle.UNSCHEDULABLE:
                # Add details augmentation if the it's UNSCHEDULABLE
                self.set_status(ExperimentLifeCycle.WARNING,
                                message='One of the jobs is unschedulable.')
            elif calculated_status == ExperimentLifeCycle.STOPPED:
                # Add details augmentation if the it's UNSCHEDULABLE
                self.set_status(ExperimentLifeCycle.STOPPED,
                                message='One of the jobs was stopped by the system.')
            else:
                # Add new status to the experiment
                self.set_status(calculated_status)
            return True
        return False

    def last_status_before(self, status_date: AwareDT = None) -> Optional[str]:
        if not status_date:
            return self.last_status
        status = ExperimentStatus.objects.filter(
            experiment=self,
            created_at__lte=status_date).last()
        return status.status if status else None

    def set_status(self,
                   status: str,
                   created_at: AwareDT = None,
                   message: str = None,
                   traceback: Dict = None,
                   **kwargs):
        if status in ExperimentLifeCycle.HEARTBEAT_STATUS:
            RedisHeartBeat.experiment_ping(self.id)
        last_status = self.last_status_before(status_date=created_at)
        if ExperimentLifeCycle.can_transition(status_from=last_status, status_to=status):
            params = {'created_at': created_at} if created_at else {}
            ExperimentStatus.objects.create(experiment=self,
                                            status=status,
                                            message=message,
                                            traceback=traceback,
                                            **params)

    def set_metric(self, metric):
        last_metric = self.last_metric
        last_metric.update(metric)
        self.last_metric = last_metric
        self.save(update_fields=['last_metric'])
        auditor.record(event_type=EXPERIMENT_NEW_METRIC, instance=self)

    def _clone(self,
               cloning_strategy: str,
               event_type: str,
               user=None,
               description: str = None,
               content=None,
               params: dict = None,
               code_reference=None,
               update_code_reference: bool = False,
               experiment_group_id=None) -> 'Experiment':
        if not code_reference and not update_code_reference:
            code_reference = self.code_reference
        instance = Experiment.objects.create(
            project_id=self.project_id,
            user=user or self.user,
            experiment_group_id=experiment_group_id or self.experiment_group_id,
            description=description or self.description,
            content=content or self.content,
            params=params or self.params,
            original_experiment=self,
            cloning_strategy=cloning_strategy,
            code_reference=code_reference)
        auditor.record(event_type=event_type, instance=instance)
        return instance

    def resume(self,
               user=None,
               description: str = None,
               content=None,
               params: Dict = None,
               code_reference=None,
               update_code_reference: bool = False,
               experiment_group_id=None) -> 'Experiment':
        # TODO: We need to check if this instance was stopped after it was created
        # TODO: If that's the case and no updates are passed we just resume this very same instance
        # If the current instance is a resume of an original than we need to resume the original
        if self.is_resume:
            return self.original_experiment.resume(
                user=user,
                description=description,
                content=content,
                params=params,
                code_reference=code_reference,
                update_code_reference=update_code_reference,
                experiment_group_id=experiment_group_id or self.experiment_group_id)

        # Resume normal workflow
        return self._clone(cloning_strategy=CloningStrategy.RESUME,
                           event_type=EXPERIMENT_RESUMED,
                           user=user,
                           description=description,
                           content=content,
                           params=params,
                           code_reference=code_reference,
                           update_code_reference=update_code_reference,
                           experiment_group_id=experiment_group_id or self.experiment_group_id)

    def restart(self,
                user=None,
                description: str = None,
                content=None,
                params: Dict = None,
                code_reference=None,
                update_code_reference: bool = False,
                experiment_group=None) -> 'Experiment':
        return self._clone(cloning_strategy=CloningStrategy.RESTART,
                           event_type=EXPERIMENT_RESTARTED,
                           user=user,
                           description=description,
                           content=content,
                           params=params,
                           code_reference=code_reference,
                           update_code_reference=update_code_reference,
                           experiment_group_id=experiment_group.id if experiment_group else None)

    def copy(self,
             user=None,
             description: str = None,
             content=None,
             params: Dict = None,
             code_reference=None,
             update_code_reference: bool = False,
             experiment_group=None) -> 'Experiment':
        return self._clone(cloning_strategy=CloningStrategy.COPY,
                           event_type=EXPERIMENT_COPIED,
                           user=user,
                           description=description,
                           content=content,
                           params=params,
                           code_reference=code_reference,
                           update_code_reference=update_code_reference,
                           experiment_group_id=experiment_group.id if experiment_group else None)


class ExperimentStatus(StatusModel):
    """A model that represents an experiment status at certain time."""
    STATUSES = ExperimentLifeCycle

    experiment = models.ForeignKey(
        'db.Experiment',
        on_delete=models.CASCADE,
        related_name='statuses')
    status = models.CharField(
        max_length=64,
        blank=True,
        null=True,
        default=STATUSES.CREATED,
        choices=STATUSES.CHOICES)

    class Meta:
        app_label = 'db'
        verbose_name_plural = 'Experiment Statuses'
        ordering = ['created_at']

    def __str__(self) -> str:
        return '{} <{}>'.format(self.experiment.unique_name, self.status)


class ExperimentMetric(models.Model):
    """A model that represents an experiment metric at certain time."""
    experiment = models.ForeignKey(
        'db.Experiment',
        on_delete=models.CASCADE,
        related_name='metrics')
    created_at = models.DateTimeField(default=timezone.now)
    values = JSONField()

    def __str__(self) -> str:
        return '{} <{}>'.format(self.experiment.unique_name, self.created_at)

    class Meta:
        app_label = 'db'
        ordering = ['created_at']


class ExperimentChartView(ChartViewModel):
    """A model that represents an experiment chart view."""
    experiment = models.ForeignKey(
        'db.Experiment',
        on_delete=models.CASCADE,
        related_name='chart_views')

    class Meta:
        app_label = 'db'
        verbose_name_plural = 'Experiment Chart Views'
        ordering = ['created_at']

A model that represents experiments. The last constants of the job in this experiment. "Return a boolean indicating if the experiment has any running jobs If the experiment belongs to a experiment_group or is independently created. # Add details augmentation if the it's UNSCHEDULABLE # Add details augmentation if the it's UNSCHEDULABLE # Add new status to the experiment # TODO: We need to check if this instance was stopped after it was created # TODO: If that's the case and no updates are passed we just resume this very same instance # If the current instance is a resume of an original than we need to resume the original # Resume normal workflow A model that represents an experiment status at certain time. A model that represents an experiment metric at certain time. A model that represents an experiment chart view.