use arrow::array::{Array, BooleanArray, Float64Array, TimestampNanosecondArray};
use arrow::record_batch::RecordBatch;
use bt_kernel::{BtError, Result};

use crate::orchestrator::{ExecutionPrice, IntraBarPrice};
use crate::slippage::BarData;

pub struct BarsView<'a> {
    pub batch: &'a RecordBatch,
    pub timestamp: &'a TimestampNanosecondArray,
    pub open: &'a Float64Array,
    pub high: &'a Float64Array,
    pub low: &'a Float64Array,
    pub close: &'a Float64Array,
    pub vwap: &'a Float64Array,
    pub volume: &'a Float64Array,
    pub bid: &'a Float64Array,
    pub ask: &'a Float64Array,
    pub spread: &'a Float64Array,
    pub is_gap: &'a BooleanArray,
}

impl<'a> BarsView<'a> {
    pub fn from_batch(batch: &'a RecordBatch) -> Result<Self> {
        Ok(Self {
            batch,
            timestamp: timestamp_column(batch, "timestamp")?,
            open: f64_column(batch, "open")?,
            high: f64_column(batch, "high")?,
            low: f64_column(batch, "low")?,
            close: f64_column(batch, "close")?,
            vwap: f64_column(batch, "vwap")?,
            volume: f64_column(batch, "volume")?,
            bid: f64_column(batch, "bid")?,
            ask: f64_column(batch, "ask")?,
            spread: f64_column(batch, "spread")?,
            is_gap: bool_column(batch, "is_gap")?,
        })
    }

    pub fn len(&self) -> usize {
        self.batch.num_rows()
    }

    pub fn is_empty(&self) -> bool {
        self.batch.num_rows() == 0
    }

    pub fn timestamp_value(&self, index: usize) -> i64 {
        self.timestamp.value(index)
    }

    pub fn is_gap_at(&self, index: usize) -> bool {
        !self.is_gap.is_null(index) && self.is_gap.value(index)
    }
}

pub fn resolve_execution_price(
    bars: &BarsView<'_>,
    index: usize,
    mode: &ExecutionPrice,
    warnings: &mut Vec<String>,
) -> Result<f64> {
    let price = match mode {
        ExecutionPrice::AtClose => value_required(bars.close, index, "close")?,
        ExecutionPrice::AtOpen => value_required(bars.open, index, "open")?,
        ExecutionPrice::AtVwap => {
            if bars.vwap.is_null(index) {
                warnings.push(format!(
                    "vwap fallback to close at row {index}: vwap unavailable"
                ));
                value_required(bars.close, index, "close")?
            } else {
                bars.vwap.value(index)
            }
        }
        ExecutionPrice::MidPrice => {
            if bars.bid.is_null(index) || bars.ask.is_null(index) {
                warnings.push(format!(
                    "mid-price fallback used at row {index}: bid/ask unavailable"
                ));
                let open = value_required(bars.open, index, "open")?;
                let close = value_required(bars.close, index, "close")?;
                (open + close) / 2.0
            } else {
                (bars.bid.value(index) + bars.ask.value(index)) / 2.0
            }
        }
        ExecutionPrice::Custom(column) => {
            let custom = f64_column(bars.batch, column)?;
            value_required(custom, index, column)?
        }
    };

    Ok(price)
}

/// Approximate the fill price within a bar using OHLC data.
/// Falls back to `base_price` for `SinglePoint`.
pub fn approximate_intra_bar_price(
    bars: &BarsView<'_>,
    index: usize,
    mode: &IntraBarPrice,
    base_price: f64,
) -> f64 {
    match mode {
        IntraBarPrice::SinglePoint => base_price,
        IntraBarPrice::TypicalPrice => {
            let h = bars.high.value(index);
            let l = bars.low.value(index);
            let c = bars.close.value(index);
            (h + l + c) / 3.0
        }
        IntraBarPrice::OhlcAverage => {
            let o = bars.open.value(index);
            let h = bars.high.value(index);
            let l = bars.low.value(index);
            let c = bars.close.value(index);
            (o + h + l + c) / 4.0
        }
    }
}

/// Validate that fill price is within bar [low, high] range; emit warning if not.
pub fn validate_fill_price(
    bars: &BarsView<'_>,
    index: usize,
    fill_price: f64,
    warnings: &mut Vec<String>,
) {
    let low = bars.low.value(index);
    let high = bars.high.value(index);
    if fill_price < low || fill_price > high {
        warnings.push(format!(
            "fill price {fill_price:.6} outside bar range [{low:.6}, {high:.6}] at row {index}"
        ));
    }
}

pub fn bar_data(bars: &BarsView<'_>, index: usize) -> Result<BarData> {
    Ok(BarData {
        volume: value_required(bars.volume, index, "volume")?,
        spread: if bars.spread.is_null(index) {
            None
        } else {
            Some(bars.spread.value(index))
        },
        is_gap: bars.is_gap_at(index),
        high: value_required(bars.high, index, "high")?,
        low: value_required(bars.low, index, "low")?,
    })
}

fn value_required(array: &Float64Array, index: usize, name: &str) -> Result<f64> {
    if array.is_null(index) {
        return Err(BtError::Data(format!(
            "column '{name}' is null at row {index}"
        )));
    }
    Ok(array.value(index))
}

fn f64_column<'a>(batch: &'a RecordBatch, name: &str) -> Result<&'a Float64Array> {
    batch
        .column_by_name(name)
        .ok_or_else(|| BtError::Data(format!("missing required column '{name}'")))?
        .as_any()
        .downcast_ref::<Float64Array>()
        .ok_or_else(|| BtError::Data(format!("column '{name}' must be Float64")))
}

fn bool_column<'a>(batch: &'a RecordBatch, name: &str) -> Result<&'a BooleanArray> {
    batch
        .column_by_name(name)
        .ok_or_else(|| BtError::Data(format!("missing required column '{name}'")))?
        .as_any()
        .downcast_ref::<BooleanArray>()
        .ok_or_else(|| BtError::Data(format!("column '{name}' must be Boolean")))
}

fn timestamp_column<'a>(
    batch: &'a RecordBatch,
    name: &str,
) -> Result<&'a TimestampNanosecondArray> {
    batch
        .column_by_name(name)
        .ok_or_else(|| BtError::Data(format!("missing required column '{name}'")))?
        .as_any()
        .downcast_ref::<TimestampNanosecondArray>()
        .ok_or_else(|| {
            BtError::Data(format!(
                "column '{name}' must be Timestamp(Nanosecond, UTC)"
            ))
        })
}
