# Copyright 2019-2025 SURF, GÉANT.
# 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.

from sqlalchemy import select, text
from sqlalchemy.exc import ProgrammingError
from sqlalchemy_utils import Ltree

from orchestrator.db import db
from orchestrator.db.database import WrappedSession
from orchestrator.db.models import AiSearchIndex
from orchestrator.search.aggregations import AggregationType
from orchestrator.search.core.types import EntityType, FieldType
from orchestrator.search.filters import FilterCondition, FilterTree, LtreeFilter, PathFilter
from orchestrator.search.filters.definitions import operators_for
from orchestrator.search.query.exceptions import (
    EmptyFilterPathError,
    IncompatibleAggregationTypeError,
    IncompatibleFilterTypeError,
    IncompatibleTemporalGroupingTypeError,
    InvalidEntityPrefixError,
    InvalidLtreePatternError,
    PathNotFoundError,
)
from orchestrator.search.query.mixins import OrderBy


def is_filter_compatible_with_field_type(filter_condition: FilterCondition, field_type: FieldType) -> bool:
    """Check whether a filter condition is compatible with a given field type.

    Args:
        filter_condition (FilterCondition): The filter condition instance to check.
        field_type (FieldType): The type of field from the index schema.

    Returns:
        bool: True if the filter condition is valid for the given field type, False otherwise.
    """

    # LtreeFilter is for path filtering only and is thus compatible with all field types.
    if isinstance(filter_condition, LtreeFilter):
        return True

    # Get valid operators for this field type and check if the filter's operator is valid.
    valid_operators = operators_for(field_type)
    return filter_condition.op in valid_operators


def is_lquery_syntactically_valid(pattern: str, db_session: WrappedSession) -> bool:
    """Validate whether a string is a syntactically correct `lquery` pattern.

    Args:
        pattern (str): The LTree lquery pattern string to validate.
        db_session (WrappedSession): The database session used to test casting.

    Returns:
        bool: True if the pattern is valid, False if it fails to cast in PostgreSQL.
    """

    try:
        with db_session.begin_nested():
            db_session.execute(text("SELECT CAST(:pattern AS lquery)"), {"pattern": pattern})
        return True
    except ProgrammingError:
        return False


def get_structured_filter_schema() -> dict[str, str]:
    """Retrieve all distinct filterable paths and their field types from the index.

    Returns:
        Dict[str, str]: Mapping of path strings to their corresponding field type values.
    """

    stmt = select(AiSearchIndex.path, AiSearchIndex.value_type).distinct().order_by(AiSearchIndex.path)
    result = db.session.execute(stmt)
    return {str(path): value_type.value for path, value_type in result}


def validate_filter_path(path: str) -> str | None:
    """Check if a given path exists in the index and return its field type.

    Args:
        path (str): The fully qualified LTree path.

    Returns:
        Optional[str]: The value type of the field if found, otherwise None.
    """

    stmt = select(AiSearchIndex.value_type).where(AiSearchIndex.path == Ltree(path)).limit(1)
    result = db.session.execute(stmt).scalar_one_or_none()
    return result.value if result else None


async def complete_filter_validation(filter: PathFilter, entity_type: EntityType) -> None:
    """Validate a PathFilter against the database schema and entity type.

    Checks performed:
    1. LTree filter syntax (for LtreeFilter only)
    2. Non-empty path
    3. Path exists in the database schema
    4. Filter type matches the field's value_type
    5. Path starts with the correct entity type prefix (unless wildcard)

    Args:
        filter (PathFilter): The filter to validate.
        entity_type (EntityType): The entity type being searched.

    Raises:
        ValueError: If any of the validation checks fail.
    """

    # Ltree is a special case
    if isinstance(filter.condition, LtreeFilter):
        lquery_pattern = filter.condition.value
        if not is_lquery_syntactically_valid(lquery_pattern, db.session):
            raise InvalidLtreePatternError(lquery_pattern)
        return

    if not filter.path or not filter.path.strip():
        raise EmptyFilterPathError()

    # 1. Check if path exists in database
    db_field_type_str = validate_filter_path(filter.path)
    if db_field_type_str is None:
        raise PathNotFoundError(filter.path)

    db_field_type = FieldType(db_field_type_str)

    # 2. Check filter compatibility with field type
    if not is_filter_compatible_with_field_type(filter.condition, db_field_type):
        expected_operators = operators_for(db_field_type)
        raise IncompatibleFilterTypeError(
            filter.condition.op.value, db_field_type.value, filter.path, expected_operators
        )

    # 3. Check entity type prefix requirements (unless it's a wildcard path)
    expected_prefix = f"{entity_type.value.lower()}."
    if not filter.path.startswith(expected_prefix) and not filter.path.startswith("*"):
        raise InvalidEntityPrefixError(filter.path, expected_prefix, entity_type.value)


async def validate_filter_tree(filters: FilterTree | None, entity_type: EntityType) -> None:
    """Validate all PathFilter leaves in a FilterTree."""
    if filters is None:
        return
    for leaf in filters.get_all_leaves():
        await complete_filter_validation(leaf, entity_type)


def validate_aggregation_field(agg_type: AggregationType, field_path: str) -> None:
    """Validate that an aggregation field exists and is compatible with the aggregation type.

    Note: Only for FieldAggregations (SUM, AVG, MIN, MAX). COUNT does not require field validation.

    Args:
        agg_type: The aggregation type enum
        field_path: The field path to validate

    Raises:
        PathNotFoundError: If the field doesn't exist in the database.
        IncompatibleAggregationTypeError: If the field type is incompatible with the aggregation type.
    """
    # Check if field exists in database
    field_type_str = validate_filter_path(field_path)
    if field_type_str is None:
        raise PathNotFoundError(field_path)

    # Validate field type compatibility with aggregation type
    if agg_type in (AggregationType.SUM, AggregationType.AVG):
        if field_type_str not in (FieldType.INTEGER.value, FieldType.FLOAT.value):
            raise IncompatibleAggregationTypeError(
                agg_type.value, field_type_str, field_path, [FieldType.INTEGER.value, FieldType.FLOAT.value]
            )
    elif agg_type in (AggregationType.MIN, AggregationType.MAX):
        if field_type_str not in (FieldType.INTEGER.value, FieldType.FLOAT.value, FieldType.DATETIME.value):
            raise IncompatibleAggregationTypeError(
                agg_type.value,
                field_type_str,
                field_path,
                [FieldType.INTEGER.value, FieldType.FLOAT.value, FieldType.DATETIME.value],
            )


def validate_temporal_grouping_field(field_path: str) -> None:
    """Validate that a field exists and is a datetime type for temporal grouping.

    Args:
        field_path: The field path to validate

    Raises:
        PathNotFoundError: If the field doesn't exist in the database
        IncompatibleTemporalGroupingTypeError: If the field is not a datetime type
    """
    # Check if field exists in database
    field_type_str = validate_filter_path(field_path)
    if field_type_str is None:
        raise PathNotFoundError(field_path)

    # Validate field type is datetime
    if field_type_str != FieldType.DATETIME.value:
        raise IncompatibleTemporalGroupingTypeError(field_path, field_type_str)


def validate_grouping_fields(group_by_paths: list[str]) -> None:
    """Validate that all grouping field paths exist in the database.

    Args:
        group_by_paths: List of field paths to group by

    Raises:
        PathNotFoundError: If any path doesn't exist in the database
    """
    for path in group_by_paths:
        field_type = validate_filter_path(path)
        if field_type is None:
            raise PathNotFoundError(path)


def validate_order_by_fields(order_by: list[OrderBy] | None) -> None:
    """Validate that order_by field paths exist in the database.

    Args:
        order_by: List of ordering instructions, or None

    Raises:
        PathNotFoundError: If a field path doesn't exist in the database

    Note:
        Only validates fields that appear to be paths (contain dots).
        Aggregation aliases (no dots, like 'count') are skipped as they
        cannot be validated until query execution time.
    """
    if order_by is None:
        return

    for order_instr in order_by:
        # Skip aggregation aliases (no dots, e.g., 'count', 'revenue')
        if "." not in order_instr.field:
            continue

        field_type = validate_filter_path(order_instr.field)
        if field_type is None:
            raise PathNotFoundError(order_instr.field)
