use std::collections::{BTreeMap, BTreeSet};

use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime};

use crate::cost::CostModel;
use crate::data::{DataSet, IntradayExecutionQuote, PriceField};
use crate::engine::BacktestError;
use crate::events::{AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent};
use crate::portfolio::PortfolioState;
use crate::rules::EquityRuleHooks;
use crate::strategy::{OrderIntent, StrategyDecision};

#[derive(Debug, Default)]
pub struct BrokerExecutionReport {
    pub order_events: Vec<OrderEvent>,
    pub fill_events: Vec<FillEvent>,
    pub position_events: Vec<PositionEvent>,
    pub account_events: Vec<AccountEvent>,
}

#[derive(Debug, Clone, Copy)]
struct ExecutionFill {
    price: f64,
    quantity: u32,
    next_cursor: NaiveDateTime,
}

#[derive(Debug, Clone)]
struct TargetConstraint {
    symbol: String,
    current_qty: u32,
    min_target_qty: u32,
    max_target_qty: u32,
    provisional_target_qty: u32,
    price: f64,
    round_lot: u32,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatchingType {
    CurrentBarClose,
    NextBarOpen,
    NextTickLast,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SlippageModel {
    None,
    PriceRatio(f64),
    TickSize(f64),
}

pub struct BrokerSimulator<C, R> {
    cost_model: C,
    rules: R,
    board_lot_size: u32,
    execution_price_field: PriceField,
    slippage_model: SlippageModel,
    volume_percent: f64,
    volume_limit: bool,
    inactive_limit: bool,
    liquidity_limit: bool,
    intraday_execution_start_time: Option<NaiveTime>,
}

impl<C, R> BrokerSimulator<C, R> {
    pub fn new(cost_model: C, rules: R) -> Self {
        Self {
            cost_model,
            rules,
            board_lot_size: 100,
            execution_price_field: PriceField::Open,
            slippage_model: SlippageModel::None,
            volume_percent: 0.25,
            volume_limit: true,
            inactive_limit: true,
            liquidity_limit: true,
            intraday_execution_start_time: None,
        }
    }

    pub fn new_with_execution_price(
        cost_model: C,
        rules: R,
        execution_price_field: PriceField,
    ) -> Self {
        Self {
            cost_model,
            rules,
            board_lot_size: 100,
            execution_price_field,
            slippage_model: SlippageModel::None,
            volume_percent: 0.25,
            volume_limit: true,
            inactive_limit: true,
            liquidity_limit: true,
            intraday_execution_start_time: None,
        }
    }

    pub fn with_volume_limit(mut self, enabled: bool) -> Self {
        self.volume_limit = enabled;
        self
    }

    pub fn with_inactive_limit(mut self, enabled: bool) -> Self {
        self.inactive_limit = enabled;
        self
    }

    pub fn with_liquidity_limit(mut self, enabled: bool) -> Self {
        self.liquidity_limit = enabled;
        self
    }

    pub fn with_volume_percent(mut self, volume_percent: f64) -> Self {
        self.volume_percent = volume_percent;
        self
    }

    pub fn with_intraday_execution_start_time(mut self, start_time: NaiveTime) -> Self {
        self.intraday_execution_start_time = Some(start_time);
        self
    }

    pub fn with_slippage_model(mut self, slippage_model: SlippageModel) -> Self {
        self.slippage_model = slippage_model;
        self
    }
}

impl<C, R> BrokerSimulator<C, R>
where
    C: CostModel,
    R: EquityRuleHooks,
{
    fn buy_price(&self, snapshot: &crate::data::DailyMarketSnapshot) -> f64 {
        snapshot.buy_price(self.execution_price_field)
    }

    fn sell_price(&self, snapshot: &crate::data::DailyMarketSnapshot) -> f64 {
        snapshot.sell_price(self.execution_price_field)
    }

    fn sizing_price(&self, snapshot: &crate::data::DailyMarketSnapshot) -> f64 {
        snapshot.price(self.execution_price_field)
    }

    fn snapshot_execution_price(
        &self,
        snapshot: &crate::data::DailyMarketSnapshot,
        side: OrderSide,
    ) -> f64 {
        let raw_price = if self.execution_price_field == PriceField::Last
            && self.intraday_execution_start_time.is_some()
        {
            let _ = side;
            snapshot.price(PriceField::Last)
        } else {
            match side {
                OrderSide::Buy => self.buy_price(snapshot),
                OrderSide::Sell => self.sell_price(snapshot),
            }
        };

        self.apply_slippage(snapshot, side, raw_price)
    }

    fn apply_slippage(
        &self,
        snapshot: &crate::data::DailyMarketSnapshot,
        side: OrderSide,
        raw_price: f64,
    ) -> f64 {
        if !raw_price.is_finite() || raw_price <= 0.0 {
            return raw_price;
        }

        let adjusted = match self.slippage_model {
            SlippageModel::None => raw_price,
            SlippageModel::PriceRatio(ratio) => {
                let ratio = ratio.max(0.0);
                match side {
                    OrderSide::Buy => raw_price * (1.0 + ratio),
                    OrderSide::Sell => raw_price * (1.0 - ratio),
                }
            }
            SlippageModel::TickSize(ticks) => {
                let tick = snapshot.effective_price_tick();
                let ticks = ticks.max(0.0);
                match side {
                    OrderSide::Buy => raw_price + tick * ticks,
                    OrderSide::Sell => raw_price - tick * ticks,
                }
            }
        };

        self.clamp_execution_price(snapshot, side, adjusted)
    }

    fn clamp_execution_price(
        &self,
        snapshot: &crate::data::DailyMarketSnapshot,
        side: OrderSide,
        adjusted_price: f64,
    ) -> f64 {
        if !adjusted_price.is_finite() {
            return adjusted_price;
        }

        let mut bounded = adjusted_price.max(snapshot.effective_price_tick());
        match side {
            OrderSide::Buy => {
                if snapshot.upper_limit.is_finite() && snapshot.upper_limit > 0.0 {
                    bounded = bounded.min(snapshot.upper_limit);
                }
            }
            OrderSide::Sell => {
                if snapshot.lower_limit.is_finite() && snapshot.lower_limit > 0.0 {
                    bounded = bounded.max(snapshot.lower_limit);
                }
            }
        }
        bounded
    }

    fn quote_execution_price(
        &self,
        snapshot: &crate::data::DailyMarketSnapshot,
        side: OrderSide,
        raw_price: f64,
    ) -> f64 {
        self.apply_slippage(snapshot, side, raw_price)
    }

    fn select_quote_reference_price(
        &self,
        snapshot: &crate::data::DailyMarketSnapshot,
        quote: &IntradayExecutionQuote,
        side: OrderSide,
    ) -> Option<f64> {
        let raw_price = match side {
            OrderSide::Buy => quote.buy_price(),
            OrderSide::Sell => quote.sell_price(),
        }?;
        let execution_price = self.quote_execution_price(snapshot, side, raw_price);
        if execution_price.is_finite() && execution_price > 0.0 {
            Some(execution_price)
        } else {
            None
        }
    }

    pub fn execute(
        &self,
        date: NaiveDate,
        portfolio: &mut PortfolioState,
        data: &DataSet,
        decision: &StrategyDecision,
    ) -> Result<BrokerExecutionReport, BacktestError> {
        let mut report = BrokerExecutionReport::default();
        let mut intraday_turnover = BTreeMap::<String, u32>::new();
        let mut execution_cursors = BTreeMap::<String, NaiveDateTime>::new();
        let mut global_execution_cursor = None::<NaiveDateTime>;
        if !decision.order_intents.is_empty() {
            for intent in &decision.order_intents {
                self.process_order_intent(
                    date,
                    portfolio,
                    data,
                    intent,
                    &mut intraday_turnover,
                    &mut execution_cursors,
                    &mut global_execution_cursor,
                    &mut report,
                )?;
            }
            portfolio.prune_flat_positions();
            return Ok(report);
        }

        let target_quantities = if decision.rebalance {
            self.target_quantities(date, portfolio, data, &decision.target_weights)?
        } else {
            BTreeMap::new()
        };

        let mut sell_symbols = BTreeSet::new();
        sell_symbols.extend(portfolio.positions().keys().cloned());
        sell_symbols.extend(decision.exit_symbols.iter().cloned());
        sell_symbols.extend(target_quantities.keys().cloned());

        for symbol in sell_symbols {
            let current_qty = portfolio
                .position(&symbol)
                .map(|pos| pos.quantity)
                .unwrap_or(0);
            if current_qty == 0 {
                continue;
            }

            let target_qty = if decision.exit_symbols.contains(&symbol) {
                0
            } else if decision.rebalance {
                *target_quantities.get(&symbol).unwrap_or(&0)
            } else {
                current_qty
            };

            if current_qty > target_qty {
                let requested_qty = current_qty - target_qty;
                self.process_sell(
                    date,
                    portfolio,
                    data,
                    &symbol,
                    requested_qty,
                    sell_reason(decision, &symbol),
                    &mut intraday_turnover,
                    &mut execution_cursors,
                    &mut global_execution_cursor,
                    &mut report,
                )?;
            }
        }

        if decision.rebalance {
            for (symbol, target_qty) in target_quantities {
                let current_qty = portfolio
                    .position(&symbol)
                    .map(|pos| pos.quantity)
                    .unwrap_or(0);
                if target_qty > current_qty {
                    let requested_qty = target_qty - current_qty;
                    self.process_buy(
                        date,
                        portfolio,
                        data,
                        &symbol,
                        requested_qty,
                        "rebalance_buy",
                        &mut intraday_turnover,
                        &mut execution_cursors,
                        &mut global_execution_cursor,
                        None,
                        &mut report,
                    )?;
                }
            }
        }

        portfolio.prune_flat_positions();
        Ok(report)
    }

    fn process_order_intent(
        &self,
        date: NaiveDate,
        portfolio: &mut PortfolioState,
        data: &DataSet,
        intent: &OrderIntent,
        intraday_turnover: &mut BTreeMap<String, u32>,
        execution_cursors: &mut BTreeMap<String, NaiveDateTime>,
        global_execution_cursor: &mut Option<NaiveDateTime>,
        report: &mut BrokerExecutionReport,
    ) -> Result<(), BacktestError> {
        match intent {
            OrderIntent::TargetValue {
                symbol,
                target_value,
                reason,
            } => self.process_target_value(
                date,
                portfolio,
                data,
                symbol,
                *target_value,
                reason,
                intraday_turnover,
                execution_cursors,
                global_execution_cursor,
                report,
            ),
            OrderIntent::Value {
                symbol,
                value,
                reason,
            } => self.process_value(
                date,
                portfolio,
                data,
                symbol,
                *value,
                reason,
                intraday_turnover,
                execution_cursors,
                global_execution_cursor,
                report,
            ),
        }
    }

    fn target_quantities(
        &self,
        date: NaiveDate,
        portfolio: &PortfolioState,
        data: &DataSet,
        target_weights: &BTreeMap<String, f64>,
    ) -> Result<BTreeMap<String, u32>, BacktestError> {
        let equity = self.total_equity_at(date, portfolio, data, self.execution_price_field)?;
        let mut desired_targets = BTreeMap::new();
        for (symbol, weight) in target_weights {
            let price = data
                .price(date, symbol, self.execution_price_field)
                .ok_or_else(|| BacktestError::MissingPrice {
                    date,
                    symbol: symbol.clone(),
                    field: price_field_name(self.execution_price_field),
                })?;
            let raw_qty = ((equity * weight) / price).floor() as u32;
            desired_targets.insert(
                symbol.clone(),
                self.round_buy_quantity(raw_qty, self.round_lot(data, symbol)),
            );
        }

        let mut symbols = BTreeSet::new();
        symbols.extend(portfolio.positions().keys().cloned());
        symbols.extend(desired_targets.keys().cloned());

        let mut constraints = Vec::new();
        let mut projected_cash = portfolio.cash();
        for symbol in symbols {
            let current_qty = portfolio
                .position(&symbol)
                .map(|pos| pos.quantity)
                .unwrap_or(0);
            let desired_qty = *desired_targets.get(&symbol).unwrap_or(&0);
            let price = data
                .price(date, &symbol, self.execution_price_field)
                .ok_or_else(|| BacktestError::MissingPrice {
                    date,
                    symbol: symbol.clone(),
                    field: price_field_name(self.execution_price_field),
                })?;
            let round_lot = self.round_lot(data, &symbol);
            let min_target_qty = self.minimum_target_quantity(
                date,
                portfolio,
                data,
                &symbol,
                current_qty,
                round_lot,
            );
            let max_target_qty = self.maximum_target_quantity(
                date,
                portfolio,
                data,
                &symbol,
                current_qty,
                round_lot,
            );
            let provisional_target_qty = desired_qty.clamp(min_target_qty, max_target_qty);
            if current_qty > provisional_target_qty {
                projected_cash += self.estimated_sell_net_cash(
                    price,
                    current_qty.saturating_sub(provisional_target_qty),
                );
            }
            constraints.push(TargetConstraint {
                symbol,
                current_qty,
                min_target_qty,
                max_target_qty,
                provisional_target_qty,
                price,
                round_lot,
            });
        }

        let mut targets = BTreeMap::new();
        for constraint in &constraints {
            let mut target_qty = constraint.provisional_target_qty;
            if target_qty > constraint.current_qty {
                let desired_additional = target_qty - constraint.current_qty;
                let affordable_additional = self.affordable_buy_quantity(
                    projected_cash,
                    None,
                    constraint.price,
                    desired_additional,
                    constraint.round_lot,
                );
                target_qty = (constraint.current_qty + affordable_additional)
                    .clamp(constraint.min_target_qty, constraint.max_target_qty);
                if target_qty > constraint.current_qty {
                    projected_cash -= self.estimated_buy_cash_out(
                        constraint.price,
                        target_qty - constraint.current_qty,
                    );
                }
            }

            if target_qty > 0 {
                targets.insert(constraint.symbol.clone(), target_qty);
            }
        }

        Ok(targets)
    }

    fn minimum_target_quantity(
        &self,
        date: NaiveDate,
        portfolio: &PortfolioState,
        data: &DataSet,
        symbol: &str,
        current_qty: u32,
        round_lot: u32,
    ) -> u32 {
        if current_qty == 0 {
            return 0;
        }
        let Some(position) = portfolio.position(symbol) else {
            return 0;
        };
        let Ok(snapshot) = data.require_market(date, symbol) else {
            return current_qty;
        };
        let Ok(candidate) = data.require_candidate(date, symbol) else {
            return current_qty;
        };
        let rule = self.rules.can_sell(
            date,
            snapshot,
            candidate,
            position,
            self.execution_price_field,
        );
        if !rule.allowed {
            return current_qty;
        }
        let sellable = position.sellable_qty(date);
        let sell_limit = match self.market_fillable_quantity(
            snapshot,
            OrderSide::Sell,
            sellable.min(current_qty),
            round_lot,
            0,
        ) {
            Ok(quantity) => quantity.min(sellable).min(current_qty),
            Err(_) => 0,
        };
        current_qty.saturating_sub(sell_limit)
    }

    fn maximum_target_quantity(
        &self,
        date: NaiveDate,
        _portfolio: &PortfolioState,
        data: &DataSet,
        symbol: &str,
        current_qty: u32,
        round_lot: u32,
    ) -> u32 {
        let Ok(snapshot) = data.require_market(date, symbol) else {
            return current_qty;
        };
        let Ok(candidate) = data.require_candidate(date, symbol) else {
            return current_qty;
        };
        let rule = self
            .rules
            .can_buy(date, snapshot, candidate, self.execution_price_field);
        if !rule.allowed {
            return current_qty;
        }
        let additional_limit =
            match self.market_fillable_quantity(snapshot, OrderSide::Buy, u32::MAX, round_lot, 0) {
                Ok(quantity) => quantity,
                Err(_) => 0,
            };
        current_qty.saturating_add(additional_limit)
    }

    fn estimated_sell_net_cash(&self, price: f64, quantity: u32) -> f64 {
        if quantity == 0 {
            return 0.0;
        }
        let gross = price * quantity as f64;
        let cost = self.cost_model.calculate(OrderSide::Sell, gross);
        gross - cost.total()
    }

    fn estimated_buy_cash_out(&self, price: f64, quantity: u32) -> f64 {
        if quantity == 0 {
            return 0.0;
        }
        let gross = price * quantity as f64;
        let cost = self.cost_model.calculate(OrderSide::Buy, gross);
        gross + cost.total()
    }

    fn process_sell(
        &self,
        date: NaiveDate,
        portfolio: &mut PortfolioState,
        data: &DataSet,
        symbol: &str,
        requested_qty: u32,
        reason: &str,
        intraday_turnover: &mut BTreeMap<String, u32>,
        execution_cursors: &mut BTreeMap<String, NaiveDateTime>,
        global_execution_cursor: &mut Option<NaiveDateTime>,
        report: &mut BrokerExecutionReport,
    ) -> Result<(), BacktestError> {
        let snapshot = data.require_market(date, symbol)?;
        let candidate = data.require_candidate(date, symbol)?;
        let Some(position) = portfolio.position(symbol) else {
            return Ok(());
        };

        let rule = self.rules.can_sell(
            date,
            snapshot,
            candidate,
            position,
            self.execution_price_field,
        );
        if !rule.allowed {
            let status = match rule.reason.as_deref() {
                Some("paused")
                | Some("sell disabled by eligibility flags")
                | Some("open at or below lower limit") => OrderStatus::Canceled,
                _ => OrderStatus::Rejected,
            };
            report.order_events.push(OrderEvent {
                date,
                symbol: symbol.to_string(),
                side: OrderSide::Sell,
                requested_quantity: requested_qty,
                filled_quantity: 0,
                status,
                reason: format!("{reason}: {}", rule.reason.unwrap_or_default()),
            });
            return Ok(());
        }

        let sellable = position.sellable_qty(date);
        let market_limited_qty = self.market_fillable_quantity(
            snapshot,
            OrderSide::Sell,
            requested_qty.min(sellable),
            self.round_lot(data, symbol),
            *intraday_turnover.get(symbol).unwrap_or(&0),
        );
        let filled_qty = match market_limited_qty {
            Ok(quantity) => quantity.min(sellable),
            Err(limit_reason) => {
                report.order_events.push(OrderEvent {
                    date,
                    symbol: symbol.to_string(),
                    side: OrderSide::Sell,
                    requested_quantity: requested_qty,
                    filled_quantity: 0,
                    status: OrderStatus::Rejected,
                    reason: format!("{reason}: {limit_reason}"),
                });
                return Ok(());
            }
        };
        if filled_qty == 0 {
            report.order_events.push(OrderEvent {
                date,
                symbol: symbol.to_string(),
                side: OrderSide::Sell,
                requested_quantity: requested_qty,
                filled_quantity: 0,
                status: OrderStatus::Rejected,
                reason: format!("{reason}: no sellable quantity"),
            });
            return Ok(());
        }

        let cash_before = portfolio.cash();
        let fill = self.resolve_execution_fill(
            date,
            symbol,
            OrderSide::Sell,
            snapshot,
            data,
            filled_qty,
            self.round_lot(data, symbol),
            execution_cursors,
            None,
            None,
            None,
        );
        let (filled_qty, execution_price) = if let Some(fill) = fill {
            execution_cursors.insert(symbol.to_string(), fill.next_cursor);
            if self.uses_serial_execution_cursor(reason) {
                *global_execution_cursor = Some(fill.next_cursor);
            }
            (fill.quantity, fill.price)
        } else {
            (filled_qty, self.sell_price(snapshot))
        };
        let gross_amount = execution_price * filled_qty as f64;
        let cost = self.cost_model.calculate(OrderSide::Sell, gross_amount);
        let net_cash = gross_amount - cost.total();

        let realized_pnl = portfolio
            .position_mut(symbol)
            .sell(filled_qty, execution_price)
            .map_err(BacktestError::Execution)?;
        portfolio.apply_cash_delta(net_cash);
        portfolio.prune_flat_positions();
        *intraday_turnover.entry(symbol.to_string()).or_default() += filled_qty;

        let status = if filled_qty < requested_qty {
            OrderStatus::PartiallyFilled
        } else {
            OrderStatus::Filled
        };

        report.order_events.push(OrderEvent {
            date,
            symbol: symbol.to_string(),
            side: OrderSide::Sell,
            requested_quantity: requested_qty,
            filled_quantity: filled_qty,
            status,
            reason: reason.to_string(),
        });
        report.fill_events.push(FillEvent {
            date,
            symbol: symbol.to_string(),
            side: OrderSide::Sell,
            quantity: filled_qty,
            price: execution_price,
            gross_amount,
            commission: cost.commission,
            stamp_tax: cost.stamp_tax,
            net_cash_flow: net_cash,
            reason: reason.to_string(),
        });
        report.position_events.push(PositionEvent {
            date,
            symbol: symbol.to_string(),
            delta_quantity: -(filled_qty as i32),
            quantity_after: portfolio
                .position(symbol)
                .map(|pos| pos.quantity)
                .unwrap_or(0),
            average_cost: portfolio
                .position(symbol)
                .map(|pos| pos.average_cost)
                .unwrap_or(0.0),
            realized_pnl_delta: realized_pnl,
            reason: reason.to_string(),
        });
        report.account_events.push(AccountEvent {
            date,
            cash_before,
            cash_after: portfolio.cash(),
            total_equity: self.total_equity_at(date, portfolio, data, PriceField::Open)?,
            note: format!("sell {symbol} {reason}"),
        });
        Ok(())
    }

    fn process_target_value(
        &self,
        date: NaiveDate,
        portfolio: &mut PortfolioState,
        data: &DataSet,
        symbol: &str,
        target_value: f64,
        reason: &str,
        intraday_turnover: &mut BTreeMap<String, u32>,
        execution_cursors: &mut BTreeMap<String, NaiveDateTime>,
        global_execution_cursor: &mut Option<NaiveDateTime>,
        report: &mut BrokerExecutionReport,
    ) -> Result<(), BacktestError> {
        let price = data
            .market(date, symbol)
            .map(|snapshot| self.sizing_price(snapshot))
            .ok_or_else(|| BacktestError::MissingPrice {
                date,
                symbol: symbol.to_string(),
                field: price_field_name(self.execution_price_field),
            })?;
        let current_qty = portfolio
            .position(symbol)
            .map(|pos| pos.quantity)
            .unwrap_or(0);
        let current_value = price * current_qty as f64;
        let target_qty = self.round_buy_quantity(
            ((target_value.max(0.0)) / price).floor() as u32,
            self.round_lot(data, symbol),
        );

        if current_qty > target_qty {
            self.process_sell(
                date,
                portfolio,
                data,
                symbol,
                current_qty - target_qty,
                reason,
                intraday_turnover,
                execution_cursors,
                global_execution_cursor,
                report,
            )?;
        } else if target_qty > current_qty {
            self.process_buy(
                date,
                portfolio,
                data,
                symbol,
                target_qty - current_qty,
                reason,
                intraday_turnover,
                execution_cursors,
                global_execution_cursor,
                None,
                report,
            )?;
        } else if (current_value - target_value).abs() <= f64::EPSILON {
            report.order_events.push(OrderEvent {
                date,
                symbol: symbol.to_string(),
                side: if current_qty > 0 {
                    OrderSide::Sell
                } else {
                    OrderSide::Buy
                },
                requested_quantity: 0,
                filled_quantity: 0,
                status: OrderStatus::Filled,
                reason: format!("{reason}: already at target value"),
            });
        }

        Ok(())
    }

    fn process_value(
        &self,
        date: NaiveDate,
        portfolio: &mut PortfolioState,
        data: &DataSet,
        symbol: &str,
        value: f64,
        reason: &str,
        intraday_turnover: &mut BTreeMap<String, u32>,
        execution_cursors: &mut BTreeMap<String, NaiveDateTime>,
        global_execution_cursor: &mut Option<NaiveDateTime>,
        report: &mut BrokerExecutionReport,
    ) -> Result<(), BacktestError> {
        if value.abs() <= f64::EPSILON {
            return Ok(());
        }
        let snapshot = data
            .market(date, symbol)
            .ok_or_else(|| BacktestError::MissingPrice {
                date,
                symbol: symbol.to_string(),
                field: price_field_name(self.execution_price_field),
            })?;
        if value > 0.0 {
            let round_lot = self.round_lot(data, symbol);
            let price = self.sizing_price(snapshot);
            let snapshot_requested_qty =
                self.round_buy_quantity(((value.abs()) / price).floor() as u32, round_lot);
            let requested_qty = self.maybe_expand_periodic_value_buy_quantity(
                date,
                portfolio,
                data,
                symbol,
                snapshot_requested_qty,
                round_lot,
                value.abs(),
                reason,
                execution_cursors,
                *global_execution_cursor,
            );
            self.process_buy(
                date,
                portfolio,
                data,
                symbol,
                requested_qty,
                reason,
                intraday_turnover,
                execution_cursors,
                global_execution_cursor,
                Some(value.abs()),
                report,
            )
        } else {
            let price = self.sizing_price(snapshot);
            let requested_qty = self.round_buy_quantity(
                ((value.abs()) / price).floor() as u32,
                self.round_lot(data, symbol),
            );
            self.process_sell(
                date,
                portfolio,
                data,
                symbol,
                requested_qty,
                reason,
                intraday_turnover,
                execution_cursors,
                global_execution_cursor,
                report,
            )
        }
    }

    fn estimate_value_buy_quantity(
        &self,
        date: NaiveDate,
        portfolio: &PortfolioState,
        data: &DataSet,
        symbol: &str,
        round_lot: u32,
        value_budget: f64,
        intraday_turnover: &BTreeMap<String, u32>,
        execution_cursors: &BTreeMap<String, NaiveDateTime>,
        global_execution_cursor: Option<NaiveDateTime>,
    ) -> Option<u32> {
        if self.execution_price_field != PriceField::Last {
            return None;
        }
        let snapshot = data.market(date, symbol)?;
        let market_limited_qty = self
            .market_fillable_quantity(
                snapshot,
                OrderSide::Buy,
                u32::MAX,
                round_lot,
                *intraday_turnover.get(symbol).unwrap_or(&0),
            )
            .ok()?;
        let max_requested_qty = market_limited_qty;
        let start_cursor = self
            .intraday_execution_start_time
            .map(|start_time| date.and_time(start_time));
        let quotes = data.execution_quotes_on(date, symbol);
        if let Some(estimated) = self.select_buy_sizing_fill(
            snapshot,
            quotes,
            start_cursor,
            max_requested_qty,
            round_lot,
            Some(portfolio.cash()),
            Some(value_budget),
        ) {
            return Some(estimated.quantity);
        }

        let execution_price = self.snapshot_execution_price(snapshot, OrderSide::Buy);
        let fallback_qty = self.affordable_buy_quantity(
            portfolio.cash(),
            Some(value_budget),
            execution_price,
            max_requested_qty,
            round_lot,
        );
        if fallback_qty > 0 {
            Some(fallback_qty)
        } else {
            None
        }
    }

    fn maybe_expand_periodic_value_buy_quantity(
        &self,
        _date: NaiveDate,
        _portfolio: &PortfolioState,
        _data: &DataSet,
        _symbol: &str,
        requested_qty: u32,
        _round_lot: u32,
        _value_budget: f64,
        _reason: &str,
        _execution_cursors: &BTreeMap<String, NaiveDateTime>,
        _global_execution_cursor: Option<NaiveDateTime>,
    ) -> u32 {
        requested_qty
    }

    fn select_buy_sizing_fill(
        &self,
        snapshot: &crate::data::DailyMarketSnapshot,
        quotes: &[IntradayExecutionQuote],
        start_cursor: Option<NaiveDateTime>,
        requested_qty: u32,
        round_lot: u32,
        cash_limit: Option<f64>,
        gross_limit: Option<f64>,
    ) -> Option<ExecutionFill> {
        if requested_qty == 0 {
            return None;
        }

        let lot = round_lot.max(1);
        let mut filled_qty = 0_u32;
        let mut gross_amount = 0.0_f64;
        let mut last_timestamp = None;

        for quote in quotes {
            if start_cursor.is_some_and(|cursor| quote.timestamp < cursor) {
                continue;
            }

            let fallback_quote_price = if quote.last_price.is_finite() && quote.last_price > 0.0 {
                Some(quote.last_price)
            } else {
                quote.buy_price()
            };

            if quote.volume_delta == 0 {
                continue;
            }
            let Some(raw_quote_price) = fallback_quote_price else {
                continue;
            };
            let quote_price = self.quote_execution_price(snapshot, OrderSide::Buy, raw_quote_price);
            if !quote_price.is_finite() || quote_price <= 0.0 {
                continue;
            }
            let available_qty = quote
                .ask1_volume
                .saturating_mul(lot as u64)
                .min(u32::MAX as u64) as u32;
            if available_qty == 0 {
                continue;
            }

            let remaining_qty = requested_qty.saturating_sub(filled_qty);
            if remaining_qty == 0 {
                break;
            }
            let mut take_qty = remaining_qty.min(available_qty);
            take_qty = self.round_buy_quantity(take_qty, lot);
            if take_qty == 0 {
                continue;
            }

            if let Some(cash) = cash_limit {
                while take_qty > 0 {
                    let candidate_gross = gross_amount + quote_price * take_qty as f64;
                    if gross_limit.is_some_and(|limit| candidate_gross > limit + 1e-6) {
                        take_qty = take_qty.saturating_sub(lot);
                        continue;
                    }
                    let candidate_cost = self.cost_model.calculate(OrderSide::Buy, candidate_gross);
                    if candidate_gross + candidate_cost.total() <= cash + 1e-6 {
                        break;
                    }
                    take_qty = take_qty.saturating_sub(lot);
                }
                if take_qty == 0 {
                    break;
                }
            }

            gross_amount += quote_price * take_qty as f64;
            filled_qty += take_qty;
            last_timestamp = Some(quote.timestamp);

            if filled_qty >= requested_qty {
                break;
            }
        }

        if filled_qty == 0 {
            return None;
        }

        Some(ExecutionFill {
            price: gross_amount / filled_qty as f64,
            quantity: filled_qty,
            next_cursor: last_timestamp.unwrap() + Duration::seconds(1),
        })
    }

    fn process_buy(
        &self,
        date: NaiveDate,
        portfolio: &mut PortfolioState,
        data: &DataSet,
        symbol: &str,
        requested_qty: u32,
        reason: &str,
        intraday_turnover: &mut BTreeMap<String, u32>,
        execution_cursors: &mut BTreeMap<String, NaiveDateTime>,
        global_execution_cursor: &mut Option<NaiveDateTime>,
        value_budget: Option<f64>,
        report: &mut BrokerExecutionReport,
    ) -> Result<(), BacktestError> {
        let snapshot = data.require_market(date, symbol)?;
        let candidate = data.require_candidate(date, symbol)?;

        let rule = self
            .rules
            .can_buy(date, snapshot, candidate, self.execution_price_field);
        if !rule.allowed {
            report.order_events.push(OrderEvent {
                date,
                symbol: symbol.to_string(),
                side: OrderSide::Buy,
                requested_quantity: requested_qty,
                filled_quantity: 0,
                status: OrderStatus::Rejected,
                reason: format!("{reason}: {}", rule.reason.unwrap_or_default()),
            });
            return Ok(());
        }

        let market_limited_qty = self.market_fillable_quantity(
            snapshot,
            OrderSide::Buy,
            requested_qty,
            self.round_lot(data, symbol),
            *intraday_turnover.get(symbol).unwrap_or(&0),
        );
        let constrained_qty = match market_limited_qty {
            Ok(quantity) => quantity,
            Err(limit_reason) => {
                report.order_events.push(OrderEvent {
                    date,
                    symbol: symbol.to_string(),
                    side: OrderSide::Buy,
                    requested_quantity: requested_qty,
                    filled_quantity: 0,
                    status: OrderStatus::Rejected,
                    reason: format!("{reason}: {limit_reason}"),
                });
                return Ok(());
            }
        };

        let fill = self.resolve_execution_fill(
            date,
            symbol,
            OrderSide::Buy,
            snapshot,
            data,
            constrained_qty,
            self.round_lot(data, symbol),
            execution_cursors,
            None,
            Some(portfolio.cash()),
            value_budget.map(|budget| budget + 400.0),
        );
        let (filled_qty, execution_price) = if let Some(fill) = fill {
            execution_cursors.insert(symbol.to_string(), fill.next_cursor);
            if self.uses_serial_execution_cursor(reason) {
                *global_execution_cursor = Some(fill.next_cursor);
            }
            (fill.quantity, fill.price)
        } else {
            let execution_price = self.snapshot_execution_price(snapshot, OrderSide::Buy);
            let filled_qty = self.affordable_buy_quantity(
                portfolio.cash(),
                value_budget.map(|budget| budget + 400.0),
                execution_price,
                constrained_qty,
                self.round_lot(data, symbol),
            );
            (filled_qty, execution_price)
        };
        if filled_qty == 0 {
            report.order_events.push(OrderEvent {
                date,
                symbol: symbol.to_string(),
                side: OrderSide::Buy,
                requested_quantity: requested_qty,
                filled_quantity: 0,
                status: OrderStatus::Rejected,
                reason: format!("{reason}: insufficient cash after fees"),
            });
            return Ok(());
        }

        let cash_before = portfolio.cash();
        let gross_amount = execution_price * filled_qty as f64;
        let cost = self.cost_model.calculate(OrderSide::Buy, gross_amount);
        let cash_out = gross_amount + cost.total();

        portfolio.apply_cash_delta(-cash_out);
        portfolio
            .position_mut(symbol)
            .buy(date, filled_qty, execution_price);
        *intraday_turnover.entry(symbol.to_string()).or_default() += filled_qty;

        let status = if filled_qty < requested_qty {
            OrderStatus::PartiallyFilled
        } else {
            OrderStatus::Filled
        };

        report.order_events.push(OrderEvent {
            date,
            symbol: symbol.to_string(),
            side: OrderSide::Buy,
            requested_quantity: requested_qty,
            filled_quantity: filled_qty,
            status,
            reason: reason.to_string(),
        });
        report.fill_events.push(FillEvent {
            date,
            symbol: symbol.to_string(),
            side: OrderSide::Buy,
            quantity: filled_qty,
            price: execution_price,
            gross_amount,
            commission: cost.commission,
            stamp_tax: cost.stamp_tax,
            net_cash_flow: -cash_out,
            reason: reason.to_string(),
        });
        report.position_events.push(PositionEvent {
            date,
            symbol: symbol.to_string(),
            delta_quantity: filled_qty as i32,
            quantity_after: portfolio
                .position(symbol)
                .map(|pos| pos.quantity)
                .unwrap_or(0),
            average_cost: portfolio
                .position(symbol)
                .map(|pos| pos.average_cost)
                .unwrap_or(0.0),
            realized_pnl_delta: 0.0,
            reason: reason.to_string(),
        });
        report.account_events.push(AccountEvent {
            date,
            cash_before,
            cash_after: portfolio.cash(),
            total_equity: self.total_equity_at(date, portfolio, data, PriceField::Open)?,
            note: format!("buy {symbol} {reason}"),
        });
        Ok(())
    }

    fn total_equity_at(
        &self,
        date: NaiveDate,
        portfolio: &PortfolioState,
        data: &DataSet,
        field: PriceField,
    ) -> Result<f64, BacktestError> {
        let mut market_value = 0.0;
        for position in portfolio.positions().values() {
            let price = data.price(date, &position.symbol, field).ok_or_else(|| {
                BacktestError::MissingPrice {
                    date,
                    symbol: position.symbol.clone(),
                    field: match field {
                        PriceField::Open => "open",
                        PriceField::Close => "close",
                        PriceField::Last => "last",
                    },
                }
            })?;
            market_value += price * position.quantity as f64;
        }

        Ok(portfolio.cash() + market_value)
    }

    fn round_lot(&self, data: &DataSet, symbol: &str) -> u32 {
        data.instruments()
            .get(symbol)
            .map(|instrument| instrument.effective_round_lot())
            .unwrap_or(self.board_lot_size.max(1))
    }

    fn round_buy_quantity(&self, quantity: u32, round_lot: u32) -> u32 {
        let lot = round_lot.max(1);
        (quantity / lot) * lot
    }

    fn affordable_buy_quantity(
        &self,
        cash: f64,
        gross_limit: Option<f64>,
        price: f64,
        requested_qty: u32,
        round_lot: u32,
    ) -> u32 {
        let lot = round_lot.max(1);
        let mut quantity = self.round_buy_quantity(requested_qty, lot);
        while quantity > 0 {
            let gross = price * quantity as f64;
            if gross_limit.is_some_and(|limit| gross > limit + 1e-6) {
                quantity = quantity.saturating_sub(lot);
                continue;
            }
            let cost = self.cost_model.calculate(OrderSide::Buy, gross);
            if gross + cost.total() <= cash + 1e-6 {
                return quantity;
            }
            quantity = quantity.saturating_sub(lot);
        }
        0
    }

    fn market_fillable_quantity(
        &self,
        snapshot: &crate::data::DailyMarketSnapshot,
        side: OrderSide,
        requested_qty: u32,
        round_lot: u32,
        consumed_turnover: u32,
    ) -> Result<u32, String> {
        if requested_qty == 0 {
            return Ok(0);
        }

        if self.inactive_limit && snapshot.tick_volume == 0 {
            return Err("tick no volume".to_string());
        }

        let mut max_fill = requested_qty;
        let lot = round_lot.max(1);

        if self.liquidity_limit {
            let top_level_liquidity = match side {
                OrderSide::Buy => snapshot.liquidity_for_buy(),
                OrderSide::Sell => snapshot.liquidity_for_sell(),
            }
            .min(u32::MAX as u64) as u32;
            if top_level_liquidity == 0 {
                return Err("no quote liquidity".to_string());
            }
            max_fill = max_fill.min(self.round_buy_quantity(top_level_liquidity, lot));
        }

        if self.volume_limit {
            let raw_limit = ((snapshot.tick_volume as f64) * self.volume_percent).round() as i64
                - consumed_turnover as i64;
            if raw_limit <= 0 {
                return Err("tick volume limit".to_string());
            }
            let volume_limited = self.round_buy_quantity(raw_limit as u32, lot);
            if volume_limited == 0 {
                return Err("tick volume limit".to_string());
            }
            max_fill = max_fill.min(volume_limited);
        }

        Ok(max_fill)
    }

    fn resolve_execution_fill(
        &self,
        date: NaiveDate,
        symbol: &str,
        side: OrderSide,
        snapshot: &crate::data::DailyMarketSnapshot,
        data: &DataSet,
        requested_qty: u32,
        round_lot: u32,
        execution_cursors: &mut BTreeMap<String, NaiveDateTime>,
        global_execution_cursor: Option<NaiveDateTime>,
        cash_limit: Option<f64>,
        gross_limit: Option<f64>,
    ) -> Option<ExecutionFill> {
        if self.execution_price_field != PriceField::Last {
            return None;
        }

        if self.intraday_execution_start_time.is_some() {
            let execution_price = self.snapshot_execution_price(snapshot, side);
            let quantity = match side {
                OrderSide::Buy => self.affordable_buy_quantity(
                    cash_limit.unwrap_or(f64::INFINITY),
                    gross_limit,
                    execution_price,
                    requested_qty,
                    round_lot,
                ),
                OrderSide::Sell => requested_qty,
            };
            if quantity == 0 {
                return None;
            }
            let next_cursor = self
                .intraday_execution_start_time
                .map(|start_time| date.and_time(start_time) + Duration::seconds(1))
                .unwrap_or_else(|| date.and_hms_opt(0, 0, 1).expect("valid midnight"));
            return Some(ExecutionFill {
                price: execution_price,
                quantity,
                next_cursor,
            });
        }

        let start_cursor = self
            .intraday_execution_start_time
            .map(|start_time| date.and_time(start_time));
        let quotes = data.execution_quotes_on(date, symbol);

        if let Some(fill) = self.select_execution_fill(
            snapshot,
            quotes,
            side,
            start_cursor,
            requested_qty,
            round_lot,
            cash_limit,
            gross_limit,
        ) {
            return Some(fill);
        }

        None
    }

    fn select_execution_fill(
        &self,
        snapshot: &crate::data::DailyMarketSnapshot,
        quotes: &[IntradayExecutionQuote],
        side: OrderSide,
        start_cursor: Option<NaiveDateTime>,
        requested_qty: u32,
        round_lot: u32,
        cash_limit: Option<f64>,
        gross_limit: Option<f64>,
    ) -> Option<ExecutionFill> {
        if requested_qty == 0 {
            return None;
        }

        let lot = round_lot.max(1);
        let mut filled_qty = 0_u32;
        let mut gross_amount = 0.0_f64;
        let mut last_timestamp = None;

        for quote in quotes {
            if start_cursor.is_some_and(|cursor| quote.timestamp < cursor) {
                continue;
            }

            // Approximate JoinQuant market-order fills with the evolving L1 book after
            // the decision time instead of trade VWAP. This keeps quantities/prices
            // closer to the observed 10:18 execution logs.
            if quote.volume_delta == 0 {
                continue;
            }
            let Some(quote_price) = self.select_quote_reference_price(snapshot, quote, side) else {
                continue;
            };
            let top_level_liquidity = match side {
                OrderSide::Buy => quote.ask1_volume,
                OrderSide::Sell => quote.bid1_volume,
            };
            let available_qty = top_level_liquidity
                .saturating_mul(lot as u64)
                .min(u32::MAX as u64) as u32;
            if available_qty == 0 {
                continue;
            }

            let remaining_qty = requested_qty.saturating_sub(filled_qty);
            if remaining_qty == 0 {
                break;
            }
            let mut take_qty = remaining_qty.min(available_qty);
            take_qty = self.round_buy_quantity(take_qty, lot);
            if take_qty == 0 {
                continue;
            }

            if let Some(cash) = cash_limit {
                while take_qty > 0 {
                    let candidate_gross = gross_amount + quote_price * take_qty as f64;
                    if gross_limit.is_some_and(|limit| candidate_gross > limit + 1e-6) {
                        take_qty = take_qty.saturating_sub(lot);
                        continue;
                    }
                    if candidate_gross <= cash + 1e-6 {
                        break;
                    }
                    take_qty = take_qty.saturating_sub(lot);
                }
                if take_qty == 0 {
                    break;
                }
            }

            gross_amount += quote_price * take_qty as f64;
            filled_qty += take_qty;
            last_timestamp = Some(quote.timestamp);

            if filled_qty >= requested_qty {
                break;
            }
        }

        if filled_qty == 0 {
            return None;
        }

        Some(ExecutionFill {
            price: gross_amount / filled_qty as f64,
            quantity: filled_qty,
            next_cursor: last_timestamp.unwrap() + Duration::seconds(1),
        })
    }

    fn uses_serial_execution_cursor(&self, reason: &str) -> bool {
        let _ = reason;
        false
    }
}

fn price_field_name(field: PriceField) -> &'static str {
    match field {
        PriceField::Open => "open",
        PriceField::Close => "close",
        PriceField::Last => "last",
    }
}

fn sell_reason(decision: &StrategyDecision, symbol: &str) -> &'static str {
    if decision.exit_symbols.contains(symbol) {
        "exit_hook_sell"
    } else {
        "rebalance_sell"
    }
}
