//! Trade dispatch: fan-out of trades to threshold processors with midnight reset.

use super::types::*;
use crate::ring_buffer::ConcurrentRingBuffer;
use opendeviationbar_core::processor::OpenDeviationBarProcessor;
use opendeviationbar_core::{AggTrade, OpenDeviationBar};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use tokio::sync::watch;

/// Type alias for the shared sink collection used in fan-out dispatch (Issue #318).
///
/// When `clickhouse-sink` feature is enabled, this is `Some(Arc<Mutex<Vec<Box<dyn BarSink>>>>)`.
/// When disabled, callers pass `None`. The type is always present to avoid
/// duplicating function signatures with `#[cfg]`.
#[cfg(feature = "clickhouse-sink")]
pub(crate) type ExtraSinks = Option<std::sync::Arc<std::sync::Mutex<Vec<Box<dyn crate::engine::traits::BarSink>>>>>;
#[cfg(not(feature = "clickhouse-sink"))]
pub(crate) type ExtraSinks = Option<()>;

/// Process a single trade through all threshold processors, handling midnight resets,
/// bar emission to ring buffer, forming bar updates, and metrics.
///
/// Extracted from the symbol_task loop to enable burst-atomic frame processing (Issue #273).
///
/// Issue #318: When `clickhouse-sink` feature is enabled, completed bars are also
/// dispatched to `extra_sinks` (e.g., ClickHouseWriterSink) AFTER the ring buffer push.
/// Sink errors never block ring buffer delivery to Python.
#[allow(clippy::too_many_arguments)]
pub(crate) fn process_trade_through_processors(
    trade: &AggTrade,
    processors: &mut [(u32, OpenDeviationBarProcessor)],
    last_days: &mut [i64],
    bar_buffer: &ConcurrentRingBuffer<CompletedBar>,
    metrics: &LiveEngineMetrics,
    symbol_arc: &Arc<str>,
    symbol: &str,
    forming_tx_map: &HashMap<u32, watch::Sender<Option<FormingBar>>>,
    committed_floors: &mut HashMap<u32, i64>,
    last_forming_update: &mut HashMap<u32, i64>, // Issue #300 (MEM-08): per-threshold 1Hz throttle
    ouroboros_mode: OuroborosMode, // Phase 16: per-symbol mode (Day or Aion)
    extra_sinks: &ExtraSinks, // Issue #318: fan-out to BarSink implementations
) {
    let _ = &extra_sinks; // Suppress unused warning when clickhouse-sink disabled
    for (idx, (threshold, processor)) in processors.iter_mut().enumerate() {
        // Midnight reset: if trade crosses day boundary, emit orphan and reset
        if let Some(orphan) =
            maybe_reset_at_midnight(processor, trade.timestamp, &mut last_days[idx], ouroboros_mode)
        {
            // Rust-side dedup: skip orphan bars within already-committed range
            if let Some(&floor) = committed_floors.get(threshold)
                && orphan.last_agg_trade_id > 0
                && orphan.last_agg_trade_id <= floor
            {
                tracing::debug!(%symbol, threshold, last_tid = orphan.last_agg_trade_id, floor, "rust-dedup: skipping orphan (within committed range)");
                continue;
            }
            metrics.bars_emitted.fetch_add(1, Ordering::Relaxed);
            let completed = CompletedBar {
                symbol: symbol_arc.clone(),
                threshold_decimal_bps: *threshold,
                bar: orphan,
            };
            // Update floor after emission
            committed_floors.insert(*threshold, completed.bar.last_agg_trade_id);
            let was_added = bar_buffer.push(completed.clone());
            if !was_added {
                metrics.dropped_bars.fetch_add(1, Ordering::Relaxed);
                metrics.backpressure_events.fetch_add(1, Ordering::Relaxed);
                tracing::warn!(%symbol, threshold, "ring buffer full, old bar dropped (midnight orphan)");
            }
            // Issue #318: Fan-out to extra sinks (ClickHouseWriterSink) AFTER ring buffer push
            #[cfg(feature = "clickhouse-sink")]
            if let Some(sinks) = extra_sinks {
                crate::clickhouse_writer::guards::dispatch_to_sinks(&completed, sinks);
            }
        }

        match processor.process_single_trade(trade) {
            Ok(Some(bar)) => {
                // Rust-side dedup: skip bars within already-committed range
                if let Some(&floor) = committed_floors.get(threshold)
                    && bar.last_agg_trade_id > 0
                    && bar.last_agg_trade_id <= floor
                {
                    tracing::debug!(%symbol, threshold, last_tid = bar.last_agg_trade_id, floor, "rust-dedup: skipping bar (within committed range)");
                    // Still clear forming bar state
                    if let Some(tx) = forming_tx_map.get(threshold) {
                        let _ = tx.send(None);
                    }
                    continue;
                }

                // Issue #214: Clear forming bar BEFORE emitting completed bar
                if let Some(tx) = forming_tx_map.get(threshold) {
                    let _ = tx.send(None);
                }

                metrics.bars_emitted.fetch_add(1, Ordering::Relaxed);
                let completed = CompletedBar {
                    symbol: symbol_arc.clone(),
                    threshold_decimal_bps: *threshold,
                    bar,
                };
                // Update floor after emission
                committed_floors.insert(*threshold, completed.bar.last_agg_trade_id);
                let was_added = bar_buffer.push(completed.clone());
                if !was_added {
                    metrics.dropped_bars.fetch_add(1, Ordering::Relaxed);
                    metrics.backpressure_events.fetch_add(1, Ordering::Relaxed);
                    tracing::warn!(%symbol, threshold, "ring buffer full, old bar dropped");
                }
                // Issue #318: Fan-out to extra sinks AFTER ring buffer push
                #[cfg(feature = "clickhouse-sink")]
                if let Some(sinks) = extra_sinks {
                    crate::clickhouse_writer::guards::dispatch_to_sinks(&completed, sinks);
                }
                let current_depth = bar_buffer.len() as u64;
                let _ = metrics
                    .max_queue_depth
                    .fetch_max(current_depth, Ordering::Relaxed);

                // Issue #300 (MEM-08): Reset forming bar throttle -- new bar started,
                // send forming state immediately on next trade.
                last_forming_update.insert(*threshold, 0);
            }
            Ok(None) => {
                // Trade absorbed, no bar completed yet.
                // Issue #214 + MEM-08: Update forming bar snapshot for consumers.
                // Throttled to 1 Hz -- consumers poll at 1s intervals, so 99.3% of
                // updates are immediately overwritten by watch::send().
                let last_us = last_forming_update.entry(*threshold).or_insert(0);
                if trade.timestamp - *last_us >= 1_000_000 {
                    // 1 second in microseconds
                    if let Some(tx) = forming_tx_map.get(threshold)
                        && let Some(incomplete) = processor.get_incomplete_bar()
                    {
                        let _ = tx.send(Some(FormingBar {
                            symbol: symbol_arc.clone(),
                            threshold_decimal_bps: *threshold,
                            bar: incomplete,
                            last_trade_timestamp_us: trade.timestamp,
                        }));
                        *last_us = trade.timestamp;
                    }
                }
            }
            Err(e) => {
                tracing::warn!(%symbol, threshold = *threshold, ?e, "trade processing error");
            }
        }
    }
}

/// Check if a trade crosses a UTC midnight boundary relative to the processor's last trade,
/// and if so, reset the processor at ouroboros. Returns the orphan bar if one was in progress.
///
/// This prevents cross-midnight bars from being produced. The orphan bar (if any) is emitted
/// as a completed bar with `is_orphan=true`. The trade that triggered the reset is NOT consumed --
/// it will be processed normally after this function returns.
pub(crate) fn maybe_reset_at_midnight(
    processor: &mut OpenDeviationBarProcessor,
    trade_timestamp_us: i64,
    last_day: &mut i64,
    ouroboros_mode: OuroborosMode,
) -> Option<OpenDeviationBar> {
    // Aion mode: never reset at midnight -- bars span across day boundaries
    if ouroboros_mode == OuroborosMode::Aion {
        // Still track the day for metrics/logging, but never trigger reset
        let trade_day = trade_timestamp_us / DAY_US;
        *last_day = trade_day;
        return None;
    }

    // Day mode: existing behavior unchanged
    let trade_day = trade_timestamp_us / DAY_US;
    if *last_day >= 0 && trade_day != *last_day {
        *last_day = trade_day;
        // Reset processor -- any in-progress bar becomes an orphan
        processor.reset_at_ouroboros()
    } else {
        *last_day = trade_day;
        None
    }
}
