# Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/)
#
# 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.

import logging, operator
from typing import Dict, List, Tuple, Union
from common.orm.Database import Database
from common.orm.HighLevel import get_object, get_or_create_object, update_or_create_object
from common.orm.backend.Tools import key_to_str
from common.orm.fields.ForeignKeyField import ForeignKeyField
from common.orm.fields.IntegerField import IntegerField
from common.orm.fields.PrimaryKeyField import PrimaryKeyField
from common.orm.fields.StringField import StringField
from common.orm.model.Model import Model
from service.service.database.Tools import fast_hasher, remove_dict_key

LOGGER = logging.getLogger(__name__)

class ConstraintsModel(Model): # pylint: disable=abstract-method
    pk = PrimaryKeyField()

    def dump(self) -> List[Dict]:
        db_constraint_pks = self.references(ConstraintModel)
        constraints = [ConstraintModel(self.database, pk).dump(include_position=True) for pk,_ in db_constraint_pks]
        constraints = sorted(constraints, key=operator.itemgetter('position'))
        return [remove_dict_key(constraint, 'position') for constraint in constraints]

class ConstraintModel(Model): # pylint: disable=abstract-method
    pk = PrimaryKeyField()
    constraints_fk = ForeignKeyField(ConstraintsModel)
    position = IntegerField(min_value=0, required=True)
    constraint_type = StringField(required=True, allow_empty=False)
    constraint_value = StringField(required=True, allow_empty=False)

    def dump(self, include_position=True) -> Dict: # pylint: disable=arguments-differ
        result = {
            'constraint_type': self.constraint_type,
            'constraint_value': self.constraint_value,
        }
        if include_position: result['position'] = self.position
        return result

def delete_all_constraints(database : Database, db_parent_pk : str, constraints_name : str) -> None:
    str_constraints_key = key_to_str([db_parent_pk, constraints_name], separator=':')
    db_constraints : ConstraintsModel = get_object(
        database, ConstraintsModel, str_constraints_key, raise_if_not_found=False)
    if db_constraints is None: return
    db_constraint_pks = db_constraints.references(ConstraintModel)
    for pk,_ in db_constraint_pks: ConstraintModel(database, pk).delete()

def grpc_constraints_to_raw(grpc_constraints) -> List[Tuple[str, str]]:
    return [
        (grpc_constraint.constraint_type, grpc_constraint.constraint_value)
        for grpc_constraint in grpc_constraints
    ]

def get_constraints(database : Database, db_parent_pk : str, constraints_name : str) -> List[Tuple[str, str]]:
    str_constraints_key = key_to_str([db_parent_pk, constraints_name], separator=':')
    db_constraints : ConstraintsModel = get_object(
        database, ConstraintsModel, str_constraints_key, raise_if_not_found=False)
    return [] if db_constraints is None else [
        (constraint['constraint_type'], constraint['constraint_value'])
        for constraint in db_constraints.dump()
    ]

def update_constraints(
    database : Database, db_parent_pk : str, constraints_name : str, raw_constraints : List[Tuple[str, str]]
    ) -> List[Tuple[Union[ConstraintsModel, ConstraintModel], bool]]:

    str_constraints_key = key_to_str([db_parent_pk, constraints_name], separator=':')
    result : Tuple[ConstraintsModel, bool] = get_or_create_object(database, ConstraintsModel, str_constraints_key)
    db_constraints, created = result

    db_objects : List[Tuple[Union[ConstraintsModel, ConstraintModel], bool]] = [(db_constraints, created)]

    for position,(constraint_type, constraint_value) in enumerate(raw_constraints):
        str_constraint_key_hash = fast_hasher(constraint_type)
        str_constraint_key = key_to_str([db_constraints.pk, str_constraint_key_hash], separator=':')
        result : Tuple[ConstraintModel, bool] = update_or_create_object(
            database, ConstraintModel, str_constraint_key, {
                'constraints_fk': db_constraints, 'position': position, 'constraint_type': constraint_type,
                'constraint_value': constraint_value})
        db_constraints_rule, updated = result
        db_objects.append((db_constraints_rule, updated))

    return db_objects
