use chrono::NaiveDate;
use serde::Serialize;
use thiserror::Error;

use crate::broker::{BrokerExecutionReport, BrokerSimulator};
use crate::cost::CostModel;
use crate::data::{BenchmarkSnapshot, DataSet, DataSetError, PriceField};
use crate::event_bus::ProcessEventBus;
use crate::events::{
    AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
    ProcessEventKind,
};
use crate::metrics::{BacktestMetrics, compute_backtest_metrics};
use crate::portfolio::{CashReceivable, HoldingSummary, PortfolioState};
use crate::rules::EquityRuleHooks;
use crate::scheduler::{ScheduleRule, ScheduleStage, Scheduler};
use crate::strategy::{Strategy, StrategyContext};

#[derive(Debug, Error)]
pub enum BacktestError {
    #[error(transparent)]
    Data(#[from] DataSetError),
    #[error("missing {field} price for {symbol} on {date}")]
    MissingPrice {
        date: NaiveDate,
        symbol: String,
        field: &'static str,
    },
    #[error("benchmark snapshot missing for {date}")]
    MissingBenchmark { date: NaiveDate },
    #[error("{0}")]
    Execution(String),
}

#[derive(Debug, Clone)]
pub struct BacktestConfig {
    pub initial_cash: f64,
    pub benchmark_code: String,
    pub start_date: Option<NaiveDate>,
    pub end_date: Option<NaiveDate>,
    pub decision_lag_trading_days: usize,
    pub execution_price_field: PriceField,
}

#[derive(Debug, Clone, Serialize)]
pub struct DailyEquityPoint {
    #[serde(with = "date_format")]
    pub date: NaiveDate,
    pub cash: f64,
    pub market_value: f64,
    pub total_equity: f64,
    pub benchmark_close: f64,
    pub notes: String,
    pub diagnostics: String,
}

#[derive(Debug, Clone)]
pub struct BacktestResult {
    pub strategy_name: String,
    pub equity_curve: Vec<DailyEquityPoint>,
    pub benchmark_series: Vec<BenchmarkSnapshot>,
    pub order_events: Vec<OrderEvent>,
    pub fills: Vec<FillEvent>,
    pub position_events: Vec<PositionEvent>,
    pub account_events: Vec<AccountEvent>,
    pub process_events: Vec<ProcessEvent>,
    pub holdings_summary: Vec<HoldingSummary>,
    pub daily_holdings: Vec<HoldingSummary>,
    pub metrics: BacktestMetrics,
}

#[derive(Debug, Clone, Serialize)]
pub struct BacktestDayProgress {
    #[serde(with = "date_format")]
    pub date: NaiveDate,
    pub cash: f64,
    pub market_value: f64,
    pub total_equity: f64,
    pub unit_nav: f64,
    pub total_return: f64,
    pub benchmark_close: f64,
    pub daily_fill_count: usize,
    pub cumulative_trade_count: usize,
    pub holding_count: usize,
    pub notes: String,
    pub diagnostics: String,
    pub orders: Vec<OrderEvent>,
    pub fills: Vec<FillEvent>,
    pub holdings: Vec<HoldingSummary>,
    pub process_events: Vec<ProcessEvent>,
}

pub struct BacktestEngine<S, C, R> {
    data: DataSet,
    strategy: S,
    broker: BrokerSimulator<C, R>,
    config: BacktestConfig,
    dividend_reinvestment: bool,
    process_event_bus: ProcessEventBus,
}

impl<S, C, R> BacktestEngine<S, C, R> {
    pub fn new(
        data: DataSet,
        strategy: S,
        broker: BrokerSimulator<C, R>,
        config: BacktestConfig,
    ) -> Self {
        Self {
            data,
            strategy,
            broker,
            config,
            dividend_reinvestment: false,
            process_event_bus: ProcessEventBus::new(),
        }
    }

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

    pub fn process_event_bus_mut(&mut self) -> &mut ProcessEventBus {
        &mut self.process_event_bus
    }

    pub fn add_process_listener<F>(&mut self, kind: ProcessEventKind, listener: F)
    where
        F: FnMut(&ProcessEvent) + 'static,
    {
        self.process_event_bus.add_listener(kind, listener);
    }

    pub fn add_any_process_listener<F>(&mut self, listener: F)
    where
        F: FnMut(&ProcessEvent) + 'static,
    {
        self.process_event_bus.add_any_listener(listener);
    }
}

impl<S, C, R> BacktestEngine<S, C, R>
where
    S: Strategy,
    C: CostModel,
    R: EquityRuleHooks,
{
    pub fn run(&mut self) -> Result<BacktestResult, BacktestError> {
        self.run_with_progress(|_| {})
    }

    pub fn run_with_progress<F>(
        &mut self,
        mut on_progress: F,
    ) -> Result<BacktestResult, BacktestError>
    where
        F: FnMut(&BacktestDayProgress),
    {
        let mut portfolio = PortfolioState::new(self.config.initial_cash);
        let scheduler_calendar = self.data.calendar().clone();
        let scheduler = Scheduler::new(&scheduler_calendar);
        let execution_dates = self
            .data
            .calendar()
            .iter()
            .filter(|date| {
                self.config
                    .start_date
                    .map(|start| *date >= start)
                    .unwrap_or(true)
            })
            .filter(|date| self.config.end_date.map(|end| *date <= end).unwrap_or(true))
            .filter(|date| {
                !self.data.factor_snapshots_on(*date).is_empty()
                    && !self.data.candidate_snapshots_on(*date).is_empty()
            })
            .collect::<Vec<_>>();
        let mut result = BacktestResult {
            strategy_name: self.strategy.name().to_string(),
            benchmark_series: self
                .data
                .benchmark_series()
                .into_iter()
                .filter(|row| {
                    self.config
                        .start_date
                        .map(|start| row.date >= start)
                        .unwrap_or(true)
                })
                .filter(|row| {
                    self.config
                        .end_date
                        .map(|end| row.date <= end)
                        .unwrap_or(true)
                })
                .collect(),
            order_events: Vec::new(),
            fills: Vec::new(),
            position_events: Vec::new(),
            account_events: Vec::new(),
            process_events: Vec::new(),
            equity_curve: Vec::new(),
            holdings_summary: Vec::new(),
            daily_holdings: Vec::new(),
            metrics: BacktestMetrics::default(),
        };

        for (execution_idx, execution_date) in execution_dates.iter().copied().enumerate() {
            let mut corporate_action_notes = Vec::new();
            let receivable_report = self.settle_cash_receivables(
                execution_date,
                &mut portfolio,
                &mut corporate_action_notes,
            )?;
            self.extend_result(&mut result, receivable_report);
            let corporate_action_report = self.apply_corporate_actions(
                execution_date,
                &mut portfolio,
                &mut corporate_action_notes,
            )?;
            self.extend_result(&mut result, corporate_action_report);
            let delisting_report = self.settle_delisted_positions(
                execution_date,
                &mut portfolio,
                &mut corporate_action_notes,
            )?;
            self.extend_result(&mut result, delisting_report);

            let decision_slot = execution_idx
                .checked_sub(self.config.decision_lag_trading_days)
                .map(|decision_idx| (decision_idx, execution_dates[decision_idx]));
            let (decision_index, decision_date) =
                decision_slot.unwrap_or((execution_idx, execution_date));
            let daily_context = StrategyContext {
                execution_date,
                decision_date,
                decision_index,
                data: &self.data,
                portfolio: &portfolio,
            };
            let schedule_rules = self.strategy.schedule_rules();
            let mut process_events = Vec::new();
            publish_phase_event(
                &mut self.strategy,
                &mut self.process_event_bus,
                &daily_context,
                &mut process_events,
                execution_date,
                ProcessEventKind::PreBeforeTrading,
                "before_trading:pre",
            )?;
            self.strategy.before_trading(&daily_context)?;
            publish_phase_event(
                &mut self.strategy,
                &mut self.process_event_bus,
                &daily_context,
                &mut process_events,
                execution_date,
                ProcessEventKind::BeforeTrading,
                "before_trading",
            )?;
            publish_phase_event(
                &mut self.strategy,
                &mut self.process_event_bus,
                &daily_context,
                &mut process_events,
                execution_date,
                ProcessEventKind::PostBeforeTrading,
                "before_trading:post",
            )?;
            publish_phase_event(
                &mut self.strategy,
                &mut self.process_event_bus,
                &daily_context,
                &mut process_events,
                execution_date,
                ProcessEventKind::PreOpenAuction,
                "open_auction:pre",
            )?;
            let mut auction_decision = collect_scheduled_decisions(
                &mut self.strategy,
                &scheduler,
                execution_date,
                ScheduleStage::OpenAuction,
                &schedule_rules,
                &daily_context,
                &mut process_events,
                &mut self.process_event_bus,
            )?;
            auction_decision.merge_from(self.strategy.open_auction(&daily_context)?);
            publish_phase_event(
                &mut self.strategy,
                &mut self.process_event_bus,
                &daily_context,
                &mut process_events,
                execution_date,
                ProcessEventKind::OpenAuction,
                "open_auction",
            )?;
            let mut report = self.broker.execute(
                execution_date,
                &mut portfolio,
                &self.data,
                &auction_decision,
            )?;
            let post_auction_context = StrategyContext {
                execution_date,
                decision_date,
                decision_index,
                data: &self.data,
                portfolio: &portfolio,
            };
            publish_process_events(
                &mut self.strategy,
                &mut self.process_event_bus,
                &post_auction_context,
                &mut process_events,
                &mut report.process_events,
            )?;
            publish_phase_event(
                &mut self.strategy,
                &mut self.process_event_bus,
                &post_auction_context,
                &mut process_events,
                execution_date,
                ProcessEventKind::PostOpenAuction,
                "open_auction:post",
            )?;

            publish_phase_event(
                &mut self.strategy,
                &mut self.process_event_bus,
                &post_auction_context,
                &mut process_events,
                execution_date,
                ProcessEventKind::PreOnDay,
                "on_day:pre",
            )?;
            let mut decision = decision_slot
                .map(|(decision_idx, decision_date)| {
                    self.strategy.on_day(&StrategyContext {
                        execution_date,
                        decision_date,
                        decision_index: decision_idx,
                        data: &self.data,
                        portfolio: &portfolio,
                    })
                })
                .transpose()?
                .unwrap_or_default();
            decision.merge_from(collect_scheduled_decisions(
                &mut self.strategy,
                &scheduler,
                execution_date,
                ScheduleStage::OnDay,
                &schedule_rules,
                &StrategyContext {
                    execution_date,
                    decision_date,
                    decision_index,
                    data: &self.data,
                    portfolio: &portfolio,
                },
                &mut process_events,
                &mut self.process_event_bus,
            )?);
            let on_day_context = StrategyContext {
                execution_date,
                decision_date,
                decision_index,
                data: &self.data,
                portfolio: &portfolio,
            };
            publish_phase_event(
                &mut self.strategy,
                &mut self.process_event_bus,
                &on_day_context,
                &mut process_events,
                execution_date,
                ProcessEventKind::OnDay,
                "on_day",
            )?;

            let mut intraday_report =
                self.broker
                    .execute(execution_date, &mut portfolio, &self.data, &decision)?;
            let post_intraday_context = StrategyContext {
                execution_date,
                decision_date,
                decision_index,
                data: &self.data,
                portfolio: &portfolio,
            };
            publish_process_events(
                &mut self.strategy,
                &mut self.process_event_bus,
                &post_intraday_context,
                &mut process_events,
                &mut intraday_report.process_events,
            )?;
            report.order_events.extend(intraday_report.order_events);
            report.fill_events.extend(intraday_report.fill_events);
            report
                .position_events
                .extend(intraday_report.position_events);
            report.account_events.extend(intraday_report.account_events);
            report.diagnostics.extend(intraday_report.diagnostics);
            publish_phase_event(
                &mut self.strategy,
                &mut self.process_event_bus,
                &post_intraday_context,
                &mut process_events,
                execution_date,
                ProcessEventKind::PostOnDay,
                "on_day:post",
            )?;

            portfolio.update_prices(execution_date, &self.data, PriceField::Close)?;

            let post_trade_context = StrategyContext {
                execution_date,
                decision_date,
                decision_index,
                data: &self.data,
                portfolio: &portfolio,
            };
            publish_phase_event(
                &mut self.strategy,
                &mut self.process_event_bus,
                &post_trade_context,
                &mut process_events,
                execution_date,
                ProcessEventKind::PreAfterTrading,
                "after_trading:pre",
            )?;
            self.strategy.after_trading(&post_trade_context)?;
            publish_phase_event(
                &mut self.strategy,
                &mut self.process_event_bus,
                &post_trade_context,
                &mut process_events,
                execution_date,
                ProcessEventKind::AfterTrading,
                "after_trading",
            )?;
            let mut close_report = self.broker.after_trading(execution_date);
            publish_process_events(
                &mut self.strategy,
                &mut self.process_event_bus,
                &post_trade_context,
                &mut process_events,
                &mut close_report.process_events,
            )?;
            report.order_events.extend(close_report.order_events);
            report.fill_events.extend(close_report.fill_events);
            report.position_events.extend(close_report.position_events);
            report.account_events.extend(close_report.account_events);
            report.diagnostics.extend(close_report.diagnostics);
            publish_phase_event(
                &mut self.strategy,
                &mut self.process_event_bus,
                &post_trade_context,
                &mut process_events,
                execution_date,
                ProcessEventKind::PostAfterTrading,
                "after_trading:post",
            )?;
            publish_phase_event(
                &mut self.strategy,
                &mut self.process_event_bus,
                &post_trade_context,
                &mut process_events,
                execution_date,
                ProcessEventKind::PreSettlement,
                "settlement:pre",
            )?;
            self.strategy.on_settlement(&post_trade_context)?;
            publish_phase_event(
                &mut self.strategy,
                &mut self.process_event_bus,
                &post_trade_context,
                &mut process_events,
                execution_date,
                ProcessEventKind::Settlement,
                "settlement",
            )?;
            publish_phase_event(
                &mut self.strategy,
                &mut self.process_event_bus,
                &post_trade_context,
                &mut process_events,
                execution_date,
                ProcessEventKind::PostSettlement,
                "settlement:post",
            )?;
            let daily_fill_count = report.fill_events.len();
            let day_orders = report.order_events.clone();
            let day_fills = report.fill_events.clone();
            let broker_diagnostics = report.diagnostics.clone();
            self.extend_result(&mut result, report);

            let benchmark =
                self.data
                    .benchmark(execution_date)
                    .ok_or(BacktestError::MissingBenchmark {
                        date: execution_date,
                    })?;
            let notes = corporate_action_notes
                .into_iter()
                .chain(decision.notes.into_iter())
                .collect::<Vec<_>>()
                .join(" | ");
            let diagnostics = decision
                .diagnostics
                .into_iter()
                .chain(broker_diagnostics.into_iter())
                .collect::<Vec<_>>()
                .join(" | ");
            let holdings_for_day = portfolio.holdings_summary(execution_date);
            let day_process_events = process_events.clone();

            result.equity_curve.push(DailyEquityPoint {
                date: execution_date,
                cash: portfolio.cash(),
                market_value: portfolio.market_value(),
                total_equity: portfolio.total_equity(),
                benchmark_close: benchmark.close,
                notes,
                diagnostics,
            });
            result.daily_holdings.extend(holdings_for_day.clone());
            let latest = result
                .equity_curve
                .last()
                .expect("equity point pushed for progress event");
            on_progress(&BacktestDayProgress {
                date: execution_date,
                cash: latest.cash,
                market_value: latest.market_value,
                total_equity: latest.total_equity,
                unit_nav: if self.config.initial_cash.abs() < f64::EPSILON {
                    0.0
                } else {
                    latest.total_equity / self.config.initial_cash
                },
                total_return: if self.config.initial_cash.abs() < f64::EPSILON {
                    0.0
                } else {
                    (latest.total_equity / self.config.initial_cash) - 1.0
                },
                benchmark_close: latest.benchmark_close,
                daily_fill_count,
                cumulative_trade_count: result.fills.len(),
                holding_count: holdings_for_day.len(),
                notes: latest.notes.clone(),
                diagnostics: latest.diagnostics.clone(),
                orders: day_orders,
                fills: day_fills,
                holdings: holdings_for_day,
                process_events: day_process_events,
            });
            result.process_events.extend(process_events);
        }

        if let Some(last_date) = execution_dates.last().copied() {
            result.holdings_summary = portfolio.holdings_summary(last_date);
        }
        result.metrics = compute_backtest_metrics(
            &result.equity_curve,
            &result.fills,
            &result.daily_holdings,
            self.config.initial_cash,
        );

        Ok(result)
    }

    fn extend_result(
        &self,
        result: &mut BacktestResult,
        report: BrokerExecutionReport,
    ) -> BrokerExecutionReport {
        result.order_events.extend(report.order_events.clone());
        result.fills.extend(report.fill_events.clone());
        result
            .position_events
            .extend(report.position_events.clone());
        result.account_events.extend(report.account_events.clone());
        report
    }

    fn apply_corporate_actions(
        &self,
        date: NaiveDate,
        portfolio: &mut PortfolioState,
        notes: &mut Vec<String>,
    ) -> Result<BrokerExecutionReport, BacktestError> {
        let mut report = BrokerExecutionReport::default();
        for action in self.data.corporate_actions_on(date) {
            if !action.has_effect() {
                continue;
            }
            let Some(existing_position) = portfolio.position(&action.symbol) else {
                continue;
            };
            if existing_position.quantity == 0 {
                continue;
            }

            if action.share_cash.abs() > f64::EPSILON {
                let cash_before = portfolio.cash();
                let (cash_delta, quantity_after, average_cost) = {
                    let position = portfolio
                        .position_mut_if_exists(&action.symbol)
                        .expect("position exists for dividend action");
                    let cash_delta = position.apply_cash_dividend(action.share_cash);
                    (cash_delta, position.quantity, position.average_cost)
                };
                if cash_delta.abs() > f64::EPSILON {
                    let payable_date = action.payable_date.unwrap_or(date);
                    let immediate_cash = payable_date <= date;
                    let note = if immediate_cash {
                        portfolio.apply_cash_delta(cash_delta);
                        format!(
                            "cash_dividend {} share_cash={:.6} quantity={} cash={:.2}",
                            action.symbol, action.share_cash, quantity_after, cash_delta
                        )
                    } else {
                        portfolio.add_cash_receivable(CashReceivable {
                            symbol: action.symbol.clone(),
                            ex_date: date,
                            payable_date,
                            amount: cash_delta,
                            reason: format!("cash_dividend {:.6}", action.share_cash),
                        });
                        format!(
                            "cash_dividend_receivable {} share_cash={:.6} quantity={} payable_date={} cash={:.2}",
                            action.symbol,
                            action.share_cash,
                            quantity_after,
                            payable_date,
                            cash_delta
                        )
                    };
                    notes.push(note.clone());
                    report.account_events.push(AccountEvent {
                        date,
                        cash_before,
                        cash_after: portfolio.cash(),
                        total_equity: portfolio.total_equity(),
                        note,
                    });
                    report.position_events.push(PositionEvent {
                        date,
                        symbol: action.symbol.clone(),
                        delta_quantity: 0,
                        quantity_after,
                        average_cost,
                        realized_pnl_delta: 0.0,
                        reason: format!("cash_dividend {:.6}", action.share_cash),
                    });
                }
            }

            let split_ratio = action.split_ratio();
            if (split_ratio - 1.0).abs() > f64::EPSILON {
                let (delta_quantity, quantity_after, average_cost) = {
                    let position = portfolio
                        .position_mut_if_exists(&action.symbol)
                        .expect("position exists for split action");
                    let delta_quantity = position.apply_split_ratio(split_ratio);
                    (delta_quantity, position.quantity, position.average_cost)
                };
                if delta_quantity != 0 {
                    let note = format!(
                        "stock_split {} ratio={:.6} delta_qty={}",
                        action.symbol, split_ratio, delta_quantity
                    );
                    notes.push(note);
                    report.position_events.push(PositionEvent {
                        date,
                        symbol: action.symbol.clone(),
                        delta_quantity,
                        quantity_after,
                        average_cost,
                        realized_pnl_delta: 0.0,
                        reason: format!("stock_split {:.6}", split_ratio),
                    });
                }
            }

            if action.has_successor_conversion() {
                let successor_symbol = action
                    .successor_symbol
                    .as_deref()
                    .expect("successor symbol checked");
                let Some(outcome) = portfolio.apply_successor_conversion(
                    &action.symbol,
                    successor_symbol,
                    action.successor_ratio_value(),
                    action.successor_cash_value(),
                ) else {
                    continue;
                };
                let reason = format!(
                    "successor_conversion {}->{} ratio={:.6} cash_per_share={:.6}",
                    outcome.old_symbol,
                    outcome.new_symbol,
                    action.successor_ratio_value(),
                    action.successor_cash_value()
                );
                notes.push(reason.clone());
                report.position_events.push(PositionEvent {
                    date,
                    symbol: outcome.old_symbol.clone(),
                    delta_quantity: -(outcome.old_quantity as i32),
                    quantity_after: 0,
                    average_cost: 0.0,
                    realized_pnl_delta: 0.0,
                    reason: reason.clone(),
                });
                report.position_events.push(PositionEvent {
                    date,
                    symbol: outcome.new_symbol.clone(),
                    delta_quantity: outcome.new_quantity_delta,
                    quantity_after: outcome.new_quantity_after,
                    average_cost: outcome.new_average_cost_after,
                    realized_pnl_delta: 0.0,
                    reason: reason.clone(),
                });
                if outcome.cash_delta.abs() > f64::EPSILON {
                    let cash_before = portfolio.cash();
                    portfolio.apply_cash_delta(outcome.cash_delta);
                    report.account_events.push(AccountEvent {
                        date,
                        cash_before,
                        cash_after: portfolio.cash(),
                        total_equity: portfolio.total_equity(),
                        note: format!("{} cash={:.2}", reason, outcome.cash_delta),
                    });
                }
            }
        }

        portfolio.prune_flat_positions();
        Ok(report)
    }

    fn settle_cash_receivables(
        &self,
        date: NaiveDate,
        portfolio: &mut PortfolioState,
        notes: &mut Vec<String>,
    ) -> Result<BrokerExecutionReport, BacktestError> {
        let mut report = BrokerExecutionReport::default();
        let settled = portfolio.settle_cash_receivables(date);
        for receivable in settled {
            let mut note = format!(
                "cash_receivable_settled {} ex_date={} payable_date={} cash={:.2}",
                receivable.symbol, receivable.ex_date, receivable.payable_date, receivable.amount
            );
            let cash_before = portfolio.cash() - receivable.amount;

            if self.dividend_reinvestment
                && receivable.reason.starts_with("cash_dividend")
                && receivable.amount > 0.0
            {
                let reinvest_price = portfolio
                    .position(&receivable.symbol)
                    .map(|position| position.last_price)
                    .filter(|price| price.is_finite() && *price > 0.0)
                    .or_else(|| {
                        self.data
                            .calendar()
                            .previous_day(date)
                            .and_then(|prev_date| {
                                self.data.price_on_or_before(
                                    prev_date,
                                    &receivable.symbol,
                                    PriceField::Close,
                                )
                            })
                    });
                let round_lot = self
                    .data
                    .instrument(&receivable.symbol)
                    .map(|instrument| instrument.round_lot.max(1))
                    .unwrap_or(100);
                if let Some(price) = reinvest_price {
                    let raw_quantity = (receivable.amount / price).floor() as u32;
                    let reinvest_quantity = (raw_quantity / round_lot) * round_lot;
                    if reinvest_quantity > 0 {
                        let reinvest_cash = reinvest_quantity as f64 * price;
                        let residual_cash = receivable.amount - reinvest_cash;
                        portfolio.apply_cash_delta(-reinvest_cash);
                        portfolio.position_mut(&receivable.symbol).buy(
                            date,
                            reinvest_quantity,
                            price,
                        );

                        note = format!(
                            "cash_receivable_reinvested {} ex_date={} payable_date={} cash={:.2} reinvest_qty={} reinvest_price={:.4} residual_cash={:.2}",
                            receivable.symbol,
                            receivable.ex_date,
                            receivable.payable_date,
                            receivable.amount,
                            reinvest_quantity,
                            price,
                            residual_cash
                        );
                        report.fill_events.push(FillEvent {
                            date,
                            order_id: None,
                            symbol: receivable.symbol.clone(),
                            side: OrderSide::Buy,
                            quantity: reinvest_quantity,
                            price,
                            gross_amount: reinvest_cash,
                            commission: 0.0,
                            stamp_tax: 0.0,
                            net_cash_flow: -reinvest_cash,
                            reason: "dividend_reinvestment".to_string(),
                        });
                        report.position_events.push(PositionEvent {
                            date,
                            symbol: receivable.symbol.clone(),
                            delta_quantity: reinvest_quantity as i32,
                            quantity_after: portfolio
                                .position(&receivable.symbol)
                                .map(|position| position.quantity)
                                .unwrap_or(0),
                            average_cost: portfolio
                                .position(&receivable.symbol)
                                .map(|position| position.average_cost)
                                .unwrap_or(0.0),
                            realized_pnl_delta: 0.0,
                            reason: "dividend_reinvestment".to_string(),
                        });
                        report.process_events.push(ProcessEvent {
                            date,
                            kind: ProcessEventKind::Trade,
                            order_id: None,
                            symbol: Some(receivable.symbol.clone()),
                            side: Some(OrderSide::Buy),
                            detail: format!(
                                "dividend_reinvestment quantity={} price={}",
                                reinvest_quantity, price
                            ),
                        });
                    }
                }
            }
            notes.push(note.clone());
            report.account_events.push(AccountEvent {
                date,
                cash_before,
                cash_after: portfolio.cash(),
                total_equity: portfolio.total_equity(),
                note,
            });
        }
        Ok(report)
    }

    fn settle_delisted_positions(
        &self,
        date: NaiveDate,
        portfolio: &mut PortfolioState,
        notes: &mut Vec<String>,
    ) -> Result<BrokerExecutionReport, BacktestError> {
        let mut report = BrokerExecutionReport::default();
        let symbols = portfolio.positions().keys().cloned().collect::<Vec<_>>();
        for symbol in symbols {
            let Some(position) = portfolio.position(&symbol) else {
                continue;
            };
            if position.quantity == 0 {
                continue;
            }
            let Some(instrument) = self.data.instrument(&symbol) else {
                continue;
            };
            let should_settle = instrument.is_delisted_before(date)
                || (instrument.status.eq_ignore_ascii_case("delisted")
                    && instrument.delisted_at.is_none()
                    && self.data.market(date, &symbol).is_none());
            if !should_settle {
                continue;
            }

            let quantity = position.quantity;
            let fallback_reference_price = if position.last_price > 0.0 {
                position.last_price
            } else {
                position.average_cost
            };
            let effective_delisted_at = instrument
                .delisted_at
                .or_else(|| self.data.calendar().previous_day(date))
                .unwrap_or(date);
            let settlement_price = self
                .data
                .price_on_or_before(effective_delisted_at, &symbol, PriceField::Close)
                .or_else(|| {
                    self.data
                        .price_on_or_before(date, &symbol, PriceField::Close)
                })
                .filter(|price| price.is_finite() && *price > 0.0)
                .unwrap_or(fallback_reference_price);
            if !settlement_price.is_finite() || settlement_price <= 0.0 {
                return Err(BacktestError::Execution(format!(
                    "missing delisting settlement price for {} on {}",
                    symbol, date
                )));
            }

            let cash_before = portfolio.cash();
            let gross_amount = settlement_price * quantity as f64;
            let realized_pnl_delta = {
                let position = portfolio
                    .position_mut_if_exists(&symbol)
                    .expect("position exists for delisting settlement");
                position
                    .sell(quantity, settlement_price)
                    .map_err(BacktestError::Execution)?
            };
            portfolio.apply_cash_delta(gross_amount);
            portfolio.prune_flat_positions();

            let reason = format!(
                "delisted_cash_settlement effective_date={} status={}",
                effective_delisted_at, instrument.status
            );
            notes.push(reason.clone());
            report.order_events.push(OrderEvent {
                date,
                order_id: None,
                symbol: symbol.clone(),
                side: OrderSide::Sell,
                requested_quantity: quantity,
                filled_quantity: quantity,
                status: OrderStatus::Filled,
                reason: reason.clone(),
            });
            report.fill_events.push(FillEvent {
                date,
                order_id: None,
                symbol: symbol.clone(),
                side: OrderSide::Sell,
                quantity,
                price: settlement_price,
                gross_amount,
                commission: 0.0,
                stamp_tax: 0.0,
                net_cash_flow: gross_amount,
                reason: reason.clone(),
            });
            report.position_events.push(PositionEvent {
                date,
                symbol: symbol.clone(),
                delta_quantity: -(quantity as i32),
                quantity_after: 0,
                average_cost: 0.0,
                realized_pnl_delta,
                reason: reason.clone(),
            });
            report.account_events.push(AccountEvent {
                date,
                cash_before,
                cash_after: portfolio.cash(),
                total_equity: portfolio.total_equity(),
                note: reason,
            });
        }
        Ok(report)
    }
}

fn collect_scheduled_decisions<S: Strategy>(
    strategy: &mut S,
    scheduler: &Scheduler<'_>,
    execution_date: NaiveDate,
    stage: ScheduleStage,
    rules: &[ScheduleRule],
    ctx: &StrategyContext<'_>,
    process_events: &mut Vec<ProcessEvent>,
    process_event_bus: &mut ProcessEventBus,
) -> Result<crate::strategy::StrategyDecision, BacktestError> {
    let mut combined = crate::strategy::StrategyDecision::default();
    for rule in scheduler.triggered_rules(execution_date, stage, rules) {
        publish_phase_event(
            strategy,
            process_event_bus,
            ctx,
            process_events,
            execution_date,
            ProcessEventKind::PreScheduled,
            format!("scheduled:{}:{}:pre", rule.name, stage_label(stage)),
        )?;
        combined.merge_from(strategy.on_scheduled(ctx, rule)?);
        publish_phase_event(
            strategy,
            process_event_bus,
            ctx,
            process_events,
            execution_date,
            ProcessEventKind::PostScheduled,
            format!("scheduled:{}:{}:post", rule.name, stage_label(stage)),
        )?;
    }
    Ok(combined)
}

fn publish_phase_event<S: Strategy>(
    strategy: &mut S,
    process_event_bus: &mut ProcessEventBus,
    ctx: &StrategyContext<'_>,
    events: &mut Vec<ProcessEvent>,
    date: NaiveDate,
    kind: ProcessEventKind,
    detail: impl Into<String>,
) -> Result<(), BacktestError> {
    let event = ProcessEvent {
        date,
        kind,
        order_id: None,
        symbol: None,
        side: None,
        detail: detail.into(),
    };
    process_event_bus.publish(&event);
    strategy.on_process_event(ctx, &event)?;
    events.push(event);
    Ok(())
}

fn publish_process_events<S: Strategy>(
    strategy: &mut S,
    process_event_bus: &mut ProcessEventBus,
    ctx: &StrategyContext<'_>,
    target: &mut Vec<ProcessEvent>,
    incoming: &mut Vec<ProcessEvent>,
) -> Result<(), BacktestError> {
    for event in incoming.drain(..) {
        process_event_bus.publish(&event);
        strategy.on_process_event(ctx, &event)?;
        target.push(event);
    }
    Ok(())
}

fn stage_label(stage: ScheduleStage) -> &'static str {
    match stage {
        ScheduleStage::OpenAuction => "open_auction",
        ScheduleStage::OnDay => "on_day",
    }
}

mod date_format {
    use chrono::NaiveDate;
    use serde::Serializer;

    const FORMAT: &str = "%Y-%m-%d";

    pub fn serialize<S>(date: &NaiveDate, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&date.format(FORMAT).to_string())
    }
}
