# Copyright 2022 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from datetime import datetime
from enum import Enum

from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship
from sqlalchemy_utils import ChoiceType

from bms_app import db


now = datetime.now


source_db_to_label = db.Table('source_db_labels', db.Model.metadata,
    db.Column('db_id', db.Integer, db.ForeignKey('source_dbs.id')),
    db.Column('label_id', db.Integer, db.ForeignKey('labels.id'))
)


class Project(db.Model):
    __tablename__ = 'projects'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String, nullable=False, unique=True)
    vpc = db.Column(db.String, nullable=False)
    subnet = db.Column(db.String, nullable=False)
    description = db.Column(db.String, nullable=False)

    waves = relationship('Wave', back_populates='project')
    source_dbs = relationship('SourceDB', back_populates='project')
    labels = relationship('Label', back_populates='project')


class BMSServer(db.Model):
    __tablename__ = 'bms_servers'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String, unique=True)
    state = db.Column(db.String)
    machine_type = db.Column(db.String)
    luns = db.Column(db.JSON)
    networks = db.Column(db.JSON)
    deleted = db.Column(db.Boolean, default=False)
    secret_name = db.Column(db.String)
    cpu = db.Column(db.String)
    socket = db.Column(db.String)
    ram = db.Column(db.String)
    created_at = db.Column(db.DateTime, default=datetime.now)
    location = db.Column(db.String)

    mapping = relationship('Mapping', back_populates='bms')


class SourceDBType(Enum):
    SI = 'SI'
    RAC = 'RAC'
    DG = 'DG'


class SourceDBStatus(Enum):
    EMPTY = 'EMPTY'
    ROLLBACKED = 'ROLLBACKED'
    FAILED = 'FAILED'
    DEPLOYED = 'DEPLOYED'
    PRE_RESTORE = 'PRE_RESTORE'
    PRE_RESTORE_COMPLETE = 'PRE_RESTORE_COMPLETE'
    PRE_RESTORE_FAILED = 'PRE_RESTORE_FAILED'
    DT = 'DT'
    DT_COMPLETE = 'DT_COMPLETE'
    DT_FAILED = 'DT_FAILED'
    DT_PARTIALLY = 'DT_PARTIALLY'
    DT_ROLLBACK = 'DT_ROLLBACK'
    FAILOVER = 'FAILOVER'
    FAILOVER_COMPLETE = 'FAILOVER_COMPLETE'
    FAILOVER_FAILED = 'FAILOVER_FAILED'

class SourceDBEngine(Enum):
    ORACLE = 'ORACLE'
    POSTGRES = 'POSTGRES'


class SourceDB(db.Model):
    __tablename__ = 'source_dbs'
    __table_args__ = (db.UniqueConstraint('server', 'db_name', 'project_id'), )

    DEPLOYABLE_STATUSES = (SourceDBStatus.EMPTY, SourceDBStatus.ROLLBACKED)

    id = db.Column(db.Integer, primary_key=True)
    server = db.Column(db.String, nullable=False)
    db_engine = db.Column(ChoiceType(SourceDBEngine, impl=db.String(20)), default=SourceDBEngine.ORACLE)
    oracle_version = db.Column(db.String)
    oracle_release = db.Column(db.String)
    oracle_edition = db.Column(db.String, default='EE')
    db_type = db.Column(ChoiceType(SourceDBType, impl=db.String(10)), default=SourceDBType.SI)
    rac_nodes = db.Column(db.Integer, default=0)  # value parsed from assessment file
    fe_rac_nodes = db.Column(db.Integer)
    arch = db.Column(db.String)
    cores = db.Column(db.Integer)
    ram = db.Column(db.Integer)
    allocated_memory = db.Column(db.Integer)
    db_name = db.Column(db.String, nullable=False)
    db_size = db.Column(db.Numeric)
    status = db.Column(ChoiceType(SourceDBStatus, impl=db.String(20)), default=SourceDBStatus.EMPTY)
    project_id = db.Column(db.Integer, db.ForeignKey('projects.id'), nullable=False)
    wave_id = db.Column(db.Integer, db.ForeignKey('waves.id'))

    wave = relationship('Wave', back_populates='source_db', uselist=False)
    config = relationship('Config', back_populates='source_db', uselist=False)
    mappings = relationship('Mapping', back_populates='source_db')
    project = relationship(Project, back_populates='source_dbs', uselist=False)
    restore_config = relationship('RestoreConfig', back_populates='source_db', uselist=False)
    scheduled_tasks = relationship('ScheduledTask', back_populates='source_db')

    labels = db.relationship('Label', secondary=source_db_to_label, back_populates='source_dbs', cascade='save-update, merge')

    @hybrid_property
    def is_rac(self):
        return self.db_type == SourceDBType.RAC

    @hybrid_property
    def is_deployable(self):
        return self.status in self.DEPLOYABLE_STATUSES

    @is_deployable.expression
    def is_deployable(cls):
        return cls.status.in_(cls.DEPLOYABLE_STATUSES)


class Config(db.Model):
    __tablename__ = 'configs'

    id = db.Column(db.Integer, primary_key=True)
    db_id = db.Column(db.Integer, db.ForeignKey('source_dbs.id'), unique=True, nullable=False)
    install_config_values = db.Column(db.JSON)
    db_params_values = db.Column(db.JSON)
    data_mounts_values = db.Column(db.JSON)
    asm_config_values = db.Column(db.JSON)
    rac_config_values = db.Column(db.JSON)
    misc_config_values = db.Column(db.JSON)
    dms_config_values = db.Column(db.JSON)
    created_at = db.Column(db.DateTime)
    is_configured = db.Column(db.Boolean, default=False)

    source_db = relationship('SourceDB', back_populates='config')


class Wave(db.Model):
    __tablename__ = 'waves'
    __table_args__ = (db.UniqueConstraint('name', 'project_id'), )

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String, nullable=False)
    project_id = db.Column(db.Integer, db.ForeignKey('projects.id'), nullable=False)
    is_running = db.Column(db.Boolean, default=False)

    project = relationship(Project, back_populates='waves', uselist=False)
    operations = relationship('Operation', back_populates='wave')
    source_db = relationship('SourceDB', back_populates='wave')
    operation_details = relationship('OperationDetails', back_populates='wave')


class Mapping(db.Model):
    __tablename__ = 'mappings'
    __table_args__ = (db.UniqueConstraint('db_id', 'bms_id'), )

    id = db.Column(db.Integer, primary_key=True)
    db_id = db.Column(db.Integer, db.ForeignKey('source_dbs.id'), nullable=False)
    bms_id = db.Column(db.Integer, db.ForeignKey('bms_servers.id'), nullable=False)
    rac_node = db.Column(db.Integer)  # node order in RAC

    bms = relationship('BMSServer', back_populates='mapping')
    source_db = relationship('SourceDB', back_populates='mappings')
    operation_details = relationship('OperationDetails', back_populates='mapping')


class OperationStatus(Enum):
    STARTING = 'STARTING'
    IN_PROGRESS = 'IN_PROGRESS'
    COMPLETE = 'COMPLETE'
    FAILED = 'FAILED'
    COMPLETE_PARTIALLY = 'COMPLETE_PARTIALLY'


class OperationType(Enum):
    DEPLOYMENT = 'DEPLOYMENT'
    ROLLBACK = 'ROLLBACK'
    PRE_RESTORE = 'PRE_RESTORE'
    BACKUP_RESTORE = 'BACKUP_RESTORE'
    IMPORT_EXPORT = 'IMPORT_EXPORT'
    DATA_GUARD = 'DATA_GUARD'
    ROLLBACK_RESTORE = 'ROLLBACK_RESTORE'
    DB_FAILOVER = 'DB_FAILOVER'


class Operation(db.Model):
    __tablename__ = 'operations'

    id = db.Column(db.Integer, primary_key=True)
    wave_id = db.Column(db.Integer, db.ForeignKey('waves.id'), nullable=True)
    operation_type = db.Column(ChoiceType(OperationType, impl=db.String(20)))
    status = db.Column(ChoiceType(OperationStatus, impl=db.String(20)))
    started_at = db.Column(db.DateTime, default=now)
    completed_at = db.Column(db.DateTime)

    wave = relationship('Wave', back_populates='operations', uselist=False)
    operation_details = relationship('OperationDetails', back_populates='operation')

    @hybrid_property
    def is_deployment(self):
        return self.operation_type == OperationType.DEPLOYMENT

    @hybrid_property
    def is_rollback(self):
        return self.operation_type == OperationType.ROLLBACK


class OperationDetails(db.Model):
    __tablename__ = 'operation_details'

    id = db.Column(db.Integer, primary_key=True)
    mapping_id = db.Column(db.Integer, db.ForeignKey('mappings.id'))
    wave_id = db.Column(db.Integer, db.ForeignKey('waves.id'), nullable=True)
    operation_id = db.Column(db.Integer, db.ForeignKey('operations.id'), nullable=False)
    operation_type = db.Column(ChoiceType(OperationType, impl=db.String(20)))
    step = db.Column(db.String(20))
    step_upd_at = db.Column(db.DateTime)
    status = db.Column(ChoiceType(OperationStatus, impl=db.String(20)))
    started_at = db.Column(db.DateTime, default=now)
    completed_at = db.Column(db.DateTime)

    mapping = relationship('Mapping', back_populates='operation_details')
    wave = relationship('Wave', back_populates='operation_details')
    operation = relationship('Operation', back_populates='operation_details')
    errors = relationship('OperationDetailsError', back_populates='operation_details')

    @hybrid_property
    def is_deployment(self):
        return self.operation_type == OperationType.DEPLOYMENT

    @hybrid_property
    def is_rollback(self):
        return self.operation_type == OperationType.ROLLBACK


class OperationDetailsError(db.Model):
    __tablename__ = 'operation_details_errors'

    id = db.Column(db.Integer, primary_key=True)
    operation_details_id = db.Column(db.Integer, db.ForeignKey('operation_details.id'), nullable=False)
    message = db.Column(db.String(255), nullable=False)

    operation_details = relationship('OperationDetails', back_populates='errors', uselist=False)


class BackupType(Enum):
    NONE = ''
    FULL = 'full'
    INCREMENTAL = 'incremental'
    ARCHIVE_LOG = 'archivelogs'

    @classmethod
    def values(cls):
        return [x.value for x in cls]


class RestoreConfig(db.Model):
    __tablename__ = 'restore_configs'

    id = db.Column(db.Integer, primary_key=True)
    db_id = db.Column(db.Integer, db.ForeignKey('source_dbs.id'), unique=True, nullable=False)
    backup_location = db.Column(db.String, nullable=False, server_default='', default='')
    rman_cmd = db.Column(db.Text, nullable=False, server_default='', default='')
    is_configured = db.Column(db.Boolean, nullable=False, server_default='f', default=False)
    pfile_file = db.Column(db.String, nullable=False, server_default='', default='')
    pwd_file = db.Column(db.String, nullable=False, server_default='', default='')
    tnsnames_file = db.Column(db.String, nullable=False, server_default='', default='')
    listener_file = db.Column(db.String, nullable=False, server_default='', default='')
    backup_type = db.Column(ChoiceType(BackupType, impl=db.String()), default='')
    run_pre_restore = db.Column(db.Boolean, nullable=False, default=False, server_default='0')
    control_file = db.Column(db.String, nullable=False, server_default='', default='')
    validations = db.Column(db.JSON, default=list)

    source_db = relationship('SourceDB', back_populates='restore_config')

    @hybrid_property
    def is_full_backup_type(self):
        return self.backup_type is BackupType.FULL


class ScheduledTask(db.Model):
    __tablename__ = 'scheduled_tasks'

    id = db.Column(db.Integer, primary_key=True)
    g_task_name = db.Column(db.String, nullable=False, default='')  # task name in GoogleTask
    schedule_time = db.Column(db.DateTime, nullable=False)  # utc tz
    completed = db.Column(db.Boolean, nullable=False, default=False)
    db_id = db.Column(db.Integer, db.ForeignKey('source_dbs.id'), nullable=False)

    source_db = relationship('SourceDB', back_populates='scheduled_tasks')


class Label(db.Model):
    __tablename__ = 'labels'
    __table_args__ = (db.UniqueConstraint('name', 'project_id'), )

    id = db.Column(db.Integer, primary_key=True)
    project_id = db.Column(db.Integer, db.ForeignKey('projects.id'), nullable=False)
    name = db.Column(db.String(15), nullable=False, index=True)

    project = relationship(Project, back_populates='labels')
    source_dbs = relationship('SourceDB', secondary=source_db_to_label, back_populates='labels', cascade='save-update, merge')


# statuses that mean that operation is alredy finished
FINISHED_OPERATION_STATUSES = (
    OperationStatus.COMPLETE,
    OperationStatus.FAILED
)

# order in which operation status can change
OPERATION_STATUSES_ORDER = [
    OperationStatus.STARTING,
    OperationStatus.IN_PROGRESS,
    OperationStatus.FAILED,  # final
    OperationStatus.COMPLETE,  # final
]

# DB statuses that displayed on Data Transfer Manager page
DATA_TRANSFER_DB_STATUSES = [
    SourceDBStatus.DEPLOYED,
    SourceDBStatus.PRE_RESTORE,
    SourceDBStatus.PRE_RESTORE_COMPLETE,
    SourceDBStatus.PRE_RESTORE_FAILED,
    SourceDBStatus.DT,
    SourceDBStatus.DT_COMPLETE,
    SourceDBStatus.DT_FAILED,
    SourceDBStatus.DT_PARTIALLY,
    SourceDBStatus.DT_ROLLBACK,
    SourceDBStatus.FAILOVER,
    SourceDBStatus.FAILOVER_COMPLETE,
    SourceDBStatus.FAILOVER_FAILED,
]

WAVE_OPERATIONS = [
    OperationType.DEPLOYMENT,
    OperationType.ROLLBACK
]

PRE_RESTORE_ALLOWED_STATUSES = [
    SourceDBStatus.DEPLOYED,
    SourceDBStatus.PRE_RESTORE_FAILED,
    SourceDBStatus.PRE_RESTORE_COMPLETE,
]

RESTORE_ALLOWED_STATUSES = [
    # Also depends on RestoreConfig.run_pre_restore
    # See bms_app.services.operations.utils.is_restore_allowed function
    SourceDBStatus.DT_COMPLETE,
    SourceDBStatus.DT_PARTIALLY,
    SourceDBStatus.FAILOVER_FAILED,
]

ROLLBACK_RESTORE_ALLOWED_STATUSES = [
    SourceDBStatus.DT_COMPLETE,
    SourceDBStatus.DT_PARTIALLY,
    SourceDBStatus.DT_FAILED,
    SourceDBStatus.FAILOVER_FAILED,
    SourceDBStatus.FAILOVER_COMPLETE,
]

FAILOVER_ALLOWED_STATUSES = [
    SourceDBStatus.DT_COMPLETE
]

# statuses which mean that db is deployed
DEPLOYED_STATUSES = [
    SourceDBStatus.DEPLOYED,
    SourceDBStatus.PRE_RESTORE,
    SourceDBStatus.PRE_RESTORE_COMPLETE,
    SourceDBStatus.PRE_RESTORE_FAILED,
    SourceDBStatus.DT,
    SourceDBStatus.DT_COMPLETE,
    SourceDBStatus.DT_FAILED,
    SourceDBStatus.DT_PARTIALLY,
    SourceDBStatus.DT_ROLLBACK,
    SourceDBStatus.FAILOVER,
    SourceDBStatus.FAILOVER_COMPLETE,
    SourceDBStatus.FAILOVER_FAILED,
]
