#  Copyright (C) 2023
#  ABM, Moscow
#
#  UNPUBLISHED PROPRIETARY MATERIAL.
#  ALL RIGHTS RESERVED.
#
#  Authors: Mike Orlov <m.orlov@abm-jsc.ru>
import abc
import copy
import dataclasses
import functools
from dataclasses import Field
from logging import getLogger
from typing import Callable, Union, Self, Mapping

import sqlalchemy
import sqlalchemy.orm
from dynamic_types.class_name import prepare_class_name
from dynamic_types.create import create_dataclass
from no_value import NoValue
from type_cast import get_dataclass_field_name_to_field, to_dataclass

from entity_read.entity import Entity
from entity_read.entity_field import get_sqlalchemy_metadata
from .abstract import AbstractView
from .relation_delete import RelationDeleteView
from .relation_insert import RelationInsertView
from .utils import is_list_relation, get_related_entity_type, get_remote_column
from ..utils.no_default import NoDefault

logger = getLogger(__name__)


@functools.cache
def _create_delta_value_dataclass(base_type: type) -> type:
    return create_dataclass(
        prepare_class_name([base_type], 'delta'), [], {
            "old": base_type,
            "new": base_type
        }
    )


@functools.cache
def _create_delta_relation_dataclass(relation: sqlalchemy.orm.RelationshipProperty) -> type:
    # sorry for this: fast way to break circular dependency
    from .relation_delta import RelationDeltaView
    related_entity_type: type[Entity] = get_related_entity_type(relation)
    return create_dataclass(
        prepare_class_name([related_entity_type], 'delta'), [], {
            "create": list[RelationInsertView[relation]],
            "update": list[RelationDeltaView[relation]],
            "delete": list[RelationDeleteView[relation]],
        }
    )


class _DeltaViewMixin:
# class _DeltaViewMixin(abc.ABC):
    # @classmethod
    # @abc.abstractmethod
    # def get_non_primary_key_names(cls) -> tuple[str, ...]: pass
    #
    # @classmethod
    # @abc.abstractmethod
    # def get_key_to_relation(cls) -> Mapping[str, sqlalchemy.orm.Relationship]: pass

    def has_changes(self) -> bool:
        return any(getattr(self, name) not in (NoValue, NoDefault) for name in self.get_non_primary_key_names()) or \
               any(getattr(self, name) for name in self.get_key_to_relation())

    @classmethod
    def _produce_field_name_to_field(cls, related_entity_type: type[Entity],
                                     relation_remote_column: sqlalchemy.Column = None) -> dict[str, Field | Callable]:
        # sorry for this: fast way to break circular dependency
        from .relation_delta import RelationDeltaView
        field_name_to_field = copy.copy(get_dataclass_field_name_to_field(related_entity_type))
        result = {}
        for field_name, field in field_name_to_field.items():
            field_copy = copy.copy(field)
            sql_alchemy_property = get_sqlalchemy_metadata(field)

            if isinstance(sql_alchemy_property, sqlalchemy.orm.RelationshipProperty):
                relation: sqlalchemy.orm.RelationshipProperty = sql_alchemy_property
                field_copy.default = NoValue
                field_copy.default_factory = dataclasses.MISSING
                # TODO: think about non list case: how should we distinct different view types after serialisation(json)
                field_copy.type = _create_delta_relation_dataclass(relation) if is_list_relation(relation) else Union[
                    RelationInsertView[relation], RelationDeltaView[relation], RelationDeleteView[relation]]

            elif isinstance(sql_alchemy_property, sqlalchemy.Column):
                column: sqlalchemy.Column = sql_alchemy_property
                if relation_remote_column is not None and column == relation_remote_column:
                    field_copy.init = False
                    field_copy.default = NoValue
                else:
                    field_copy.default = NoDefault if column.primary_key else NoValue
                if not column.primary_key:
                    field_copy.type = _create_delta_value_dataclass(field_copy.type)
            else:
                logger.warning(f"ignored attribute: %s, cos %s has unexpected type", field, sql_alchemy_property)

            result[field_name] = field_copy

        result['entity_type'] = related_entity_type
        return result

    # def get_affected_types_and_objects_amount(self) -> tuple[int, int]:
    #     ...

    _base = None

    def get_base(self) -> Entity:
        return self._base

    @classmethod
    def from_comparison(cls, old: Entity, new: Entity) -> Self:
        if isinstance(old, _DeltaViewMixin) or isinstance(new, _DeltaViewMixin):
            raise TypeError(f"DeltaViews cannot be compared")
        old_type = old.entity_type if isinstance(old, AbstractView) else type(old)
        new_type = new.entity_type if isinstance(new, AbstractView) else type(new)
        if old_type != new_type:
            raise TypeError(f"DeltaView requires entities and/or views of same entity type,got: {old_type}, {new_type}")
        entity_type = old_type

        delta_key_to_value = old.get_primary_key_name_to_value()

        for column_name in entity_type.get_non_primary_key_names():
            old_value = getattr(old, column_name)
            new_value = getattr(new, column_name, NoDefault)
            if new_value not in (NoValue, NoDefault) and new_value != old_value:
                if old_value in (NoValue, NoDefault):
                    raise ValueError(f'"old" argument MUST have all attributes present in "new", {column_name=}')
                delta_key_to_value[column_name] = {"old": old_value, "new": new_value}

        for relation_name, relation in entity_type.get_key_to_relation().items():
            old_value = getattr(old, relation_name)
            new_value = getattr(new, relation_name)
            if new_value in (NoValue, NoDefault):
                continue
            if old_value in (NoValue, NoDefault):
                raise ValueError(f'"old" argument MUST have all attributes present in "new", {relation_name=}')
            if not relation.uselist:
                old_value = [old_value]
                new_value = [new_value]

            insert_views, delta_views, delete_views = _compare_values(old_value, new_value, relation)
            delta_key_to_value[relation_name] = {"create": insert_views, "update": delta_views, "delete": delete_views}

        result = to_dataclass(delta_key_to_value, cls[entity_type])
        result._base = old
        return result


def _compare_values(
        left_values: list[Entity], right_values: list[Entity], relation
) -> tuple[list[RelationInsertView], list[_DeltaViewMixin], list[RelationDeleteView]]:
    from entity_read.view.relation_delta import RelationDeltaView
    related_entity_type: type[Entity] = get_related_entity_type(relation)
    remote_column = get_remote_column(relation)

    non_relational_primary_key_names: list[str] = [col.key for col in related_entity_type.get_primary_key_columns() if col != remote_column]
    left_primary_keys_to_value: dict[tuple, Entity] = {tuple(getattr(val, name) for name in non_relational_primary_key_names): val for val in left_values}

    right_values_without_primary_keys: list[Entity] = []
    right_primary_keys_to_value: dict[tuple, Entity] = {}
    for val in right_values:
        primary_key_values = tuple(getattr(val, name) for name in non_relational_primary_key_names)
        if None in primary_key_values or NoValue in primary_key_values or NoDefault in primary_key_values:
            right_values_without_primary_keys.append(val)
        else:
            right_primary_keys_to_value[primary_key_values] = val

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

    required_but_missing_right_values: list[Entity] = right_values_without_primary_keys + [
        val for keys, val in right_primary_keys_to_value.items() if keys not in left_primary_keys_to_value]

    existent_but_unused_left_values = []
    updated_values = []
    for keys, left_value in left_primary_keys_to_value.items():
        if right_value := right_primary_keys_to_value.get(keys):
            if delta := RelationDeltaView[relation].from_comparison(left_value, right_value):
                updated_values.append(delta)
        else:
            existent_but_unused_left_values.append(left_value)

    # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
    identifier_column_names = [col.name for col in related_entity_type.get_identifier_columns() if col != remote_column]
    _optimize_created_updated_deleted(identifier_column_names, right_values_without_primary_keys,
        required_but_missing_right_values, updated_values, existent_but_unused_left_values)

    insert_views = [to_dataclass(val, RelationInsertView[relation]) for val in required_but_missing_right_values]
    delta_views = [to_dataclass(val, RelationDeltaView[relation]) for val in updated_values]
    delete_views = [to_dataclass(val, RelationDeleteView[relation]) for val in existent_but_unused_left_values]
    return insert_views, delta_views, delete_views


def _optimize_created_updated_deleted(
        identifier_column_names, right_values_without_primary_keys,
        required_but_missing_right_values, updated_values, existent_but_unused_left_values
) -> None:  # updated in place
    identifier_to_required_but_missing_relation_values = {}
    for required in right_values_without_primary_keys:
        identifier: tuple = tuple(getattr(required, name) for name in identifier_column_names)
        identifier_to_required_but_missing_relation_values.setdefault(identifier, []).append(required)

    for existent in existent_but_unused_left_values[:]:  # copy, coz is changed in iteration
        identifier: tuple = tuple(getattr(existent, name) for name in identifier_column_names)
        candidates = identifier_to_required_but_missing_relation_values.get(identifier, [])
        if not candidates:
            continue
        best_candidate = next(iter(candidates))
        from entity_read.view.entity_delta import EntityDeltaView
        best_candidate_delta = EntityDeltaView.from_comparison(existent, best_candidate)
        # TODO: implement best candidate selection
        # best_candidate = None
        # best_candidate_delta = None
        # for candidate in candidates:
        #     candidate_delta = EntityDeltaView.from_comparison(existent, candidate)
        #     if best_candidate_delta is None:
        #         best_candidate_delta, best_candidate = candidate_delta, candidate
        #     else:
        #         candidate_delta_complexity: tuple[int, int] = candidate_delta.get_affected_types_and_objects_amount()
        #         # TODO: use cache here somehow
        #         best_candidate_delta_complexity: tuple[int, int] = best_candidate_delta.get_types_and_objects_amount()
        #         if candidate_delta_complexity < best_candidate_delta_complexity:
        #             best_candidate_delta, best_candidate = candidate_delta, candidate

        candidates.remove(best_candidate)
        if best_candidate_delta.has_changes():
            updated_values.append(best_candidate_delta)
        required_but_missing_right_values.remove(best_candidate)
        existent_but_unused_left_values.remove(existent)