"""
Expression Service.

Core service for expression parsing, validation, and conversion.

This is a PURE PYTHON module with NO QGIS dependencies,
enabling true unit testing and clear separation of concerns.
"""
from typing import Optional, List, Set
from dataclasses import dataclass, field
import re

from ..domain.filter_expression import ProviderType, SpatialPredicate


@dataclass
class ValidationResult:
    """
    Result of expression validation.

    Attributes:
        is_valid: Whether expression is syntactically valid
        error_message: Error description if invalid
        error_position: Character position of error (if known)
        warnings: List of non-fatal warnings
    """
    is_valid: bool
    error_message: Optional[str] = None
    error_position: Optional[int] = None
    warnings: List[str] = field(default_factory=list)

    @classmethod
    def valid(cls, warnings: Optional[List[str]] = None) -> 'ValidationResult':
        """Create a valid result."""
        return cls(is_valid=True, warnings=warnings or [])

    @classmethod
    def invalid(
        cls,
        message: str,
        position: Optional[int] = None
    ) -> 'ValidationResult':
        """Create an invalid result."""
        return cls(
            is_valid=False,
            error_message=message,
            error_position=position
        )

    def __bool__(self) -> bool:
        """Allow using ValidationResult in boolean context."""
        return self.is_valid


@dataclass
class ParsedExpression:
    """
    Parsed expression with extracted components.

    Attributes:
        original: Original expression string
        fields: Set of field names referenced
        spatial_predicates: List of spatial predicates used
        has_geometry_reference: Whether $geometry is referenced
        has_layer_reference: Whether layer functions are used
        estimated_complexity: Complexity score (1-10)
        operators: List of operators used (AND, OR, etc.)
    """
    original: str
    fields: Set[str]
    spatial_predicates: List[SpatialPredicate]
    has_geometry_reference: bool
    has_layer_reference: bool
    estimated_complexity: int
    operators: List[str] = field(default_factory=list)

    @property
    def is_spatial(self) -> bool:
        """Check if expression is spatial."""
        return len(self.spatial_predicates) > 0 or self.has_geometry_reference

    @property
    def is_simple(self) -> bool:
        """Check if expression is simple (low complexity)."""
        return self.estimated_complexity <= 2

    @property
    def is_complex(self) -> bool:
        """Check if expression is complex (high complexity)."""
        return self.estimated_complexity >= 5

    @property
    def field_count(self) -> int:
        """Number of fields referenced."""
        return len(self.fields)


class ExpressionService:
    """
    Service for expression parsing and conversion.

    Handles:
    - QGIS expression validation
    - Conversion to PostgreSQL/PostGIS SQL
    - Conversion to Spatialite SQL
    - Spatial predicate detection
    - Field extraction
    - Expression complexity estimation

    Example:
        service = ExpressionService()

        # Validate expression
        result = service.validate("\"name\" = 'test'")
        if result.is_valid:
            # Parse for analysis
            parsed = service.parse("\"name\" = 'test'")


            # Convert to SQL
            sql = service.to_sql("intersects($geometry, @g)", ProviderType.POSTGRESQL)

    """

    # Patterns for expression parsing
    FIELD_PATTERN = re.compile(r'"([^"]+)"')
    GEOMETRY_PATTERN = re.compile(r'\$geometry|\$geom|geometry\(\)', re.IGNORECASE)
    LAYER_PATTERN = re.compile(r"@layer|layer_property|get_feature", re.IGNORECASE)
    OPERATOR_PATTERN = re.compile(r'\b(AND|OR|NOT|IN|LIKE|ILIKE|BETWEEN|IS NULL|IS NOT NULL)\b', re.IGNORECASE)

    # Spatial predicates mapping
    SPATIAL_PREDICATES = {
        'intersects': SpatialPredicate.INTERSECTS,
        'contains': SpatialPredicate.CONTAINS,
        'within': SpatialPredicate.WITHIN,
        'crosses': SpatialPredicate.CROSSES,
        'touches': SpatialPredicate.TOUCHES,
        'overlaps': SpatialPredicate.OVERLAPS,
        'disjoint': SpatialPredicate.DISJOINT,
        'equals': SpatialPredicate.EQUALS,
        'dwithin': SpatialPredicate.DWITHIN,
    }

    # QGIS to PostgreSQL/PostGIS function mapping
    POSTGIS_FUNCTIONS = {
        'intersects': 'ST_Intersects',
        'contains': 'ST_Contains',
        'within': 'ST_Within',
        'crosses': 'ST_Crosses',
        'touches': 'ST_Touches',
        'overlaps': 'ST_Overlaps',
        'disjoint': 'ST_Disjoint',
        'equals': 'ST_Equals',
        'buffer': 'ST_Buffer',
        'area': 'ST_Area',
        'length': 'ST_Length',
        'distance': 'ST_Distance',
        'centroid': 'ST_Centroid',
        'convexhull': 'ST_ConvexHull',
        'envelope': 'ST_Envelope',
        'simplify': 'ST_Simplify',
        'union': 'ST_Union',
        'intersection': 'ST_Intersection',
        'difference': 'ST_Difference',
        'symdifference': 'ST_SymDifference',
        'transform': 'ST_Transform',
        'makevalid': 'ST_MakeValid',
        'isvalid': 'ST_IsValid',
        'numpoints': 'ST_NPoints',
        'numgeometries': 'ST_NumGeometries',
        'geometryn': 'ST_GeometryN',
        'startpoint': 'ST_StartPoint',
        'endpoint': 'ST_EndPoint',
        'pointn': 'ST_PointN',
        'exteriorring': 'ST_ExteriorRing',
        'x': 'ST_X',
        'y': 'ST_Y',
    }

    # QGIS to Spatialite function mapping
    SPATIALITE_FUNCTIONS = {
        'intersects': 'Intersects',
        'contains': 'Contains',
        'within': 'Within',
        'crosses': 'Crosses',
        'touches': 'Touches',
        'overlaps': 'Overlaps',
        'disjoint': 'Disjoint',
        'equals': 'Equals',
        'buffer': 'Buffer',
        'area': 'Area',
        'length': 'GLength',  # Note: Different name in Spatialite
        'distance': 'Distance',
        'centroid': 'Centroid',
        'convexhull': 'ConvexHull',
        'envelope': 'Envelope',
        'simplify': 'Simplify',
        'union': 'GUnion',  # Note: Different name in Spatialite
        'intersection': 'Intersection',
        'difference': 'Difference',
        'symdifference': 'SymDifference',
        'transform': 'Transform',
        'makevalid': 'MakeValid',
        'isvalid': 'IsValid',
        'numpoints': 'NumPoints',
        'numgeometries': 'NumGeometries',
        'geometryn': 'GeometryN',
        'startpoint': 'StartPoint',
        'endpoint': 'EndPoint',
        'pointn': 'PointN',
        'exteriorring': 'ExteriorRing',
        'x': 'X',
        'y': 'Y',
    }

    def validate(self, expression: str) -> ValidationResult:
        """
        Validate expression syntax.

        Checks for:
        - Empty expression
        - Balanced parentheses
        - Balanced quotes
        - Common syntax issues

        Args:
            expression: QGIS expression string

        Returns:
            ValidationResult with validity and any errors/warnings
        """
        if not expression or not expression.strip():
            return ValidationResult.invalid("Expression cannot be empty")

        warnings: List[str] = []

        # Check for balanced parentheses
        paren_count = 0
        for i, char in enumerate(expression):
            if char == '(':
                paren_count += 1
            elif char == ')':
                paren_count -= 1
                if paren_count < 0:
                    return ValidationResult.invalid(
                        "Unbalanced parentheses: unexpected ')'",
                        position=i
                    )
        if paren_count != 0:
            return ValidationResult.invalid("Unbalanced parentheses: missing ')'")

        # Check for balanced double quotes
        in_double_quote = False
        for i, char in enumerate(expression):
            if char == '"':
                in_double_quote = not in_double_quote
        if in_double_quote:
            return ValidationResult.invalid("Unbalanced double quotes")

        # Check for balanced single quotes (more complex due to escaping)
        single_quote_count = 0
        i = 0
        while i < len(expression):
            if expression[i] == "'":
                # Check for escaped quote
                if i + 1 < len(expression) and expression[i + 1] == "'":
                    i += 2  # Skip escaped quote
                    continue
                single_quote_count += 1
            i += 1
        if single_quote_count % 2 != 0:
            return ValidationResult.invalid("Unbalanced single quotes")

        # Check for common issues (warnings, not errors)
        if '==' in expression:
            warnings.append("Use '=' instead of '==' for equality")

        if '!=' in expression:
            warnings.append("Use '<>' instead of '!=' for inequality")

        if '&&' in expression:
            warnings.append("Use 'AND' instead of '&&'")

        if '||' in expression and 'concat' not in expression.lower():
            warnings.append("Use 'OR' instead of '||' (unless string concatenation)")

        # Check for potential SQL injection (basic check)
        dangerous_patterns = [';', '--', '/*', '*/']
        for pattern in dangerous_patterns:
            if pattern in expression:
                warnings.append(f"Potentially dangerous pattern '{pattern}' detected")

        return ValidationResult.valid(warnings)

    def parse(self, expression: str) -> ParsedExpression:
        """
        Parse expression and extract components.

        Extracts:
        - Field references
        - Spatial predicates
        - Geometry references
        - Layer references
        - Operators
        - Complexity estimate

        Args:
            expression: QGIS expression string

        Returns:
            ParsedExpression with extracted components
        """
        # Extract fields (quoted identifiers)
        fields = set(self.FIELD_PATTERN.findall(expression))

        # Detect spatial predicates
        spatial_predicates: List[SpatialPredicate] = []
        expr_lower = expression.lower()
        for name, predicate in self.SPATIAL_PREDICATES.items():
            if name in expr_lower:
                spatial_predicates.append(predicate)

        # Check for geometry and layer references
        has_geometry = bool(self.GEOMETRY_PATTERN.search(expression))
        has_layer = bool(self.LAYER_PATTERN.search(expression))

        # Extract operators
        operators = self.OPERATOR_PATTERN.findall(expression.upper())

        # Estimate complexity based on various factors
        complexity = self._estimate_complexity(
            expression=expression,
            fields=fields,
            spatial_predicates=spatial_predicates,
            has_geometry=has_geometry,
            operators=operators
        )

        return ParsedExpression(
            original=expression,
            fields=fields,
            spatial_predicates=spatial_predicates,
            has_geometry_reference=has_geometry,
            has_layer_reference=has_layer,
            estimated_complexity=complexity,
            operators=operators
        )

    def _estimate_complexity(
        self,
        expression: str,
        fields: Set[str],
        spatial_predicates: List[SpatialPredicate],
        has_geometry: bool,
        operators: List[str]
    ) -> int:
        """
        Estimate expression complexity on a scale of 1-10.

        Higher complexity suggests:
        - Longer execution time
        - May benefit from materialized views
        - May need more memory for caching
        """
        complexity = 1

        # Factor in operators
        and_or_count = sum(1 for op in operators if op in ('AND', 'OR'))
        complexity += min(and_or_count, 3)  # Cap at +3

        # Factor in spatial predicates (expensive)
        complexity += len(spatial_predicates) * 2

        # Factor in geometry reference
        if has_geometry:
            complexity += 1

        # Factor in field count
        if len(fields) > 3:
            complexity += 1

        # Factor in expression length
        if len(expression) > 200:
            complexity += 1

        # Cap at 10
        return min(complexity, 10)

    def to_sql(
        self,
        expression: str,
        provider: ProviderType,
        geometry_column: str = "geometry"
    ) -> str:
        """
        Convert QGIS expression to provider-specific SQL.

        Args:
            expression: QGIS expression
            provider: Target provider type
            geometry_column: Name of geometry column in table

        Returns:
            SQL expression string for the target provider
        """
        if provider == ProviderType.POSTGRESQL:
            return self._to_postgis(expression, geometry_column)
        elif provider == ProviderType.SPATIALITE:
            return self._to_spatialite(expression, geometry_column)
        else:
            # OGR and Memory use QGIS expressions directly
            return expression

    def _to_postgis(self, expression: str, geometry_column: str) -> str:
        """
        Convert to PostGIS SQL.

        Handles:
        - $geometry -> column reference
        - QGIS functions -> PostGIS equivalents
        - IF -> CASE WHEN conversion
        - Type casting for numeric/text operations
        - SQL keyword normalization

        Consolidated from legacy filter_task.py qgis_expression_to_postgis()
        """
        sql = expression

        if not sql:
            return sql

        # 1. Replace $geometry with column reference (including $area, $length, etc.)
        spatial_conversions = {
            r'\$area': f'ST_Area("{geometry_column}")',
            r'\$length': f'ST_Length("{geometry_column}")',
            r'\$perimeter': f'ST_Perimeter("{geometry_column}")',
            r'\$x': f'ST_X("{geometry_column}")',
            r'\$y': f'ST_Y("{geometry_column}")',
            r'\$geometry|\$geom': f'"{geometry_column}"',
        }
        for pattern, replacement in spatial_conversions.items():
            sql = re.sub(pattern, replacement, sql, flags=re.IGNORECASE)

        # 2. Replace QGIS functions with PostGIS equivalents
        for qgis_func, pg_func in self.POSTGIS_FUNCTIONS.items():
            pattern = rf'\b{qgis_func}\s*\('
            sql = re.sub(pattern, f'{pg_func}(', sql, flags=re.IGNORECASE)

        # 3. Convert QGIS IF statements to SQL CASE WHEN
        # Pattern: if(condition, value_true, value_false) -> CASE WHEN condition THEN value_true ELSE value_false END
        sql = re.sub(
            r'\bif\s*\(\s*([^,]+),\s*([^,]+),\s*([^)]+)\)',
            r'CASE WHEN \1 THEN \2 ELSE \3 END',
            sql,
            flags=re.IGNORECASE
        )

        # 4. Normalize SQL keywords
        sql = re.sub(r'\bcase\b', ' CASE ', sql, flags=re.IGNORECASE)
        sql = re.sub(r'\bwhen\b', ' WHEN ', sql, flags=re.IGNORECASE)
        sql = re.sub(r'\bthen\b', ' THEN ', sql, flags=re.IGNORECASE)
        sql = re.sub(r'\belse\b', ' ELSE ', sql, flags=re.IGNORECASE)
        sql = re.sub(r'\bend\b', ' END ', sql, flags=re.IGNORECASE)
        sql = re.sub(r'\bilike\b', ' ILIKE ', sql, flags=re.IGNORECASE)
        sql = re.sub(r'\blike\b', ' LIKE ', sql, flags=re.IGNORECASE)
        sql = re.sub(r'\bnot\b', ' NOT ', sql, flags=re.IGNORECASE)
        sql = re.sub(r'\bis\b', ' IS ', sql, flags=re.IGNORECASE)

        # 5. Add type casting for numeric operations (comparison operators)
        # "field" > value -> "field"::numeric > value
        sql = re.sub(r'"(\w+)"\s*>', r'"\1"::numeric >', sql)
        sql = re.sub(r'"(\w+)"\s*<', r'"\1"::numeric <', sql)
        sql = re.sub(r'"(\w+)"\s*\+', r'"\1"::numeric +', sql)
        sql = re.sub(r'"(\w+)"\s*-', r'"\1"::numeric -', sql)

        # 6. Add type casting for text operations (LIKE/ILIKE)
        sql = re.sub(r'"(\w+)"\s+(NOT\s+)?ILIKE', r'"\1"::text \2ILIKE', sql)
        sql = re.sub(r'"(\w+)"\s+(NOT\s+)?LIKE', r'"\1"::text \2LIKE', sql)

        # 7. Handle NULL comparisons
        sql = re.sub(r'\bIS\s+NULL\b', 'IS NULL', sql, flags=re.IGNORECASE)
        sql = re.sub(r'\bIS\s+NOT\s+NULL\b', 'IS NOT NULL', sql, flags=re.IGNORECASE)

        # 8. Handle boolean literals
        sql = re.sub(r'\bTRUE\b', 'TRUE', sql, flags=re.IGNORECASE)
        sql = re.sub(r'\bFALSE\b', 'FALSE', sql, flags=re.IGNORECASE)

        # 9. Clean up extra spaces
        sql = re.sub(r'\s+', ' ', sql).strip()

        return sql

    def _to_spatialite(self, expression: str, geometry_column: str) -> str:
        """
        Convert to Spatialite SQL.

        Handles:
        - $geometry -> column reference
        - QGIS functions -> Spatialite equivalents
        - PostgreSQL :: type casting -> CAST() function
        - ILIKE -> LOWER(field) LIKE LOWER(pattern)
        - SQL keyword normalization

        Consolidated from legacy filter_task.py qgis_expression_to_spatialite()

        Note:
            Spatialite spatial functions are ~90% compatible with PostGIS.
            Main differences: type casting syntax, no ILIKE, some function names.
        """
        sql = expression

        if not sql:
            return sql

        # 1. Replace $geometry with column reference
        sql = re.sub(
            r'\$geometry|\$geom',
            f'"{geometry_column}"',
            sql,
            flags=re.IGNORECASE
        )

        # 2. Replace QGIS functions with Spatialite equivalents
        for qgis_func, sl_func in self.SPATIALITE_FUNCTIONS.items():
            pattern = rf'\b{qgis_func}\s*\('
            sql = re.sub(pattern, f'{sl_func}(', sql, flags=re.IGNORECASE)

        # 3. Normalize CASE expressions
        sql = re.sub(r'\bcase\b', ' CASE ', sql, flags=re.IGNORECASE)
        sql = re.sub(r'\bwhen\b', ' WHEN ', sql, flags=re.IGNORECASE)
        sql = re.sub(r'\bthen\b', ' THEN ', sql, flags=re.IGNORECASE)
        sql = re.sub(r'\belse\b', ' ELSE ', sql, flags=re.IGNORECASE)
        sql = re.sub(r'\bend\b', ' END ', sql, flags=re.IGNORECASE)

        # 4. Handle ILIKE - Spatialite doesn't have ILIKE, use LOWER() with LIKE
        # IMPORTANT: Process ILIKE before LIKE to avoid double-replacement
        # "field" ILIKE 'pattern' -> LOWER("field") LIKE LOWER('pattern')
        sql = re.sub(
            r'"(\w+)"\s+ILIKE\s+\'([^\']+)\'',
            r'LOWER("\1") LIKE LOWER(\'\2\')',
            sql,
            flags=re.IGNORECASE
        )
        sql = re.sub(
            r'"(\w+)"\s+NOT\s+ILIKE\s+\'([^\']+)\'',
            r'LOWER("\1") NOT LIKE LOWER(\'\2\')',
            sql,
            flags=re.IGNORECASE
        )

        # 5. Normalize LIKE and NOT
        sql = re.sub(r'\bnot\b', ' NOT ', sql, flags=re.IGNORECASE)
        sql = re.sub(r'\blike\b', ' LIKE ', sql, flags=re.IGNORECASE)

        # 6. Convert PostgreSQL :: type casting to Spatialite CAST() function
        # "field"::numeric -> CAST("field" AS REAL)
        # "field"::integer -> CAST("field" AS INTEGER)
        # "field"::text -> CAST("field" AS TEXT)
        sql = re.sub(r'"(\w+)"::numeric', r'CAST("\1" AS REAL)', sql)
        sql = re.sub(r'"(\w+)"::integer', r'CAST("\1" AS INTEGER)', sql)
        sql = re.sub(r'"(\w+)"::text', r'CAST("\1" AS TEXT)', sql)
        sql = re.sub(r'"(\w+)"::double', r'CAST("\1" AS REAL)', sql)
        sql = re.sub(r'"(\w+)"::float', r'CAST("\1" AS REAL)', sql)

        # 7. Handle NULL comparisons
        sql = re.sub(r'\bIS\s+NULL\b', 'IS NULL', sql, flags=re.IGNORECASE)
        sql = re.sub(r'\bIS\s+NOT\s+NULL\b', 'IS NOT NULL', sql, flags=re.IGNORECASE)

        # 8. Spatialite boolean handling (uses 0/1 instead of TRUE/FALSE)
        sql = re.sub(r'\bTRUE\b', '1', sql, flags=re.IGNORECASE)
        sql = re.sub(r'\bFALSE\b', '0', sql, flags=re.IGNORECASE)

        # 9. Clean up extra spaces
        sql = re.sub(r'\s+', ' ', sql).strip()

        return sql

    def extract_fields(self, expression: str) -> Set[str]:
        """
        Extract field names from expression.

        Args:
            expression: QGIS expression string

        Returns:
            Set of field names
        """
        return set(self.FIELD_PATTERN.findall(expression))

    def is_spatial(self, expression: str) -> bool:
        """
        Check if expression contains spatial predicates or geometry references.

        Args:
            expression: QGIS expression string

        Returns:
            True if expression is spatial
        """
        expr_lower = expression.lower()
        has_predicate = any(pred in expr_lower for pred in self.SPATIAL_PREDICATES.keys())
        has_geometry = bool(self.GEOMETRY_PATTERN.search(expression))
        return has_predicate or has_geometry

    def get_spatial_predicates(self, expression: str) -> List[SpatialPredicate]:
        """
        Get list of spatial predicates in expression.

        Args:
            expression: QGIS expression string

        Returns:
            List of SpatialPredicate enums found
        """
        predicates: List[SpatialPredicate] = []
        expr_lower = expression.lower()
        for name, predicate in self.SPATIAL_PREDICATES.items():
            if name in expr_lower:
                predicates.append(predicate)
        return predicates

    def add_buffer(
        self,
        expression: str,
        buffer_value: float,
        provider: ProviderType,
        buffer_segments: int = 8
    ) -> str:
        """
        Wrap geometry references in buffer function.

        Args:
            expression: Original expression
            buffer_value: Buffer distance in layer units
            provider: Target provider type
            buffer_segments: Number of segments for buffer curves

        Returns:
            Expression with buffered geometry references
        """
        if provider == ProviderType.POSTGRESQL:
            buffer_fn = "ST_Buffer"
        elif provider == ProviderType.SPATIALITE:
            buffer_fn = "Buffer"
        else:
            buffer_fn = "buffer"

        # Wrap $geometry references in buffer
        pattern = r'(\$geometry|\$geom)'

        if provider == ProviderType.POSTGRESQL:
            # PostGIS uses ST_Buffer(geom, distance, segments)
            replacement = f'{buffer_fn}(\\1, {buffer_value})'
        elif provider == ProviderType.SPATIALITE:
            # Spatialite uses Buffer(geom, distance)
            replacement = f'{buffer_fn}(\\1, {buffer_value})'
        else:
            # QGIS expression: buffer($geometry, distance, segments)
            replacement = f'{buffer_fn}(\\1, {buffer_value}, {buffer_segments})'

        return re.sub(pattern, replacement, expression, flags=re.IGNORECASE)

    def normalize(self, expression: str) -> str:
        """
        Normalize expression for consistent comparison.

        - Strips whitespace
        - Normalizes operator case
        - Removes extra spaces

        Args:
            expression: QGIS expression string

        Returns:
            Normalized expression
        """
        result = expression.strip()

        # Normalize whitespace
        result = re.sub(r'\s+', ' ', result)

        # Normalize operators to uppercase
        for op in ['AND', 'OR', 'NOT', 'IN', 'LIKE', 'ILIKE', 'BETWEEN', 'IS', 'NULL']:
            result = re.sub(rf'\b{op}\b', op, result, flags=re.IGNORECASE)

        return result

    def combine_expressions(
        self,
        expressions: List[str],
        operator: str = 'AND'
    ) -> str:
        """
        Combine multiple expressions with an operator.

        Args:
            expressions: List of expression strings
            operator: Operator to use ('AND' or 'OR')

        Returns:
            Combined expression string
        """
        if not expressions:
            return ""
        if len(expressions) == 1:
            return expressions[0]

        # Wrap each expression in parentheses for safety
        wrapped = [f"({expr})" for expr in expressions if expr.strip()]
        return f" {operator.upper()} ".join(wrapped)

    def negate(self, expression: str) -> str:
        """
        Negate an expression.

        Args:
            expression: Expression to negate

        Returns:
            Negated expression
        """
        return f"NOT ({expression})"

    def detect_type_mismatches(
        self,
        expression: str,
        field_types: Optional[dict] = None
    ) -> List[str]:
        """
        Detect potential type mismatches in PostgreSQL expressions.

        FIX v4.8.2 (2026-01-25): Smart detection for VARCHAR fields
        used in numeric comparisons.

        This prevents the error:
        "ERROR: operator does not exist: character varying < integer"

        Args:
            expression: SQL expression to analyze
            field_types: Optional dict mapping field names to types
                        e.g., {'importance': 'varchar', 'fid': 'integer'}

        Returns:
            List of warning messages for detected type mismatches

        Example:
            >>> service = ExpressionService()
            >>> warnings = service.detect_type_mismatches(
            ...     '"importance" < 4',
            ...     {'importance': 'varchar'}
            ... )
            >>> print(warnings[0])
            'Field "importance" (VARCHAR) used in numeric comparison. Consider: "importance"::integer < 4'
        """
        warnings = []

        if not expression or not field_types:
            return warnings

        # Pattern: "field_name" <operator> <number>
        # Matches: "importance" < 4, "age" >= 18, "count" = 0
        numeric_comparison_pattern = re.compile(
            r'"([^"]+)"\s*(<|>|<=|>=|=|!=|<>)\s*(\d+(?:\.\d+)?)',
            re.IGNORECASE
        )

        for match in numeric_comparison_pattern.finditer(expression):
            field_name = match.group(1)
            operator = match.group(2)
            value = match.group(3)

            # Check if field exists in field_types dict
            field_type = field_types.get(field_name.lower(), '').lower()

            # Check for VARCHAR/TEXT fields used in numeric comparisons
            if field_type in ('varchar', 'character varying', 'text', 'char'):
                # Check if the field already has ::integer or ::numeric cast
                # Look backwards from the match to check for existing cast
                start_pos = max(0, match.start() - 20)
                context_before = expression[start_pos:match.start()]

                if '::integer' not in context_before and '::numeric' not in context_before:
                    warnings.append(
                        f'Field "{field_name}" (VARCHAR) used in numeric comparison '
                        f'with operator "{operator}". This may cause PostgreSQL error. '
                        f'Consider: "{field_name}"::integer {operator} {value}'
                    )

        # Pattern: "field_name" LIKE/ILIKE '%pattern%'
        # Check for numeric fields used in string comparisons
        string_comparison_pattern = re.compile(
            r'"([^"]+)"\s+(LIKE|ILIKE|NOT\s+LIKE|NOT\s+ILIKE)\s+',
            re.IGNORECASE
        )

        for match in string_comparison_pattern.finditer(expression):
            field_name = match.group(1)
            operator = match.group(2)

            field_type = field_types.get(field_name.lower(), '').lower()

            # Check for INTEGER/NUMERIC fields used in LIKE/ILIKE
            if field_type in ('integer', 'smallint', 'bigint', 'numeric', 'decimal', 'real', 'double precision'):
                # Check if the field already has ::text cast
                context_before = expression[max(0, match.start() - 20):match.start()]

                if '::text' not in context_before:
                    warnings.append(
                        f'Field "{field_name}" ({field_type.upper()}) used in string comparison '
                        f'with {operator}. Consider: "{field_name}"::text {operator} ...'
                    )

        return warnings


def sanitize_subset_string(subset_string: str, logger=None) -> str:
    """
    Remove non-boolean display expressions and fix type casting issues in subset string.

    CONSOLIDATED v4.1: Delegates to core.filter.expression_sanitizer for DRY compliance.
    Wrapper maintained for backward compatibility with logger parameter.

    Args:
        subset_string: The original subset string
        logger: Optional logger for diagnostics (deprecated, ignored)

    Returns:
        Sanitized subset string with non-boolean expressions removed
    """
    # Delegate to the canonical implementation
    from ..filter.expression_sanitizer import sanitize_subset_string as core_sanitize
    return core_sanitize(subset_string)
