from datetime import datetime
from typing import Dict, Any, Optional, Tuple, Union, List

from analogai.foundation.modules.belief.belief import Belief
from analogai.foundation.common.enums.relationship_type import RelationshipType
from analogai.foundation.core.value_objects.relation import Relation
from analogai.foundation.common.enums.criterion import Criterion
from analogai.foundation.common.enums.constraint_operator import ConstraintOperator
from analogai.foundation.common.utils.time_utils.time_converter import TimeConverter
from analogai.foundation.core.value_objects.time import Time
from analogai.foundation.common.utils.similarity.similarity_service import SimilarityService
from analogai.foundation.settings import config
from analogai.foundation.common.logging.app_logger import app_logger


def _environment() -> str:
    value = config.get("ENVIRONMENT")
    return str(value).strip().lower()


def _use_vector_search() -> bool:
    if _environment() in {"test", "testing"}:
        return False
    return bool(config.get("vector.enabled", True))


def _similarity_cutoff() -> float:
    value = config.get("vector.cutoff", 0.4)
    try:
        return float(value)
    except (TypeError, ValueError):
        return 0.4


class Condition:
    def __init__(
        self,
        criterion: Criterion,
        operator: ConstraintOperator,
        value: Optional[Any] = None,
    ):
        self.criterion = criterion
        self.operator = operator
        self.value = value


CriterionConstraint = Union[Any, Tuple[Union[str, ConstraintOperator], Any], Condition]


class BeliefCriteriaMatcher:
    @staticmethod
    def matches_clause(
        belief: Belief,
        clause: Dict[Criterion, Union[CriterionConstraint, List[CriterionConstraint]]],
        similarity_service: Optional[SimilarityService] = None,
    ) -> bool:
        for criterion, constraint in clause.items():
            if not BeliefCriteriaMatcher._criterion_matches_constraint(
                belief, criterion, constraint, similarity_service
            ):
                return False
        return True

    @staticmethod
    def _criterion_matches_constraint(
        belief: Belief,
        criterion: Criterion,
        constraint: Union[CriterionConstraint, List[CriterionConstraint]],
        similarity_service: Optional[SimilarityService] = None,
    ) -> bool:
        if type(constraint) is list:
            if constraint and (type(constraint[0]) is tuple or type(constraint[0]) is list) and len(constraint[0]) >= 1:
                first = constraint[0]
                if BeliefCriteriaMatcher._is_operator_constraint(first[0]):
                    return all(
                        BeliefCriteriaMatcher._criterion_matches_constraint(
                            belief, criterion, c, similarity_service
                        )
                        for c in constraint
                    )
            return any(
                BeliefCriteriaMatcher._criterion_matches_from_condition(
                    belief, Condition(criterion, ConstraintOperator.Eq, v), similarity_service
                )
                for v in constraint
            )
        if type(constraint) is Condition:
            return BeliefCriteriaMatcher._criterion_matches_from_condition(
                belief, constraint, similarity_service
            )
        if (type(constraint) is tuple or type(constraint) is list) and len(constraint) >= 1:
            operator = BeliefCriteriaMatcher._normalize_operator(constraint[0])
            value = constraint[1] if len(constraint) > 1 else None
            return BeliefCriteriaMatcher._criterion_matches_from_condition(
                belief, Condition(criterion, operator, value), similarity_service
            )
        return BeliefCriteriaMatcher._criterion_matches_from_condition(
            belief, Condition(criterion, ConstraintOperator.Eq, constraint), similarity_service
        )

    @staticmethod
    def _is_operator_constraint(first: Any) -> bool:
        if type(first) is ConstraintOperator:
            return first in (
                ConstraintOperator.Min,
                ConstraintOperator.Max,
                ConstraintOperator.NotNull,
                ConstraintOperator.Null,
            )
        if type(first) is str:
            return first in ("min", "max", "not_null", "null")
        return False

    @staticmethod
    def _normalize_operator(op: Union[str, ConstraintOperator]) -> ConstraintOperator:
        if type(op) is ConstraintOperator:
            return op
        return ConstraintOperator(op)

    @staticmethod
    def _criterion_matches_from_condition(
        belief: Belief,
        condition: Condition,
        similarity_service: Optional[SimilarityService] = None,
    ) -> bool:
        operator = condition.operator
        value = condition.value
        if operator == ConstraintOperator.Eq:
            return BeliefCriteriaMatcher._criterion_equals(
                belief, condition.criterion, value, similarity_service
            )
        if operator == ConstraintOperator.Min:
            return BeliefCriteriaMatcher._criterion_min(belief, condition.criterion, value)
        if operator == ConstraintOperator.Max:
            return BeliefCriteriaMatcher._criterion_max(belief, condition.criterion, value)
        if operator == ConstraintOperator.NotNull:
            return BeliefCriteriaMatcher._criterion_not_null(belief, condition.criterion)
        if operator == ConstraintOperator.Null:
            return BeliefCriteriaMatcher._criterion_null(belief, condition.criterion)
        return True

    @staticmethod
    def _criterion_equals(
        belief: Belief,
        criterion: Criterion,
        expected: Any,
        similarity_service: Optional[SimilarityService] = None,
    ) -> bool:
        if criterion == Criterion.SubjectCaption:
            return BeliefCriteriaMatcher._text_matches(
                belief.subject_notion.caption,
                expected,
                similarity_service,
            )
        if criterion in (Criterion.ObjectCaption, Criterion.ComplementCaption):
            return BeliefCriteriaMatcher._text_matches(
                belief.complement_notion.caption,
                expected,
                similarity_service,
            )
        if criterion == Criterion.Relation:
            if expected is None:
                return False
            if isinstance(expected, (Relation, RelationshipType)):
                if belief.relation == expected:
                    return True
                if similarity_service and _use_vector_search():
                    score = similarity_service.similarity(
                        belief.relation.value,
                        expected.value,
                    )
                    threshold = _similarity_cutoff()
                    app_logger.debug(
                        "BeliefQuery relation similarity: actual='%s' expected='%s' score=%.4f threshold=%.4f",
                        belief.relation.value,
                        expected.value,
                        score,
                        threshold,
                    )
                    return score >= threshold
                return False
            raise TypeError("Criterion.Relation expects a Relation or RelationshipType")
        if criterion == Criterion.Location:
            return BeliefCriteriaMatcher._belief_has_location(belief, expected)
        if criterion == Criterion.DeonticModality:
            return BeliefCriteriaMatcher._belief_deontic_modality_equals(belief, expected)
        if criterion == Criterion.AttributeTag:
            return BeliefCriteriaMatcher._belief_has_measure_kind(belief, expected)
        if criterion == Criterion.AttributeUnit:
            return BeliefCriteriaMatcher._belief_has_measure_unit(belief, expected)
        if criterion == Criterion.AttributeValue:
            return BeliefCriteriaMatcher._belief_has_measure_value(belief, expected)
        return True

    @staticmethod
    def _criterion_min(belief: Belief, criterion: Criterion, expected: Any) -> bool:
        if criterion == Criterion.Quantity:
            quantity = belief.modifier.quantity if belief.modifier else None
            return quantity is not None and expected is not None and quantity >= float(expected)
        if criterion == Criterion.Time:
            return BeliefCriteriaMatcher._belief_has_time_ge(belief, expected)
        if criterion == Criterion.AttributeMinValue:
            return BeliefCriteriaMatcher._belief_has_measure_number_ge(belief, expected)
        if criterion == Criterion.Probability:
            return expected is not None and float(belief.probability) >= float(expected)
        return True

    @staticmethod
    def _criterion_max(belief: Belief, criterion: Criterion, expected: Any) -> bool:
        if criterion == Criterion.Quantity:
            quantity = belief.modifier.quantity if belief.modifier else None
            return quantity is not None and expected is not None and quantity <= float(expected)
        if criterion == Criterion.Time:
            return BeliefCriteriaMatcher._belief_has_time_le(belief, expected)
        if criterion == Criterion.AttributeMaxValue:
            return BeliefCriteriaMatcher._belief_has_measure_number_le(belief, expected)
        if criterion == Criterion.Probability:
            return expected is not None and float(belief.probability) <= float(expected)
        return True

    @staticmethod
    def _criterion_not_null(belief: Belief, criterion: Criterion) -> bool:
        if criterion == Criterion.Time:
            return BeliefCriteriaMatcher._belief_has_time(belief)
        if criterion == Criterion.DeonticModality:
            return BeliefCriteriaMatcher._belief_has_deontic_modality(belief)
        if criterion in (Criterion.ObjectCaption, Criterion.ComplementCaption):
            return belief.complement_notion.caption != ""
        if criterion == Criterion.Quantity:
            return belief.modifier is not None and belief.modifier.quantity is not None
        if criterion == Criterion.AttributeTag:
            return BeliefCriteriaMatcher._belief_has_any_measure(belief)
        return True

    @staticmethod
    def _criterion_null(belief: Belief, criterion: Criterion) -> bool:
        if criterion == Criterion.Time:
            return not BeliefCriteriaMatcher._belief_has_time(belief)
        if criterion == Criterion.DeonticModality:
            return not BeliefCriteriaMatcher._belief_has_deontic_modality(belief)
        if criterion in (Criterion.ObjectCaption, Criterion.ComplementCaption):
            return belief.complement_notion.caption == ""
        if criterion == Criterion.Quantity:
            return belief.modifier is None or belief.modifier.quantity is None
        if criterion == Criterion.AttributeTag:
            return not BeliefCriteriaMatcher._belief_has_any_measure(belief)
        return False

    @staticmethod
    def _belief_has_any_measure(belief: Belief) -> bool:
        measures = BeliefCriteriaMatcher._collect_notion_measures(belief)
        return len(measures) > 0

    @staticmethod
    def _belief_has_measure_kind(belief: Belief, expected: Any) -> bool:
        if expected is None:
            return False
        expected_kind = str(expected).lower()
        measures = BeliefCriteriaMatcher._collect_notion_measures(belief)
        return any(m.tag.lower() == expected_kind for m in measures)

    @staticmethod
    def _belief_has_measure_unit(belief: Belief, expected: Any) -> bool:
        if expected is None:
            return False
        expected_unit = str(expected).lower()
        measures = BeliefCriteriaMatcher._collect_notion_measures(belief)
        return any(m.unit is not None and m.unit.lower() == expected_unit for m in measures)

    @staticmethod
    def _belief_has_measure_value(belief: Belief, expected: Any) -> bool:
        if expected is None:
            return False
        measures = BeliefCriteriaMatcher._collect_notion_measures(belief)
        if type(expected) is tuple and len(expected) >= 2:
            kind = str(expected[0]).lower()
            value = expected[1]
            unit = expected[2] if len(expected) > 2 else None
            for m in measures:
                if m.tag.lower() != kind:
                    continue
                if unit is not None and (m.unit is None or m.unit.lower() != str(unit).lower()):
                    continue
                return m.value == value
            return False
        return any(m.value == expected for m in measures)

    @staticmethod
    def _belief_has_measure_number_ge(belief: Belief, expected: Any) -> bool:
        kind, unit, number = BeliefCriteriaMatcher._parse_measure_constraint(expected)
        if kind is None or number is None:
            return False
        return BeliefCriteriaMatcher._belief_has_measure_number_compare(
            belief, kind, unit, number, ConstraintOperator.Ge
        )

    @staticmethod
    def _belief_has_measure_number_le(belief: Belief, expected: Any) -> bool:
        kind, unit, number = BeliefCriteriaMatcher._parse_measure_constraint(expected)
        if kind is None or number is None:
            return False
        return BeliefCriteriaMatcher._belief_has_measure_number_compare(
            belief, kind, unit, number, ConstraintOperator.Le
        )

    @staticmethod
    def _parse_measure_constraint(expected: Any) -> tuple[Optional[str], Optional[str], Optional[float]]:
        if type(expected) is tuple and len(expected) >= 2:
            kind = str(expected[0]).lower()
            number = float(expected[1]) if expected[1] is not None else None
            unit = str(expected[2]).lower() if len(expected) > 2 and expected[2] is not None else None
            return kind, unit, number
        return None, None, None

    @staticmethod
    def _belief_has_measure_number_compare(
        belief: Belief,
        kind: str,
        unit: Optional[str],
        number: float,
        op: ConstraintOperator,
    ) -> bool:
        measures = BeliefCriteriaMatcher._collect_notion_measures(belief)
        if not measures:
            return False
        for m in measures:
            if m.tag.lower() != kind:
                continue
            if unit is not None and (m.unit is None or m.unit.lower() != unit):
                continue
            try:
                actual = float(m.value)
            except (TypeError, ValueError):
                continue
            if op == ConstraintOperator.Ge and actual >= number:
                return True
            if op == ConstraintOperator.Le and actual <= number:
                return True
        return False

    @staticmethod
    def _belief_has_time(belief: Belief) -> bool:
        modifier = belief.modifier
        if modifier is None:
            return False
        return TimeConverter.has_time_data(modifier.from_time) or TimeConverter.has_time_data(modifier.to_time)

    @staticmethod
    def _belief_has_time_ge(belief: Belief, expected: Any) -> bool:
        learning_time = BeliefCriteriaMatcher._get_belief_earliest_time(belief)
        if learning_time is None or expected is None:
            return False
        return BeliefCriteriaMatcher._time_compare_ge(learning_time, expected)

    @staticmethod
    def _belief_has_time_le(belief: Belief, expected: Any) -> bool:
        learning_time = BeliefCriteriaMatcher._get_belief_latest_time(belief)
        if learning_time is None or expected is None:
            return False
        return BeliefCriteriaMatcher._time_compare_le(learning_time, expected)

    @staticmethod
    def _belief_has_location(belief: Belief, expected: Any) -> bool:
        modifier = belief.modifier
        if modifier is None or expected is None:
            return False
        location = modifier.location
        return location is not None and str(location).lower() == str(expected).lower()

    @staticmethod
    def _belief_has_deontic_modality(belief: Belief) -> bool:
        modifier = belief.modifier
        if modifier is None:
            return False
        return modifier.deontic_modality is not None

    @staticmethod
    def _belief_deontic_modality_equals(belief: Belief, expected: Any) -> bool:
        modifier = belief.modifier
        if modifier is None or expected is None:
            return False
        actual = modifier.deontic_modality
        if actual is None:
            return False
        return BeliefCriteriaMatcher._deontic_modality_matches(actual, expected)

    @staticmethod
    def _deontic_modality_matches(actual: Any, expected: Any) -> bool:
        if actual == expected:
            return True
        try:
            actual_str = actual.value
        except AttributeError:
            actual_str = str(actual)
        try:
            expected_str = expected.value
        except AttributeError:
            expected_str = str(expected)
        return str(actual_str).lower() == str(expected_str).lower()

    @staticmethod
    def _get_belief_earliest_time(belief: Belief) -> Optional[Time]:
        modifier = belief.modifier
        if modifier is None:
            return None
        from_time = modifier.from_time
        to_time = modifier.to_time
        if TimeConverter.has_time_data(from_time):
            return from_time
        if TimeConverter.has_time_data(to_time):
            return to_time
        return None

    @staticmethod
    def _get_belief_latest_time(belief: Belief) -> Optional[Time]:
        modifier = belief.modifier
        if modifier is None:
            return None
        from_time = modifier.from_time
        to_time = modifier.to_time
        if TimeConverter.has_time_data(to_time):
            return to_time
        if TimeConverter.has_time_data(from_time):
            return from_time
        return None

    @staticmethod
    def _time_to_datetime(time_or_dt: Any) -> Optional[datetime]:
        if time_or_dt is None:
            return None
        if type(time_or_dt) is datetime:
            return time_or_dt
        if type(time_or_dt) is Time:
            reference = datetime.now().astimezone()
            return TimeConverter.to_datetime(time_or_dt, reference)
        return None

    @staticmethod
    def _time_compare_ge(learning_time: Time, expected: Any) -> bool:
        learning_dt = BeliefCriteriaMatcher._time_to_datetime(learning_time)
        expected_dt = BeliefCriteriaMatcher._time_to_datetime(expected)
        if learning_dt is None or expected_dt is None:
            return False
        return learning_dt >= expected_dt

    @staticmethod
    def _time_compare_le(learning_time: Time, expected: Any) -> bool:
        learning_dt = BeliefCriteriaMatcher._time_to_datetime(learning_time)
        expected_dt = BeliefCriteriaMatcher._time_to_datetime(expected)
        if learning_dt is None or expected_dt is None:
            return False
        return learning_dt <= expected_dt

    @staticmethod
    def _collect_notion_measures(belief: Belief) -> List:
        measures: List = []
        if belief.subject_notion and belief.subject_notion.attributes:
            measures.extend(belief.subject_notion.attributes)
        if belief.complement_notion and belief.complement_notion.attributes:
            measures.extend(belief.complement_notion.attributes)
        return measures

    @staticmethod
    def _text_matches(
        actual: str,
        expected: Any,
        similarity_service: Optional[SimilarityService] = None,
    ) -> bool:
        if expected is None:
            return False
        expected_str = str(expected)
        if actual.lower() == expected_str.lower():
            return True
        if _use_vector_search():
            raise RuntimeError(
                "Caption similarity fallback disabled while USE_VECTOR_SEARCH is enabled"
            )
        return False
