from typing import Any, Dict, List, Optional, Set, Tuple
from common.rpc_method_wrapper.ServiceExceptions import NotFoundException
from common.orm.Database import Database
from common.orm.backend.Tools import key_to_str
from common.orm.fields.ForeignKeyField import ForeignKeyField
from common.orm.model.Model import Model, MetaModel

def get_all_objects(database : Database, model_class : MetaModel) -> List[Model]:
    db_pks = sorted(list(model_class.get_primary_keys(database)))
    return [model_class(database, db_pk) for db_pk in db_pks]

def get_object(
    database : Database, model_class : Model, key_parts : List[str], raise_if_not_found : bool = True
    ) -> Optional[Model]:

    str_key = key_to_str(key_parts)
    db_object = model_class(database, str_key, auto_load=False)
    found = db_object.load()
    if found: return db_object
    if raise_if_not_found: raise NotFoundException(model_class.__name__.replace('Model', ''), str_key)
    return None

def get_related_objects(
    source_instance : Model, reference_model_class : MetaModel, navigation_field_name : str = None) -> Set[Model]:

    database = source_instance.database
    db_target_instances = set()

    if navigation_field_name is not None:
        navigation_fk_field : Optional[ForeignKeyField] = getattr(reference_model_class, navigation_field_name, None)
        if navigation_fk_field is None or not isinstance(navigation_fk_field, ForeignKeyField):
            msg = 'navigation_field_name({:s}) must be a ForeignKeyField in reference_model_class({:s})'
            raise AttributeError(msg.format(navigation_field_name, reference_model_class.__name__))
        target_model_class = navigation_fk_field.foreign_model

    for db_reference_pk,_ in source_instance.references(reference_model_class):
        db_reference = reference_model_class(database, db_reference_pk)
        if navigation_field_name is not None:
            target_fk_field = getattr(db_reference, navigation_field_name, None)
            if target_fk_field is None: continue
            db_reference = target_model_class(database, target_fk_field)
        db_target_instances.add(db_reference)
    return db_target_instances

def update_or_create_object(
    database : Database, model_class : Model, key_parts : List[str], attributes : Dict[str, Any]
    ) -> Tuple[Model, bool]:

    str_key = key_to_str(key_parts)
    db_object : Model = model_class(database, str_key, auto_load=False)
    found = db_object.load()
    for attr_name, attr_value in attributes.items():
        setattr(db_object, attr_name, attr_value)
    db_object.save()
    updated = found # updated if found, else created
    return db_object, updated

def get_or_create_object(
    database : Database, model_class : Model, key_parts : List[str], defaults : Dict[str, Any] = {}
    ) -> Tuple[Model, bool]:

    str_key = key_to_str(key_parts)
    db_object : Model = model_class(database, str_key, auto_load=False)
    found = db_object.load()
    if not found:
        for attr_name, attr_value in defaults.items():
            setattr(db_object, attr_name, attr_value)
        db_object.save()
    created = not found # created if not found, else loaded
    return db_object, created
