"""Build, rank and filter Candidate objects from raw Parquet row dicts."""
from __future__ import annotations

import datetime as dt
import math
from typing import Any
from zoneinfo import ZoneInfo

from libs.backtest.domain import (
    Candidate,
    EventTypeProfile,
    SignalConfig,
    StrategyEngineConfig,
    UniverseConfig,
)
from libs.common.logging import get_logger

logger = get_logger(__name__)
_UTC = ZoneInfo("UTC")


def build_candidate(
    row: dict[str, Any],
    strategy_engine: StrategyEngineConfig | None = None,
    engine_lookup: dict[str, StrategyEngineConfig] | None = None,
) -> Candidate | None:
    """Build a Candidate from a raw Parquet row dict.

    Returns None (logged as skip) if:
    - event_timestamp is null/missing
    - entry_price_est is null/zero
    - execution_date is null/missing
    """
    event_id = row.get("event_id", "")

    # Strict: no silent substitution for event_timestamp
    raw_ts = row.get("event_timestamp")
    if raw_ts is None:
        logger.warning("skip_candidate_no_timestamp", event_id=event_id)
        return None

    # Normalise to timezone-aware datetime
    if isinstance(raw_ts, str):
        try:
            event_timestamp = dt.datetime.fromisoformat(raw_ts)
        except ValueError:
            logger.warning("skip_candidate_bad_timestamp", event_id=event_id, raw=raw_ts)
            return None
    elif isinstance(raw_ts, dt.datetime):
        event_timestamp = raw_ts
    else:
        logger.warning("skip_candidate_unknown_timestamp_type", event_id=event_id)
        return None

    if event_timestamp.tzinfo is None:
        event_timestamp = event_timestamp.replace(tzinfo=_UTC)

    # reaction_date
    raw_react = row.get("reaction_date")
    reaction_date = _parse_date(raw_react)

    # execution_date (mapped from Parquet entry_date) or reaction date for close-entry engines
    raw_exec_date = row.get("execution_date") or row.get("entry_date")
    strategy_engine = _resolve_strategy_engine(strategy_engine, engine_lookup=engine_lookup)
    execution_date = _parse_date(raw_exec_date)
    if execution_date is None:
        if strategy_engine and strategy_engine.entry_timing_policy == "reaction_close":
            execution_date = reaction_date
        else:
            logger.warning("skip_candidate_no_exec_date", event_id=event_id)
            return None

    if reaction_date is None:
        reaction_date = execution_date

    event_date = _parse_date(row.get("event_date")) or event_timestamp.date()
    timing_class = _classify_timing_class(event_date, reaction_date)

    trade_direction = _resolve_trade_direction(row, strategy_engine=strategy_engine)
    if strategy_engine is not None and not _matches_strategy_engine(
        row=row,
        strategy_engine=strategy_engine,
        event_type=str(row.get("event_type", "")),
        timing_class=timing_class,
        trade_direction=trade_direction,
        engine_lookup=engine_lookup,
    ):
        return None

    source_symbol = str(row.get("source_symbol") or row.get("symbol", row.get("ticker", ""))).upper()
    trade_symbol_mode = (
        strategy_engine.trade_symbol_mode
        if strategy_engine is not None
        else "event"
    )
    trade_symbol = source_symbol
    execution_entry_price = row.get("entry_price") or row.get("entry_price_est")
    execution_event_close = row.get("event_close")
    execution_avg_dollar_volume = row.get(
        "avg_dollar_volume_20d",
        row.get("avg_dollar_volume", 0.0),
    )
    execution_atr_14_raw = row.get("atr_14")
    execution_features = {k: v for k, v in row.items() if k not in _RESERVED_KEYS}

    if strategy_engine and trade_symbol_mode == "sector_etf":
        proxy_symbol = str(row.get("sector_etf_proxy") or "").upper()
        if not proxy_symbol:
            logger.debug(
                "skip_candidate_no_sector_etf_proxy",
                event_id=event_id,
                source_symbol=source_symbol,
                engine_id=strategy_engine.engine_id,
            )
            return None
        trade_symbol = proxy_symbol
        execution_entry_price = row.get("sector_etf_entry_price") or execution_entry_price
        execution_event_close = row.get("sector_etf_event_close") or execution_event_close
        execution_avg_dollar_volume = (
            row.get("sector_etf_avg_dollar_volume")
            or execution_avg_dollar_volume
        )
        execution_atr_14_raw = row.get("sector_etf_atr_14")
        execution_features.update(
            {
                "source_symbol": source_symbol,
                "trade_symbol_mode": trade_symbol_mode,
                "proxy_trade_symbol": trade_symbol,
                "proxy_reference_sector": row.get("sector"),
                "source_event_close": row.get("event_close"),
                "source_reaction_day_low": row.get("reaction_day_low"),
                "source_reaction_day_high": row.get("reaction_day_high"),
                "event_close": row.get("sector_etf_event_close"),
                "reaction_day_low": row.get("sector_etf_reaction_day_low"),
                "reaction_day_high": row.get("sector_etf_reaction_day_high"),
            }
        )
    elif strategy_engine and trade_symbol_mode == "peer_proxy":
        proxy_symbol = str(row.get("peer_proxy_symbol") or "").upper()
        if not proxy_symbol:
            logger.debug(
                "skip_candidate_no_peer_proxy",
                event_id=event_id,
                source_symbol=source_symbol,
                engine_id=strategy_engine.engine_id,
            )
            return None
        trade_symbol = proxy_symbol
        execution_entry_price = row.get("peer_proxy_entry_price") or execution_entry_price
        execution_event_close = row.get("peer_proxy_event_close") or execution_event_close
        execution_avg_dollar_volume = (
            row.get("peer_proxy_avg_dollar_volume")
            or execution_avg_dollar_volume
        )
        execution_atr_14_raw = row.get("peer_proxy_atr_14")
        execution_features.update(
            {
                "source_symbol": source_symbol,
                "trade_symbol_mode": trade_symbol_mode,
                "proxy_trade_symbol": trade_symbol,
                "proxy_reference_sector": row.get("sector"),
                "source_event_close": row.get("event_close"),
                "source_reaction_day_low": row.get("reaction_day_low"),
                "source_reaction_day_high": row.get("reaction_day_high"),
                "event_close": row.get("peer_proxy_event_close"),
                "reaction_day_low": row.get("peer_proxy_reaction_day_low"),
                "reaction_day_high": row.get("peer_proxy_reaction_day_high"),
            }
        )

    if strategy_engine and strategy_engine.entry_timing_policy == "reaction_close":
        execution_date = reaction_date
        entry_price_est = execution_event_close or row.get("entry_price_est")
        if not entry_price_est:
            logger.debug(
                "skip_candidate_no_event_close",
                event_id=event_id,
                engine_id=strategy_engine.engine_id,
            )
            return None
    else:
        entry_price_est = execution_entry_price

    if not entry_price_est:
        logger.warning("skip_candidate_no_entry_price", event_id=event_id)
        return None
    entry_price_est = float(entry_price_est)
    if entry_price_est <= 0:
        logger.warning("skip_candidate_zero_entry_price", event_id=event_id)
        return None

    score = float(row.get("score", 0.0))
    avg_dollar_volume = float(execution_avg_dollar_volume or 0.0)
    atr_14_raw = execution_atr_14_raw
    atr_14 = float(atr_14_raw) if atr_14_raw is not None else None
    event_direction = str(row.get("event_direction", "")).lower()
    guidance_status = str(row.get("guidance_status", "")).lower()
    close_location_raw = row.get("close_location")
    close_location = float(close_location_raw) if close_location_raw is not None else None
    gap_size_raw = row.get("gap_size")
    gap_size = float(gap_size_raw) if gap_size_raw is not None else None
    is_unknown_inline = (
        event_direction == "unknown"
        and guidance_status == "inline_or_maintained"
    )
    is_mixed_inline = (
        event_direction == "mixed"
        and guidance_status == "inline_or_maintained"
    )

    engine_early_failure_close_below_entry_and_reaction_close = None
    engine_early_failure_no_progress_days = None
    engine_early_failure_no_progress_r = None
    engine_early_failure_no_progress_fraction = None
    engine_dynamic_hold_checkpoints = None
    engine_dynamic_hold_extend_day = None
    engine_dynamic_hold_extend_r = None
    engine_dynamic_hold_extend_to = None
    engine_veto_oneoff_penalty = None
    engine_allow_oneoff_downsizing = None
    engine_oneoff_downsize_floor = None
    engine_veto_parse_confidence_min = None
    engine_allow_unknown_direction = None
    if strategy_engine is not None:
        engine_early_failure_close_below_entry_and_reaction_close = (
            strategy_engine.early_failure_close_below_entry_and_reaction_close_override
        )
        engine_early_failure_no_progress_days = (
            strategy_engine.early_failure_no_progress_days_override
        )
        engine_early_failure_no_progress_r = (
            strategy_engine.early_failure_no_progress_r_override
        )
        engine_early_failure_no_progress_fraction = (
            strategy_engine.early_failure_no_progress_fraction_override
        )
        engine_dynamic_hold_checkpoints = strategy_engine.dynamic_hold_checkpoints_override
        engine_dynamic_hold_extend_day = strategy_engine.dynamic_hold_extend_day_override
        engine_dynamic_hold_extend_r = strategy_engine.dynamic_hold_extend_r_override
        engine_dynamic_hold_extend_to = strategy_engine.dynamic_hold_extend_to_override
        engine_veto_oneoff_penalty = strategy_engine.veto_oneoff_penalty_override
        engine_allow_oneoff_downsizing = strategy_engine.allow_oneoff_downsizing_override
        engine_oneoff_downsize_floor = strategy_engine.oneoff_downsize_floor_override
        engine_veto_parse_confidence_min = (
            strategy_engine.veto_parse_confidence_min_override
        )
        if strategy_engine.event_directions and "unknown" in strategy_engine.event_directions:
            engine_allow_unknown_direction = True
        apply_unknown_inline_override = is_unknown_inline
        if apply_unknown_inline_override:
            if (
                strategy_engine.unknown_inline_exit_close_location_min is not None
                and (
                    close_location is None
                    or close_location < strategy_engine.unknown_inline_exit_close_location_min
                )
            ):
                apply_unknown_inline_override = False
            if (
                strategy_engine.unknown_inline_exit_gap_size_max is not None
                and (
                    gap_size is None
                    or gap_size > strategy_engine.unknown_inline_exit_gap_size_max
                )
            ):
                apply_unknown_inline_override = False
        if apply_unknown_inline_override:
            if (
                strategy_engine.unknown_inline_early_failure_close_below_entry_and_reaction_close_override
                is not None
            ):
                engine_early_failure_close_below_entry_and_reaction_close = (
                    strategy_engine.unknown_inline_early_failure_close_below_entry_and_reaction_close_override
                )
            if strategy_engine.unknown_inline_early_failure_no_progress_days_override is not None:
                engine_early_failure_no_progress_days = (
                    strategy_engine.unknown_inline_early_failure_no_progress_days_override
                )
            if strategy_engine.unknown_inline_early_failure_no_progress_r_override is not None:
                engine_early_failure_no_progress_r = (
                    strategy_engine.unknown_inline_early_failure_no_progress_r_override
                )
            if strategy_engine.unknown_inline_early_failure_no_progress_fraction_override is not None:
                engine_early_failure_no_progress_fraction = (
                    strategy_engine.unknown_inline_early_failure_no_progress_fraction_override
                )
        if is_mixed_inline:
            if (
                strategy_engine.mixed_inline_early_failure_close_below_entry_and_reaction_close_override
                is not None
            ):
                engine_early_failure_close_below_entry_and_reaction_close = (
                    strategy_engine.mixed_inline_early_failure_close_below_entry_and_reaction_close_override
                )
            if strategy_engine.mixed_inline_early_failure_no_progress_days_override is not None:
                engine_early_failure_no_progress_days = (
                    strategy_engine.mixed_inline_early_failure_no_progress_days_override
                )
            if strategy_engine.mixed_inline_early_failure_no_progress_r_override is not None:
                engine_early_failure_no_progress_r = (
                    strategy_engine.mixed_inline_early_failure_no_progress_r_override
                )
            if strategy_engine.mixed_inline_early_failure_no_progress_fraction_override is not None:
                engine_early_failure_no_progress_fraction = (
                    strategy_engine.mixed_inline_early_failure_no_progress_fraction_override
                )

    # Classify score bucket
    score_bucket = _classify_score_bucket(score)

    return Candidate(
        event_id=event_id,
        symbol=trade_symbol,
        source_symbol=source_symbol,
        issuer_id=row.get("issuer_id"),
        score=score,
        sector=str(row.get("sector") or "UNKNOWN"),
        event_type=str(row.get("event_type", "")),
        event_timestamp=event_timestamp,
        event_date=event_date,
        filing_time_bucket=str(row.get("filing_time_bucket", "unknown")),
        timing_class=timing_class,
        reaction_date=reaction_date,
        execution_date=execution_date,
        entry_price_est=entry_price_est,
        avg_dollar_volume=avg_dollar_volume,
        atr_14=atr_14,
        score_bucket=score_bucket,
        engine_id=strategy_engine.engine_id if strategy_engine else "default",
        entry_timing_policy=(
            strategy_engine.entry_timing_policy
            if strategy_engine
            else "next_open"
        ),
        shadow_only=strategy_engine.shadow_only if strategy_engine else False,
        engine_min_entry_price=(
            strategy_engine.min_entry_price_override
            if strategy_engine
            else None
        ),
        engine_max_entry_price=(
            strategy_engine.max_entry_price_override
            if strategy_engine
            else None
        ),
        engine_max_holding_days=(
            strategy_engine.max_holding_days
            if strategy_engine
            else None
        ),
        engine_max_positions_per_sector=(
            strategy_engine.max_positions_per_sector_override
            if strategy_engine
            else None
        ),
        engine_max_position_value_pct=(
            strategy_engine.max_position_value_pct_override
            if strategy_engine
            else None
        ),
        engine_max_adv_fraction=(
            strategy_engine.max_adv_fraction_override
            if strategy_engine
            else None
        ),
        engine_risk_budget_pct=(
            strategy_engine.engine_risk_budget_pct
            if strategy_engine
            else 1.0
        ),
        engine_capital_bucket_id=(
            (
                strategy_engine.capital_bucket_id
                or strategy_engine.engine_id
            )
            if strategy_engine and strategy_engine.capital_bucket_allocation_pct is not None
            else None
        ),
        engine_capital_bucket_allocation_pct=(
            strategy_engine.capital_bucket_allocation_pct
            if strategy_engine
            else None
        ),
        engine_per_trade_risk_pct=(
            strategy_engine.per_trade_risk_pct_override
            if strategy_engine
            else None
        ),
        engine_macro_vix_size_scaler_low=(
            strategy_engine.macro_vix_size_scaler_low
            if strategy_engine
            else None
        ),
        engine_macro_vix_size_scaler_high=(
            strategy_engine.macro_vix_size_scaler_high
            if strategy_engine
            else None
        ),
        engine_macro_vix_size_scaler_min=(
            strategy_engine.macro_vix_size_scaler_min
            if strategy_engine
            else None
        ),
        engine_macro_hy_spread_size_scaler_low=(
            strategy_engine.macro_hy_spread_size_scaler_low
            if strategy_engine
            else None
        ),
        engine_macro_hy_spread_size_scaler_high=(
            strategy_engine.macro_hy_spread_size_scaler_high
            if strategy_engine
            else None
        ),
        engine_macro_hy_spread_size_scaler_min=(
            strategy_engine.macro_hy_spread_size_scaler_min
            if strategy_engine
            else None
        ),
        engine_score_size_scaler_low=(
            strategy_engine.score_size_scaler_low
            if strategy_engine
            else None
        ),
        engine_score_size_scaler_high=(
            strategy_engine.score_size_scaler_high
            if strategy_engine
            else None
        ),
        engine_score_size_scaler_min=(
            strategy_engine.score_size_scaler_min
            if strategy_engine
            else None
        ),
        engine_entropy_size_scaler_low=(
            strategy_engine.entropy_size_scaler_low
            if strategy_engine
            else None
        ),
        engine_entropy_size_scaler_high=(
            strategy_engine.entropy_size_scaler_high
            if strategy_engine
            else None
        ),
        engine_entropy_size_scaler_min=(
            strategy_engine.entropy_size_scaler_min
            if strategy_engine
            else None
        ),
        engine_target_atr_multiplier=(
            strategy_engine.target_atr_multiplier_override
            if strategy_engine
            else None
        ),
        engine_stop_atr_multiplier=(
            strategy_engine.stop_atr_multiplier_override
            if strategy_engine
            else None
        ),
        engine_target_1_r=(
            strategy_engine.target_1_r_override
            if strategy_engine
            else None
        ),
        engine_target_1_fraction=(
            strategy_engine.target_1_fraction_override
            if strategy_engine
            else None
        ),
        engine_trailing_model=(
            strategy_engine.trailing_model_override
            if strategy_engine
            else None
        ),
        engine_trailing_warmup_days=(
            strategy_engine.trailing_warmup_days_override
            if strategy_engine
            else None
        ),
        engine_use_reaction_day_low_stop=(
            strategy_engine.use_reaction_day_low_stop_override
            if strategy_engine
            else None
        ),
        engine_early_failure_close_below_entry_and_reaction_close=(
            engine_early_failure_close_below_entry_and_reaction_close
        ),
        engine_early_failure_no_progress_days=engine_early_failure_no_progress_days,
        engine_early_failure_no_progress_r=engine_early_failure_no_progress_r,
        engine_early_failure_no_progress_fraction=engine_early_failure_no_progress_fraction,
        engine_dynamic_hold_checkpoints=engine_dynamic_hold_checkpoints,
        engine_dynamic_hold_extend_day=engine_dynamic_hold_extend_day,
        engine_dynamic_hold_extend_r=engine_dynamic_hold_extend_r,
        engine_dynamic_hold_extend_to=engine_dynamic_hold_extend_to,
        engine_veto_oneoff_penalty=engine_veto_oneoff_penalty,
        engine_allow_oneoff_downsizing=engine_allow_oneoff_downsizing,
        engine_oneoff_downsize_floor=engine_oneoff_downsize_floor,
        engine_veto_parse_confidence_min=engine_veto_parse_confidence_min,
        engine_allow_unknown_direction=engine_allow_unknown_direction,
        engine_next_open_gap_cap_pct=(
            strategy_engine.next_open_gap_cap_pct
            if strategy_engine
            else None
        ),
        engine_add_on_max_count=(
            strategy_engine.add_on_max_count
            if strategy_engine
            else None
        ),
        engine_add_on_size_fraction=(
            strategy_engine.add_on_size_fraction
            if strategy_engine
            else None
        ),
        trade_symbol_mode=trade_symbol_mode,
        trade_direction=trade_direction,
        engine_forced_trade_direction=(
            str(strategy_engine.forced_trade_direction_override).lower()
            if strategy_engine and strategy_engine.forced_trade_direction_override
            else None
        ),
        parent_position_id=row.get("parent_position_id"),
        is_add_on=bool(row.get("is_add_on", False)),
        forced_shares=(
            int(row["forced_shares"])
            if row.get("forced_shares") not in (None, "")
            else None
        ),
        features=execution_features,
    )


_RESERVED_KEYS = {
    "event_id", "symbol", "ticker", "issuer_id", "score", "sector",
    "event_type", "event_timestamp", "event_date", "filing_time_bucket", "reaction_date",
    "entry_date", "execution_date", "entry_price", "entry_price_est",
    "event_close", "avg_dollar_volume", "atr_14", "score_bucket", "trade_direction",
    "parent_position_id", "is_add_on", "forced_shares",
}


def _parse_date(raw: Any) -> dt.date | None:
    if isinstance(raw, dt.datetime):
        return raw.date()
    if isinstance(raw, dt.date):
        return raw
    if isinstance(raw, str):
        try:
            return dt.date.fromisoformat(raw)
        except ValueError:
            return None
    return None


def _classify_timing_class(event_date: dt.date | None, reaction_date: dt.date) -> str:
    if event_date is None:
        return "unknown"
    if reaction_date == event_date:
        return "same_day"
    if reaction_date > event_date:
        return "after_close"
    return "unknown"


def _resolve_trade_direction(
    row: dict[str, Any],
    strategy_engine: StrategyEngineConfig | None = None,
) -> str:
    forced_direction = (
        str(strategy_engine.forced_trade_direction_override).lower()
        if strategy_engine and strategy_engine.forced_trade_direction_override
        else ""
    )
    if forced_direction in {"long", "short"}:
        return forced_direction

    raw_direction = str(row.get("trade_direction", "")).lower()
    if raw_direction in {"long", "short"}:
        return raw_direction

    reaction = row.get("reaction_day_return")
    if reaction is not None:
        try:
            return "short" if float(reaction) < 0 else "long"
        except (TypeError, ValueError):
            pass
    return "long"


def _resolve_strategy_engine(
    strategy_engine: StrategyEngineConfig | None,
    engine_lookup: dict[str, StrategyEngineConfig] | None = None,
    _seen: set[str] | None = None,
) -> StrategyEngineConfig | None:
    if strategy_engine is None or not strategy_engine.inherits_from_engine_id or not engine_lookup:
        return strategy_engine
    parent_id = strategy_engine.inherits_from_engine_id
    parent = engine_lookup.get(parent_id)
    if parent is None:
        return strategy_engine
    seen = set(_seen or set())
    if strategy_engine.engine_id in seen or parent_id in seen:
        raise ValueError(f"Cyclic strategy engine inheritance detected for {strategy_engine.engine_id}")
    seen.add(strategy_engine.engine_id)
    resolved_parent = _resolve_strategy_engine(parent, engine_lookup=engine_lookup, _seen=seen)
    if resolved_parent is None:
        return strategy_engine
    merged = resolved_parent.model_dump()
    for field_name in strategy_engine.model_fields_set:
        merged[field_name] = getattr(strategy_engine, field_name)
    return StrategyEngineConfig.model_validate(merged)


def _row_matches_strategy_engine_filters(
    row: dict[str, Any],
    strategy_engine: StrategyEngineConfig,
    event_type: str,
    timing_class: str,
    trade_direction: str,
) -> bool:
    if strategy_engine.entry_conventions:
        entry_convention = str(row.get("entry_convention", "")).lower()
        allowed_entry_conventions = {
            str(value).lower() for value in strategy_engine.entry_conventions if value
        }
        if entry_convention not in allowed_entry_conventions:
            return False

    if strategy_engine.event_types and event_type not in strategy_engine.event_types:
        return False

    event_direction = str(row.get("event_direction", "")).lower()
    if strategy_engine.event_directions:
        allowed_directions = {value.lower() for value in strategy_engine.event_directions}
        if event_direction not in allowed_directions:
            return False

    guidance_status = str(row.get("guidance_status", "")).lower()
    if strategy_engine.guidance_statuses:
        allowed_statuses = {value.lower() for value in strategy_engine.guidance_statuses}
        if guidance_status not in allowed_statuses:
            return False
    if strategy_engine.filing_time_buckets:
        allowed_buckets = {value.lower() for value in strategy_engine.filing_time_buckets}
        filing_time_bucket = str(row.get("filing_time_bucket", "")).lower()
        if filing_time_bucket not in allowed_buckets:
            return False
    if strategy_engine.allowed_exchanges:
        allowed_exchanges = {value.upper() for value in strategy_engine.allowed_exchanges}
        exchange = str(row.get("exchange_proxy") or row.get("exchange") or "").upper()
        if exchange not in allowed_exchanges:
            return False
    if strategy_engine.excluded_symbols:
        excluded_symbols = {value.upper() for value in strategy_engine.excluded_symbols}
        symbol = str(row.get("symbol") or "").upper()
        if symbol in excluded_symbols:
            return False
    if strategy_engine.allowed_sectors:
        allowed_sectors = {value.lower() for value in strategy_engine.allowed_sectors}
        sector = str(row.get("sector") or "UNKNOWN").lower()
        if sector not in allowed_sectors:
            return False

    if strategy_engine.timing_class != "any" and timing_class != strategy_engine.timing_class:
        return False

    if strategy_engine.direction == "long_only" and trade_direction != "long":
        return False
    if strategy_engine.direction == "short_only" and trade_direction != "short":
        return False

    reaction_day_return = _safe_float(row.get("reaction_day_return"))
    if (
        strategy_engine.reaction_day_return_min is not None
        and reaction_day_return is not None
        and reaction_day_return < strategy_engine.reaction_day_return_min
    ):
        return False
    if (
        strategy_engine.reaction_day_return_max is not None
        and reaction_day_return is not None
        and reaction_day_return > strategy_engine.reaction_day_return_max
    ):
        return False

    close_location = _safe_float(row.get("close_location"))
    if (
        strategy_engine.close_location_min is not None
        and close_location is not None
        and close_location < strategy_engine.close_location_min
    ):
        return False
    if (
        strategy_engine.close_location_max is not None
        and close_location is not None
        and close_location > strategy_engine.close_location_max
    ):
        return False

    volume_ratio = _safe_float(row.get("volume_ratio_20d"))
    if (
        strategy_engine.volume_ratio_min is not None
        and volume_ratio is not None
        and volume_ratio < strategy_engine.volume_ratio_min
    ):
        return False
    if (
        strategy_engine.volume_ratio_max is not None
        and volume_ratio is not None
        and volume_ratio > strategy_engine.volume_ratio_max
    ):
        return False

    avg_dollar_volume = _safe_float(
        row.get("avg_dollar_volume_20d", row.get("avg_dollar_volume"))
    )
    if (
        strategy_engine.avg_dollar_volume_min is not None
        and avg_dollar_volume is not None
        and avg_dollar_volume < strategy_engine.avg_dollar_volume_min
    ):
        return False
    if (
        strategy_engine.avg_dollar_volume_max is not None
        and avg_dollar_volume is not None
        and avg_dollar_volume > strategy_engine.avg_dollar_volume_max
    ):
        return False

    gap_size = _safe_float(row.get("gap_size"))
    if (
        strategy_engine.gap_size_min is not None
        and gap_size is not None
        and gap_size < strategy_engine.gap_size_min
    ):
        return False
    if (
        strategy_engine.gap_size_max is not None
        and gap_size is not None
        and gap_size > strategy_engine.gap_size_max
    ):
        return False

    proxy_prefix = None
    if strategy_engine.trade_symbol_mode == "sector_etf":
        proxy_prefix = "sector_etf"
    elif strategy_engine.trade_symbol_mode == "peer_proxy":
        proxy_prefix = "peer_proxy"
    if proxy_prefix is not None:
        proxy_reaction_day_return = _safe_float(row.get(f"{proxy_prefix}_reaction_day_return"))
        if strategy_engine.proxy_reaction_day_return_min is not None:
            if (
                proxy_reaction_day_return is None
                or proxy_reaction_day_return < strategy_engine.proxy_reaction_day_return_min
            ):
                return False
        if strategy_engine.proxy_reaction_day_return_max is not None:
            if (
                proxy_reaction_day_return is None
                or proxy_reaction_day_return > strategy_engine.proxy_reaction_day_return_max
            ):
                return False

        proxy_close_location = _safe_float(row.get(f"{proxy_prefix}_close_location"))
        if strategy_engine.proxy_close_location_min is not None:
            if (
                proxy_close_location is None
                or proxy_close_location < strategy_engine.proxy_close_location_min
            ):
                return False
        if strategy_engine.proxy_close_location_max is not None:
            if (
                proxy_close_location is None
                or proxy_close_location > strategy_engine.proxy_close_location_max
            ):
                return False

        proxy_volume_ratio = _safe_float(row.get(f"{proxy_prefix}_volume_ratio_20d"))
        if strategy_engine.proxy_volume_ratio_min is not None:
            if (
                proxy_volume_ratio is None
                or proxy_volume_ratio < strategy_engine.proxy_volume_ratio_min
            ):
                return False
        if strategy_engine.proxy_volume_ratio_max is not None:
            if (
                proxy_volume_ratio is None
                or proxy_volume_ratio > strategy_engine.proxy_volume_ratio_max
            ):
                return False

        proxy_gap_size = _safe_float(row.get(f"{proxy_prefix}_gap_size"))
        if strategy_engine.proxy_gap_size_min is not None:
            if (
                proxy_gap_size is None
                or proxy_gap_size < strategy_engine.proxy_gap_size_min
            ):
                return False
        if strategy_engine.proxy_gap_size_max is not None:
            if (
                proxy_gap_size is None
                or proxy_gap_size > strategy_engine.proxy_gap_size_max
            ):
                return False

        proxy_avg_dollar_volume = _safe_float(row.get(f"{proxy_prefix}_avg_dollar_volume"))
        if strategy_engine.proxy_avg_dollar_volume_min is not None:
            if (
                proxy_avg_dollar_volume is None
                or proxy_avg_dollar_volume < strategy_engine.proxy_avg_dollar_volume_min
            ):
                return False
        if strategy_engine.proxy_avg_dollar_volume_max is not None:
            if (
                proxy_avg_dollar_volume is None
                or proxy_avg_dollar_volume > strategy_engine.proxy_avg_dollar_volume_max
            ):
                return False

    event_close = _safe_float(row.get("event_close"))
    reaction_day_low = _safe_float(row.get("reaction_day_low"))
    reaction_day_high = _safe_float(row.get("reaction_day_high"))
    reaction_day_range_pct = None
    upper_wick_pct = None
    if (
        event_close is not None
        and event_close > 0
        and reaction_day_low is not None
        and reaction_day_high is not None
        and reaction_day_high >= reaction_day_low
    ):
        reaction_day_range_pct = (reaction_day_high - reaction_day_low) / event_close
        upper_wick_pct = (reaction_day_high - event_close) / event_close
    if strategy_engine.reaction_day_range_pct_min is not None:
        if (
            reaction_day_range_pct is None
            or reaction_day_range_pct < strategy_engine.reaction_day_range_pct_min
        ):
            return False
    if strategy_engine.reaction_day_range_pct_max is not None:
        if (
            reaction_day_range_pct is None
            or reaction_day_range_pct > strategy_engine.reaction_day_range_pct_max
        ):
            return False
    if strategy_engine.upper_wick_pct_min is not None:
        if upper_wick_pct is None or upper_wick_pct < strategy_engine.upper_wick_pct_min:
            return False
    if strategy_engine.upper_wick_pct_max is not None:
        if upper_wick_pct is None or upper_wick_pct > strategy_engine.upper_wick_pct_max:
            return False

    market_cap_proxy = _safe_float(row.get("market_cap_proxy"))
    if strategy_engine.min_market_cap_proxy is not None:
        if market_cap_proxy is None or market_cap_proxy < strategy_engine.min_market_cap_proxy:
            return False
    if strategy_engine.max_market_cap_proxy is not None:
        if market_cap_proxy is None or market_cap_proxy > strategy_engine.max_market_cap_proxy:
            return False

    document_quality_score = _safe_float(row.get("document_quality_score"))
    if strategy_engine.document_quality_score_min is not None:
        if (
            document_quality_score is None
            or document_quality_score < strategy_engine.document_quality_score_min
        ):
            return False
    if strategy_engine.document_quality_score_max is not None:
        if (
            document_quality_score is None
            or document_quality_score > strategy_engine.document_quality_score_max
        ):
            return False

    signal_strength_score = _safe_float(row.get("signal_strength_score"))
    if strategy_engine.signal_strength_score_min is not None:
        if (
            signal_strength_score is None
            or signal_strength_score < strategy_engine.signal_strength_score_min
        ):
            return False
    if strategy_engine.signal_strength_score_max is not None:
        if (
            signal_strength_score is None
            or signal_strength_score > strategy_engine.signal_strength_score_max
        ):
            return False

    oneoff_penalty = _safe_float(row.get("oneoff_penalty"))
    if strategy_engine.oneoff_penalty_min is not None:
        if oneoff_penalty is None or oneoff_penalty < strategy_engine.oneoff_penalty_min:
            return False
    if strategy_engine.oneoff_penalty_max is not None:
        if oneoff_penalty is None or oneoff_penalty > strategy_engine.oneoff_penalty_max:
            return False

    parse_confidence_overall = _safe_float(row.get("parse_confidence_overall"))
    if strategy_engine.parse_confidence_overall_min is not None:
        if (
            parse_confidence_overall is None
            or parse_confidence_overall < strategy_engine.parse_confidence_overall_min
        ):
            return False
    if strategy_engine.parse_confidence_overall_max is not None:
        if (
            parse_confidence_overall is None
            or parse_confidence_overall > strategy_engine.parse_confidence_overall_max
        ):
            return False

    prior_event_fwd5d = _safe_float(row.get("prior_event_fwd5d"))
    if strategy_engine.prior_event_fwd5d_min is not None:
        if (
            prior_event_fwd5d is None
            or prior_event_fwd5d < strategy_engine.prior_event_fwd5d_min
        ):
            return False
    if strategy_engine.prior_event_fwd5d_max is not None:
        if (
            prior_event_fwd5d is None
            or prior_event_fwd5d > strategy_engine.prior_event_fwd5d_max
        ):
            return False

    lm_net_sentiment = _safe_float(row.get("lm_net_sentiment"))
    if strategy_engine.lm_net_sentiment_min is not None:
        if (
            lm_net_sentiment is None
            or lm_net_sentiment < strategy_engine.lm_net_sentiment_min
        ):
            return False
    if strategy_engine.lm_net_sentiment_max is not None:
        if (
            lm_net_sentiment is None
            or lm_net_sentiment > strategy_engine.lm_net_sentiment_max
        ):
            return False

    earnings_surprise_pct = _safe_float(row.get("earnings_surprise_pct"))
    if strategy_engine.earnings_surprise_pct_min is not None:
        if (
            earnings_surprise_pct is None
            or earnings_surprise_pct < strategy_engine.earnings_surprise_pct_min
        ):
            return False
    if strategy_engine.earnings_surprise_pct_max is not None:
        if (
            earnings_surprise_pct is None
            or earnings_surprise_pct > strategy_engine.earnings_surprise_pct_max
        ):
            return False

    peer_sector_event_count_365d = _safe_float(row.get("peer_sector_event_count_365d"))
    if strategy_engine.peer_sector_event_count_365d_min is not None:
        if (
            peer_sector_event_count_365d is None
            or peer_sector_event_count_365d < strategy_engine.peer_sector_event_count_365d_min
        ):
            return False
    if strategy_engine.peer_sector_event_count_365d_max is not None:
        if (
            peer_sector_event_count_365d is None
            or peer_sector_event_count_365d > strategy_engine.peer_sector_event_count_365d_max
        ):
            return False

    sector_recent_event_count_3d = _safe_float(row.get("sector_recent_event_count_3d"))
    if strategy_engine.sector_recent_event_count_3d_min is not None:
        if (
            sector_recent_event_count_3d is None
            or sector_recent_event_count_3d < strategy_engine.sector_recent_event_count_3d_min
        ):
            return False
    if strategy_engine.sector_recent_event_count_3d_max is not None:
        if (
            sector_recent_event_count_3d is None
            or sector_recent_event_count_3d > strategy_engine.sector_recent_event_count_3d_max
        ):
            return False

    sector_recent_leader_count_3d = _safe_float(row.get("sector_recent_leader_count_3d"))
    if strategy_engine.sector_recent_leader_count_3d_min is not None:
        if (
            sector_recent_leader_count_3d is None
            or sector_recent_leader_count_3d < strategy_engine.sector_recent_leader_count_3d_min
        ):
            return False
    if strategy_engine.sector_recent_leader_count_3d_max is not None:
        if (
            sector_recent_leader_count_3d is None
            or sector_recent_leader_count_3d > strategy_engine.sector_recent_leader_count_3d_max
        ):
            return False

    sector_recent_leader_reaction_max_3d = _safe_float(
        row.get("sector_recent_leader_reaction_max_3d")
    )
    if strategy_engine.sector_recent_leader_reaction_max_3d_min is not None:
        if (
            sector_recent_leader_reaction_max_3d is None
            or sector_recent_leader_reaction_max_3d
            < strategy_engine.sector_recent_leader_reaction_max_3d_min
        ):
            return False
    if strategy_engine.sector_recent_leader_reaction_max_3d_max is not None:
        if (
            sector_recent_leader_reaction_max_3d is None
            or sector_recent_leader_reaction_max_3d
            > strategy_engine.sector_recent_leader_reaction_max_3d_max
        ):
            return False

    peer_relative_surprise_pct_365d = _safe_float(row.get("peer_relative_surprise_pct_365d"))
    if strategy_engine.peer_relative_surprise_pct_365d_min is not None:
        if (
            peer_relative_surprise_pct_365d is None
            or peer_relative_surprise_pct_365d < strategy_engine.peer_relative_surprise_pct_365d_min
        ):
            return False
    if strategy_engine.peer_relative_surprise_pct_365d_max is not None:
        if (
            peer_relative_surprise_pct_365d is None
            or peer_relative_surprise_pct_365d > strategy_engine.peer_relative_surprise_pct_365d_max
        ):
            return False

    peer_relative_sue_hist_mean_4q_365d = _safe_float(row.get("peer_relative_sue_hist_mean_4q_365d"))
    if strategy_engine.peer_relative_sue_hist_mean_4q_365d_min is not None:
        if (
            peer_relative_sue_hist_mean_4q_365d is None
            or peer_relative_sue_hist_mean_4q_365d < strategy_engine.peer_relative_sue_hist_mean_4q_365d_min
        ):
            return False
    if strategy_engine.peer_relative_sue_hist_mean_4q_365d_max is not None:
        if (
            peer_relative_sue_hist_mean_4q_365d is None
            or peer_relative_sue_hist_mean_4q_365d > strategy_engine.peer_relative_sue_hist_mean_4q_365d_max
        ):
            return False

    prior_catalyst_count_20d = _safe_float(row.get("prior_catalyst_count_20d"))
    if strategy_engine.prior_catalyst_count_20d_min is not None:
        if (
            prior_catalyst_count_20d is None
            or prior_catalyst_count_20d < strategy_engine.prior_catalyst_count_20d_min
        ):
            return False
    if strategy_engine.prior_catalyst_count_20d_max is not None:
        if (
            prior_catalyst_count_20d is None
            or prior_catalyst_count_20d > strategy_engine.prior_catalyst_count_20d_max
        ):
            return False

    prior_catalyst_count_60d = _safe_float(row.get("prior_catalyst_count_60d"))
    if strategy_engine.prior_catalyst_count_60d_min is not None:
        if (
            prior_catalyst_count_60d is None
            or prior_catalyst_count_60d < strategy_engine.prior_catalyst_count_60d_min
        ):
            return False
    if strategy_engine.prior_catalyst_count_60d_max is not None:
        if (
            prior_catalyst_count_60d is None
            or prior_catalyst_count_60d > strategy_engine.prior_catalyst_count_60d_max
        ):
            return False

    prior_catalyst_type_diversity_20d = _safe_float(row.get("prior_catalyst_type_diversity_20d"))
    if strategy_engine.prior_catalyst_type_diversity_20d_min is not None:
        if (
            prior_catalyst_type_diversity_20d is None
            or prior_catalyst_type_diversity_20d < strategy_engine.prior_catalyst_type_diversity_20d_min
        ):
            return False
    if strategy_engine.prior_catalyst_type_diversity_20d_max is not None:
        if (
            prior_catalyst_type_diversity_20d is None
            or prior_catalyst_type_diversity_20d > strategy_engine.prior_catalyst_type_diversity_20d_max
        ):
            return False

    prior_catalyst_type_diversity_60d = _safe_float(row.get("prior_catalyst_type_diversity_60d"))
    if strategy_engine.prior_catalyst_type_diversity_60d_min is not None:
        if (
            prior_catalyst_type_diversity_60d is None
            or prior_catalyst_type_diversity_60d < strategy_engine.prior_catalyst_type_diversity_60d_min
        ):
            return False
    if strategy_engine.prior_catalyst_type_diversity_60d_max is not None:
        if (
            prior_catalyst_type_diversity_60d is None
            or prior_catalyst_type_diversity_60d > strategy_engine.prior_catalyst_type_diversity_60d_max
        ):
            return False

    sentiment_surprise = None
    if lm_net_sentiment is not None:
        surprise_component = (
            earnings_surprise_pct / 100.0 if earnings_surprise_pct is not None else 0.0
        )
        sentiment_surprise = -lm_net_sentiment - surprise_component
    if strategy_engine.sentiment_surprise_min is not None:
        if (
            sentiment_surprise is None
            or sentiment_surprise < strategy_engine.sentiment_surprise_min
        ):
            return False
    if strategy_engine.sentiment_surprise_max is not None:
        if (
            sentiment_surprise is None
            or sentiment_surprise > strategy_engine.sentiment_surprise_max
        ):
            return False

    price_text_dislocation = None
    if lm_net_sentiment is not None and reaction_day_return is not None:
        price_text_dislocation = -lm_net_sentiment - reaction_day_return
    if strategy_engine.price_text_dislocation_min is not None:
        if (
            price_text_dislocation is None
            or price_text_dislocation < strategy_engine.price_text_dislocation_min
        ):
            return False
    if strategy_engine.price_text_dislocation_max is not None:
        if (
            price_text_dislocation is None
            or price_text_dislocation > strategy_engine.price_text_dislocation_max
        ):
            return False

    positive_price_text_dislocation = _safe_float(row.get("positive_price_text_dislocation"))
    if positive_price_text_dislocation is None:
        positive_price_text_dislocation = _compute_positive_price_text_dislocation(row)
    if strategy_engine.positive_price_text_dislocation_min is not None:
        if (
            positive_price_text_dislocation is None
            or positive_price_text_dislocation
            < strategy_engine.positive_price_text_dislocation_min
        ):
            return False
    if strategy_engine.positive_price_text_dislocation_max is not None:
        if (
            positive_price_text_dislocation is None
            or positive_price_text_dislocation
            > strategy_engine.positive_price_text_dislocation_max
        ):
            return False

    positive_price_text_dislocation_rank = _safe_float(
        row.get("positive_price_text_dislocation_rank")
    )
    if strategy_engine.positive_price_text_dislocation_rank_min is not None:
        if (
            positive_price_text_dislocation_rank is None
            or positive_price_text_dislocation_rank
            < strategy_engine.positive_price_text_dislocation_rank_min
        ):
            return False
    if strategy_engine.positive_price_text_dislocation_rank_max is not None:
        if (
            positive_price_text_dislocation_rank is None
            or positive_price_text_dislocation_rank
            > strategy_engine.positive_price_text_dislocation_rank_max
        ):
            return False

    macro_vix = _safe_float(row.get("macro_vix"))
    if strategy_engine.macro_vix_min is not None:
        if macro_vix is None or macro_vix < strategy_engine.macro_vix_min:
            return False
    if strategy_engine.macro_vix_max is not None:
        if macro_vix is None or macro_vix > strategy_engine.macro_vix_max:
            return False

    macro_hy_spread = _safe_float(row.get("macro_hy_spread"))
    if strategy_engine.macro_hy_spread_min is not None:
        if macro_hy_spread is None or macro_hy_spread < strategy_engine.macro_hy_spread_min:
            return False
    if strategy_engine.macro_hy_spread_max is not None:
        if macro_hy_spread is None or macro_hy_spread > strategy_engine.macro_hy_spread_max:
            return False

    pre_event_hurst_60d = _safe_float(row.get("pre_event_hurst_60d"))
    if strategy_engine.pre_event_hurst_60d_min is not None:
        if (
            pre_event_hurst_60d is None
            or pre_event_hurst_60d < strategy_engine.pre_event_hurst_60d_min
        ):
            return False
    if strategy_engine.pre_event_hurst_60d_max is not None:
        if (
            pre_event_hurst_60d is None
            or pre_event_hurst_60d > strategy_engine.pre_event_hurst_60d_max
        ):
            return False

    pre_event_entropy_60d = _safe_float(row.get("pre_event_entropy_60d"))
    if strategy_engine.pre_event_entropy_60d_min is not None:
        if (
            pre_event_entropy_60d is None
            or pre_event_entropy_60d < strategy_engine.pre_event_entropy_60d_min
        ):
            return False
    if strategy_engine.pre_event_entropy_60d_max is not None:
        if (
            pre_event_entropy_60d is None
            or pre_event_entropy_60d > strategy_engine.pre_event_entropy_60d_max
        ):
            return False

    pre_event_short_ratio = _safe_float(row.get("pre_event_short_ratio"))
    if strategy_engine.pre_event_short_ratio_min is not None:
        if (
            pre_event_short_ratio is None
            or pre_event_short_ratio < strategy_engine.pre_event_short_ratio_min
        ):
            return False
    if strategy_engine.pre_event_short_ratio_max is not None:
        if (
            pre_event_short_ratio is None
            or pre_event_short_ratio > strategy_engine.pre_event_short_ratio_max
        ):
            return False

    pre_event_sector_momentum_20d = _safe_float(row.get("pre_event_sector_momentum_20d"))
    if strategy_engine.pre_event_sector_momentum_20d_min is not None:
        if (
            pre_event_sector_momentum_20d is None
            or pre_event_sector_momentum_20d < strategy_engine.pre_event_sector_momentum_20d_min
        ):
            return False
    if strategy_engine.pre_event_sector_momentum_20d_max is not None:
        if (
            pre_event_sector_momentum_20d is None
            or pre_event_sector_momentum_20d > strategy_engine.pre_event_sector_momentum_20d_max
        ):
            return False

    pre_event_bb_position = _safe_float(row.get("pre_event_bb_position"))
    if strategy_engine.pre_event_bb_position_min is not None:
        if (
            pre_event_bb_position is None
            or pre_event_bb_position < strategy_engine.pre_event_bb_position_min
        ):
            return False
    if strategy_engine.pre_event_bb_position_max is not None:
        if (
            pre_event_bb_position is None
            or pre_event_bb_position > strategy_engine.pre_event_bb_position_max
        ):
            return False

    pre_event_gravitational_pull = _safe_float(row.get("pre_event_gravitational_pull"))
    if strategy_engine.pre_event_gravitational_pull_min is not None:
        if (
            pre_event_gravitational_pull is None
            or pre_event_gravitational_pull < strategy_engine.pre_event_gravitational_pull_min
        ):
            return False
    if strategy_engine.pre_event_gravitational_pull_max is not None:
        if (
            pre_event_gravitational_pull is None
            or pre_event_gravitational_pull > strategy_engine.pre_event_gravitational_pull_max
        ):
            return False

    pre_event_market_temperature = _safe_float(row.get("pre_event_market_temperature"))
    if strategy_engine.pre_event_market_temperature_min is not None:
        if (
            pre_event_market_temperature is None
            or pre_event_market_temperature < strategy_engine.pre_event_market_temperature_min
        ):
            return False
    if strategy_engine.pre_event_market_temperature_max is not None:
        if (
            pre_event_market_temperature is None
            or pre_event_market_temperature > strategy_engine.pre_event_market_temperature_max
        ):
            return False

    if (
        strategy_engine.weak_reaction_threshold is not None
        and strategy_engine.weak_reaction_gap_max is not None
        and reaction_day_return is not None
        and gap_size is not None
        and reaction_day_return < strategy_engine.weak_reaction_threshold
        and gap_size > strategy_engine.weak_reaction_gap_max
    ):
        return False

    if (
        strategy_engine.unknown_direction_reaction_min is not None
        and event_direction == "unknown"
        and reaction_day_return is not None
        and reaction_day_return < strategy_engine.unknown_direction_reaction_min
    ):
        return False
    if (
        strategy_engine.unknown_direction_close_location_min is not None
        and event_direction == "unknown"
        and close_location is not None
        and close_location < strategy_engine.unknown_direction_close_location_min
    ):
        return False
    if (
        strategy_engine.unknown_direction_close_location_max is not None
        and event_direction == "unknown"
        and close_location is not None
        and close_location > strategy_engine.unknown_direction_close_location_max
    ):
        return False
    if (
        strategy_engine.unknown_direction_gap_size_min is not None
        and event_direction == "unknown"
        and gap_size is not None
        and gap_size < strategy_engine.unknown_direction_gap_size_min
    ):
        return False

    is_mixed_inline = (
        event_direction == "mixed"
        and guidance_status == "inline_or_maintained"
    )
    if (
        strategy_engine.mixed_inline_close_location_min is not None
        and is_mixed_inline
        and close_location is not None
        and close_location < strategy_engine.mixed_inline_close_location_min
    ):
        return False
    if (
        strategy_engine.mixed_inline_close_location_max is not None
        and is_mixed_inline
        and close_location is not None
        and close_location > strategy_engine.mixed_inline_close_location_max
    ):
        return False
    if (
        strategy_engine.mixed_inline_gap_size_max is not None
        and is_mixed_inline
        and gap_size is not None
        and gap_size > strategy_engine.mixed_inline_gap_size_max
    ):
        return False

    if (
        strategy_engine.entry_timing_policy == "reaction_close"
        and row.get("event_close") in (None, 0, 0.0, "")
    ):
        return False

    return True


def _matches_strategy_engine(
    row: dict[str, Any],
    strategy_engine: StrategyEngineConfig,
    event_type: str,
    timing_class: str,
    trade_direction: str,
    engine_lookup: dict[str, StrategyEngineConfig] | None = None,
) -> bool:
    resolved_engine = _resolve_strategy_engine(strategy_engine, engine_lookup=engine_lookup)
    if resolved_engine is None:
        return False

    if not _row_matches_strategy_engine_filters(
        row=row,
        strategy_engine=resolved_engine,
        event_type=event_type,
        timing_class=timing_class,
        trade_direction=trade_direction,
    ):
        return False

    exclude_engine_id = resolved_engine.exclude_if_matches_engine_id
    if exclude_engine_id and engine_lookup:
        excluded_engine = engine_lookup.get(exclude_engine_id)
        if excluded_engine is not None:
            resolved_excluded = _resolve_strategy_engine(excluded_engine, engine_lookup=engine_lookup)
            if resolved_excluded is not None and _row_matches_strategy_engine_filters(
                row=row,
                strategy_engine=resolved_excluded,
                event_type=event_type,
                timing_class=timing_class,
                trade_direction=trade_direction,
            ):
                return False

    return True


def _classify_score_bucket(score: float) -> str:
    if score >= 0.8:
        return "high"
    if score >= 0.6:
        return "medium_high"
    if score >= 0.4:
        return "medium"
    if score >= 0.2:
        return "medium_low"
    return "low"


def _safe_float(raw: Any) -> float | None:
    try:
        return float(raw)
    except (TypeError, ValueError):
        return None


def _compute_positive_price_text_dislocation(row: dict[str, Any]) -> float | None:
    lm_net_sentiment = _safe_float(row.get("lm_net_sentiment"))
    reaction_day_return = _safe_float(row.get("reaction_day_return"))
    if lm_net_sentiment is None or reaction_day_return is None:
        return None

    earnings_surprise_pct = _safe_float(row.get("earnings_surprise_pct")) or 0.0
    surprise_component = max(earnings_surprise_pct, 0.0) / 100.0
    return lm_net_sentiment + surprise_component - reaction_day_return


def _augment_rows_with_cross_sectional_rank_features(
    raw_rows: list[dict[str, Any]],
) -> list[dict[str, Any]]:
    if not raw_rows:
        return []

    prepared_rows = [dict(row) for row in raw_rows]
    scored_rows: list[tuple[int, float]] = []
    for idx, row in enumerate(prepared_rows):
        metric = _safe_float(row.get("positive_price_text_dislocation"))
        if metric is None:
            metric = _compute_positive_price_text_dislocation(row)
        row["positive_price_text_dislocation"] = metric
        if metric is not None:
            scored_rows.append((idx, metric))

    scored_rows.sort(key=lambda item: item[1], reverse=True)
    if len(scored_rows) == 1:
        prepared_rows[scored_rows[0][0]]["positive_price_text_dislocation_rank"] = 1.0
        return prepared_rows

    denominator = len(scored_rows) - 1
    for rank_index, (row_index, _) in enumerate(scored_rows):
        prepared_rows[row_index]["positive_price_text_dislocation_rank"] = (
            1.0 - (rank_index / denominator)
        )

    return prepared_rows


def rank_candidates(
    candidates: list[Candidate],
    ranking_fields: list[str] | None = None,
) -> list[Candidate]:
    """Sort candidates using configurable ranking fields.

    Field syntax:
    - `score` / `avg_dollar_volume` etc.: ascending
    - `-score` / `-close_location`: descending

    When no custom fields are provided, preserve the historical default:
    `score DESC, avg_dollar_volume DESC, symbol ASC`.
    """
    if not ranking_fields:
        return sorted(candidates, key=lambda c: (-c.score, -c.avg_dollar_volume, c.symbol))

    normalized_fields = list(ranking_fields) + ["symbol"]
    return sorted(candidates, key=lambda c: _candidate_sort_key(c, normalized_fields))


def _candidate_sort_key(candidate: Candidate, ranking_fields: list[str]) -> tuple[Any, ...]:
    keys: list[Any] = []
    for field in ranking_fields:
        descending = field.startswith("-")
        raw_field = field[1:] if descending else field
        value = _resolve_candidate_field(candidate, raw_field)
        if raw_field == "symbol":
            keys.append(str(value or ""))
            continue

        if isinstance(value, (int, float)):
            numeric = float(value)
            keys.append(-numeric if descending else numeric)
            continue

        text = str(value or "")
        keys.append(_invert_text_key(text) if descending else text)
    return tuple(keys)


def _resolve_candidate_field(candidate: Candidate, field: str) -> Any:
    if hasattr(candidate, field):
        return getattr(candidate, field)

    if field == "score_band_2dp":
        return math.floor(candidate.score * 100.0 + 1e-9)
    if field == "guidance_raised_flag":
        return 1.0 if str(candidate.features.get("guidance_status", "")).lower() == "raised" else 0.0
    if field == "bullish_direction_flag":
        return 1.0 if str(candidate.features.get("event_direction", "")).lower() == "bullish" else 0.0
    if field == "mixed_direction_flag":
        return 1.0 if str(candidate.features.get("event_direction", "")).lower() == "mixed" else 0.0
    if field == "unknown_direction_flag":
        return 1.0 if str(candidate.features.get("event_direction", "")).lower() == "unknown" else 0.0
    if field == "prior_positive_flag":
        prior = _safe_float(candidate.features.get("prior_event_fwd5d"))
        return 1.0 if prior is not None and prior > 0.02 else 0.0
    if field == "macro_favorable_flag":
        vix = _safe_float(candidate.features.get("macro_vix"))
        hy = _safe_float(candidate.features.get("macro_hy_spread"))
        return 1.0 if vix is not None and hy is not None and vix > 18.0 and hy > 3.25 else 0.0

    return candidate.features.get(field)


def _invert_text_key(value: str) -> tuple[int, ...]:
    return tuple(-ord(char) for char in value)


def filter_by_universe(
    candidates: list[Candidate],
    config: UniverseConfig,
) -> list[Candidate]:
    """Apply universe filters: min_price, min_avg_dollar_volume, exchange."""
    filtered = []
    for c in candidates:
        min_price = (
            c.engine_min_entry_price
            if c.engine_min_entry_price is not None
            else config.min_price
        )
        if c.entry_price_est < min_price:
            continue
        if (
            c.engine_max_entry_price is not None
            and c.entry_price_est > c.engine_max_entry_price
        ):
            continue
        if c.avg_dollar_volume < config.min_avg_dollar_volume:
            continue
        if config.min_market_cap_proxy is not None:
            market_cap_proxy = _safe_float(c.features.get("market_cap_proxy"))
            if market_cap_proxy is None or market_cap_proxy < config.min_market_cap_proxy:
                continue
        if config.allowed_exchanges:
            exchange = str(c.features.get("exchange_proxy") or c.features.get("exchange") or "").upper()
            if exchange and exchange not in {e.upper() for e in config.allowed_exchanges}:
                continue
        if config.exclude_asset_types:
            asset_type = str(c.features.get("asset_type_proxy") or c.features.get("asset_type") or "").upper()
            if asset_type and asset_type in {a.upper() for a in config.exclude_asset_types}:
                continue
        filtered.append(c)
    return filtered


def filter_by_score(
    candidates: list[Candidate],
    score_threshold: float,
) -> list[Candidate]:
    return [c for c in candidates if c.score >= score_threshold]


def filter_by_momentum(
    candidates: list[Candidate],
    max_momentum_20d: float | None,
) -> list[Candidate]:
    """Reject candidates where pre-event 20d return exceeds the cap."""
    if max_momentum_20d is None:
        return candidates
    filtered = []
    for c in candidates:
        mom = c.features.get("pre_event_momentum_20d")
        if mom is not None and float(mom) > max_momentum_20d:
            logger.debug("momentum_gate_reject", symbol=c.symbol, momentum=mom, cap=max_momentum_20d)
            continue
        filtered.append(c)
    return filtered


def truncate_candidates(
    candidates: list[Candidate],
    max_per_day: int,
) -> list[Candidate]:
    return candidates[:max_per_day]


def filter_by_event_type(
    candidates: list[Candidate],
    profiles: dict[str, EventTypeProfile],
) -> list[Candidate]:
    """Filter out candidates whose event_type is unknown, disabled, or below per-type threshold.

    Default-deny: if profiles dict is non-empty and event_type is not in profiles,
    the candidate is skipped (unknown event types are blocked).
    """
    if not profiles:
        return candidates
    filtered = []
    for c in candidates:
        profile = profiles.get(c.event_type)
        if profile is None:
            logger.debug("skip_unknown_event_type", symbol=c.symbol, event_type=c.event_type)
            continue
        if not profile.enabled:
            logger.debug("skip_disabled_event_type", symbol=c.symbol, event_type=c.event_type)
            continue
        if profile.score_threshold_override is not None:
            if c.score < profile.score_threshold_override:
                logger.debug(
                    "skip_event_type_score",
                    symbol=c.symbol,
                    event_type=c.event_type,
                    score=c.score,
                    threshold=profile.score_threshold_override,
                )
                continue
        filtered.append(c)
    return filtered


def select_candidates(
    raw_rows: list[dict[str, Any]],
    universe_config: UniverseConfig,
    signal_config: SignalConfig,
    event_type_profiles: dict[str, EventTypeProfile] | None = None,
    strategy_engine: StrategyEngineConfig | None = None,
    engine_lookup: dict[str, StrategyEngineConfig] | None = None,
    truncate_to: int | None = None,
    excluded_event_ids: set[str] | None = None,
    excluded_symbols: set[str] | None = None,
) -> list[Candidate]:
    """Full selection pipeline: build → filter → rank → truncate."""
    candidates = []
    prepared_rows = _augment_rows_with_cross_sectional_rank_features(raw_rows)
    excluded_event_ids = excluded_event_ids or set()
    excluded_symbols = {symbol.upper() for symbol in (excluded_symbols or set())}
    for row in prepared_rows:
        event_id = str(row.get("event_id", ""))
        if event_id and event_id in excluded_event_ids:
            continue
        prepared_row = _prepare_row_for_strategy_engine(
            row,
            signal_config=signal_config,
            strategy_engine=strategy_engine,
        )
        c = build_candidate(
            prepared_row,
            strategy_engine=strategy_engine,
            engine_lookup=engine_lookup,
        )
        if c is not None:
            if c.symbol and c.symbol.upper() in excluded_symbols:
                continue
            candidates.append(c)

    candidates = filter_by_universe(candidates, universe_config)
    candidates = filter_by_score(
        candidates,
        _resolve_score_threshold(signal_config, strategy_engine),
    )
    candidates = filter_by_momentum(
        candidates,
        signal_config.pre_event_momentum_20d_max,
    )
    if event_type_profiles:
        candidates = filter_by_event_type(candidates, event_type_profiles)
    candidates = rank_candidates(
        candidates,
        _resolve_ranking_fields(signal_config, strategy_engine),
    )
    if strategy_engine and strategy_engine.trade_symbol_mode != "event":
        candidates = _dedupe_candidates_by_symbol(candidates)
    candidates = truncate_candidates(
        candidates,
        truncate_to if truncate_to is not None else signal_config.max_candidates_per_day,
    )
    return candidates


def _dedupe_candidates_by_symbol(candidates: list[Candidate]) -> list[Candidate]:
    seen_symbols: set[str] = set()
    deduped: list[Candidate] = []
    for candidate in candidates:
        symbol = candidate.symbol.upper()
        if symbol in seen_symbols:
            continue
        seen_symbols.add(symbol)
        deduped.append(candidate)
    return deduped


def _resolve_score_threshold(
    signal_config: SignalConfig,
    strategy_engine: StrategyEngineConfig | None,
) -> float:
    if strategy_engine and strategy_engine.score_threshold_override is not None:
        return strategy_engine.score_threshold_override
    return signal_config.score_threshold


def _resolve_ranking_fields(
    signal_config: SignalConfig,
    strategy_engine: StrategyEngineConfig | None,
) -> list[str] | None:
    if strategy_engine and strategy_engine.ranking_fields_override is not None:
        return strategy_engine.ranking_fields_override
    return signal_config.ranking_fields


def _prepare_row_for_strategy_engine(
    row: dict[str, Any],
    signal_config: SignalConfig,
    strategy_engine: StrategyEngineConfig | None,
) -> dict[str, Any]:
    prepared = row
    if signal_config.ranking_model_path:
        from libs.backtest.ranking_models import compute_ranking_model_score

        ranking_score = compute_ranking_model_score(row, signal_config.ranking_model_path)
        if ranking_score is not None:
            prepared = dict(prepared)
            prepared["ranking_model_score"] = ranking_score
            prepared["learned_rank_score"] = ranking_score

    if strategy_engine is None or signal_config.scoring_model != "pead":
        return prepared

    reaction_threshold = (
        strategy_engine.pead_reaction_threshold_override
        if strategy_engine.pead_reaction_threshold_override is not None
        else signal_config.pead_reaction_threshold
    )
    volume_threshold = (
        strategy_engine.pead_volume_threshold_override
        if strategy_engine.pead_volume_threshold_override is not None
        else signal_config.pead_volume_threshold
    )

    if (
        reaction_threshold == signal_config.pead_reaction_threshold
        and volume_threshold == signal_config.pead_volume_threshold
    ):
        return prepared

    from libs.backtest.scoring import compute_pead_score

    if prepared is row:
        prepared = dict(row)
    prepared["score"] = compute_pead_score(
        prepared,
        reaction_threshold=reaction_threshold,
        volume_threshold=volume_threshold,
    )
    return prepared
