from typing import Optional, Iterable, List, TYPE_CHECKING, Dict
from abc import ABC, abstractmethod

from analogai.foundation.common.logging.app_logger import app_logger
from analogai.foundation.common.utils.statement_serializers import serialize_statement
from analogai.foundation.common.utils.modifier_utils import ModifierBuilder
from analogai.foundation.modules.conditionals.conditional_deduction import ConditionalDeduction
from analogai.foundation.modules.conditionals.hypothetical_statement import HypotheticalStatement
from analogai.foundation.modules.notion.notion import Notion
from analogai.foundation.modules.notion.notion_service import NotionService
from analogai.foundation.common.dtos.notion_expression import NotionExpression
from analogai.foundation.common.enums.criterion import Criterion
from analogai.foundation.common.enums.certainty import certainty_from_value
from analogai.foundation.common.utils.belief_criteria_matcher import BeliefCriteriaMatcher
from analogai.foundation.core.value_objects.modifier import Modifier
from analogai.foundation.core.value_objects.relation import Relation
from analogai.foundation.core.value_objects.time import Time
from analogai.foundation.core.value_objects.attribute import Attribute
from analogai.foundation.common.enums.deontic_modality import DeonticModality

if TYPE_CHECKING:
    from analogai.foundation.modules.belief.belief_service import BeliefService


class CriterionValueResolver(ABC):
    @abstractmethod
    def resolve(self, value: object):
        raise NotImplementedError


class TimeValueResolver(CriterionValueResolver):
    def resolve(self, value: object) -> Optional[Time]:
        if value is None:
            return None
        if isinstance(value, Time):
            return value
        raise TypeError("Criterion time value must be a Time instance")


class DeonticValueResolver(CriterionValueResolver):
    def resolve(self, value: object) -> Optional[DeonticModality]:
        if value is None:
            return None
        if isinstance(value, DeonticModality):
            return value
        raise TypeError("Criterion deontic modality must be a DeonticModality instance")


class AttributesValueResolver(CriterionValueResolver):
    def resolve(self, value: object) -> Optional[List[Attribute]]:
        if value is None:
            return None
        if not isinstance(value, list):
            raise TypeError("Criterion attributes must be a list of Attribute instances")
        if any(not isinstance(item, Attribute) for item in value):
            raise TypeError("Criterion attributes must be a list of Attribute instances")
        return list(value)


class ConditionalDeductionService:
    def __init__(
        self,
        hypothetical_statement_service,
        notion_service: NotionService,
        belief_service: "BeliefService",
    ):
        if belief_service is None:
            raise ValueError("BeliefService is required for conditional deductions")
        if notion_service is None:
            raise ValueError("NotionService is required for conditional deductions")
        if hypothetical_statement_service is None:
            raise ValueError("HypotheticalStatementService is required for conditional deductions")
        self._hypothetical_statement_service = hypothetical_statement_service
        self._notion_service = notion_service
        self._belief_service = belief_service
        self._criteria_resolvers: Dict[Criterion, "CriterionValueResolver"] = {
            Criterion.DeonticModality: DeonticValueResolver(),
            Criterion.FromTime: TimeValueResolver(),
            Criterion.ToTime: TimeValueResolver(),
            Criterion.AttributeValue: AttributesValueResolver(),
        }

    def _has_modifier_context(self, modifier: Optional[Modifier]) -> bool:
        if modifier is None:
            return False
        if modifier.location is not None:
            return True
        if modifier.deontic_modality is not None:
            return True
        if modifier.from_time is not None:
            return True
        if modifier.to_time is not None:
            return True
        if modifier.attributes:
            return True
        if modifier.quantity is not None:
            return True
        return False

    def _build_effect_modifier(
        self,
        effect_criteria: dict,
        base_modifier: Optional[Modifier],
    ) -> Optional[Modifier]:
        builder = ModifierBuilder()
        if base_modifier is not None:
            if base_modifier.location is not None:
                builder.with_location(base_modifier.location)
            if base_modifier.deontic_modality is not None:
                builder.with_deontic_modality(base_modifier.deontic_modality)
            if base_modifier.from_time is not None:
                builder.with_from_time(
                    year=base_modifier.from_time.year,
                    month=base_modifier.from_time.month,
                    day=base_modifier.from_time.day,
                    hour=base_modifier.from_time.hour,
                    minute=base_modifier.from_time.minute,
                )
            if base_modifier.to_time is not None:
                builder.with_to_time(
                    year=base_modifier.to_time.year,
                    month=base_modifier.to_time.month,
                    day=base_modifier.to_time.day,
                    hour=base_modifier.to_time.hour,
                    minute=base_modifier.to_time.minute,
                )
            if base_modifier.attributes is not None:
                builder.with_attributes(list(base_modifier.attributes))
            if base_modifier.quantity is not None:
                builder.with_quantity(base_modifier.quantity)

        location = effect_criteria.get(Criterion.Location)
        if location is not None:
            builder.with_location(location)

        deontic = self._criteria_resolvers[Criterion.DeonticModality].resolve(
            effect_criteria.get(Criterion.DeonticModality)
        )
        if deontic is not None:
            builder.with_deontic_modality(deontic)

        from_time = self._criteria_resolvers[Criterion.FromTime].resolve(
            effect_criteria.get(Criterion.FromTime)
        )
        if from_time is not None:
            builder.with_from_time(
                year=from_time.year,
                month=from_time.month,
                day=from_time.day,
                hour=from_time.hour,
                minute=from_time.minute,
            )

        to_time = self._criteria_resolvers[Criterion.ToTime].resolve(
            effect_criteria.get(Criterion.ToTime)
        )
        if to_time is not None:
            builder.with_to_time(
                year=to_time.year,
                month=to_time.month,
                day=to_time.day,
                hour=to_time.hour,
                minute=to_time.minute,
            )

        attributes = self._criteria_resolvers[Criterion.AttributeValue].resolve(
            effect_criteria.get(Criterion.AttributeValue)
        )
        if attributes is not None:
            builder.with_attributes(attributes)

        quantity = effect_criteria.get(Criterion.Quantity)
        if quantity is not None:
            builder.with_quantity(quantity)

        modifier = builder.build()
        if not self._has_modifier_context(modifier):
            return None
        return modifier

    def resolve_deductions(
        self,
        agent_id: str,
        beliefs: Iterable,
        modifier: Optional[Modifier] = None,
        user_id: Optional[str] = None,
        hypothetical_statements: Optional[List[HypotheticalStatement]] = None,
    ) -> List[ConditionalDeduction]:
        if hypothetical_statements is None:
            if self._hypothetical_statement_service is None:
                raise ValueError("HypotheticalStatementService is not configured")
            hypothetical_statements = self._hypothetical_statement_service.find(agent_id)
        if hypothetical_statements is None:
            hypothetical_statements = []

        deductions: List[ConditionalDeduction] = []
        beliefs_list = list(beliefs)
        for hypothetical in hypothetical_statements:
            if not hypothetical.causes:
                continue
            if not self._causes_match_beliefs(hypothetical, beliefs_list):
                continue
            effect_clause = hypothetical.effect
            subject_caption = effect_clause.criteria.get(Criterion.SubjectCaption)
            object_caption = effect_clause.criteria.get(Criterion.ObjectCaption)
            relation_value = effect_clause.criteria.get(Criterion.Relation)
            if not subject_caption or not object_caption or not relation_value:
                continue
            if not isinstance(relation_value, Relation):
                raise TypeError("Criterion.Relation must be a Relation instance")
            subject_notion = self._resolve_notion(agent_id, subject_caption, user_id)
            complement_notion = self._resolve_notion(agent_id, object_caption, user_id)
            relation = relation_value
            deduction_modifier = self._build_effect_modifier(effect_clause.criteria, modifier)
            certainty_value = effect_clause.criteria.get(Criterion.Certainty)
            probability = certainty_from_value(certainty_value).to_probability()
            deduction = ConditionalDeduction(
                agent_id=agent_id,
                hypothetical_statement=hypothetical,
                subject_notion=subject_notion,
                complement_notion=complement_notion,
                relation=relation,
                probability=probability,
                validity=hypothetical.validity,
                modifier=deduction_modifier,
            )
            deductions.append(deduction)

            deduction_serialized = serialize_statement(
                subject_notion_caption=deduction.subject_notion.caption,
                complement_notion_caption=deduction.complement_notion.caption,
                relation=deduction.relation,
                probability=float(deduction.probability) if deduction.probability is not None else 1.0,
                modifier=deduction.modifier,
                subject_attributes=deduction.subject_notion.attributes,
                complement_attributes=deduction.complement_notion.attributes,
            )
            app_logger.debug(
                "Resolved ConditionalDeduction: %s (id: %s)",
                deduction_serialized,
                deduction.id,
            )

        return deductions

    def find_deductions_by_agent_id(
        self,
        agent_id: str,
        modifier: Optional[Modifier] = None,
        user_id: Optional[str] = None,
        hypothetical_statements: Optional[List[HypotheticalStatement]] = None,
        limit: int = 0,
        offset: int = 0,
    ) -> List[ConditionalDeduction]:
        if self._belief_service is None:
            raise ValueError("BeliefService is not configured")
        beliefs = self._belief_service.get_all_for_agent(
            agent_id,
        )
        filtered = list(beliefs)
        if offset or limit > 0:
            start = offset
            end = offset + limit if limit > 0 else None
            filtered = filtered[start:end]
        return self.resolve_deductions(
            agent_id=agent_id,
            beliefs=filtered,
            modifier=modifier,
            user_id=user_id,
            hypothetical_statements=hypothetical_statements,
        )

    def _resolve_notion(self, agent_id: str, caption: str, user_id: Optional[str]) -> Notion:
        if self._notion_service is not None:
            return self._notion_service.find_or_create(
                user_id=user_id if user_id else "system",
                agent_id=agent_id,
                expression=NotionExpression(caption=caption),
            )
        return Notion(user_id=user_id if user_id else "system", agent_id=agent_id, caption=caption, id=caption)

    def _causes_match_beliefs(self, hypothetical: HypotheticalStatement, beliefs: List) -> bool:
        for cause in hypothetical.causes:
            if not any(BeliefCriteriaMatcher.matches_clause(belief, cause.criteria) for belief in beliefs):
                return False
        return True