use std::sync::Arc;

use arrow::array::{Array, TimestampNanosecondArray};
use arrow::datatypes::{DataType, Field, Schema, SchemaRef, TimeUnit};
use arrow::record_batch::RecordBatch;
use bt_kernel::{BtError, Result, NANOS_PER_SECOND};
use serde_json::json;
use sha2::{Digest, Sha256};

pub fn canonical_bars_1m_schema() -> SchemaRef {
    Arc::new(Schema::new(vec![
        Field::new(
            "timestamp",
            DataType::Timestamp(TimeUnit::Nanosecond, Some("UTC".into())),
            false,
        ),
        Field::new("symbol_id", DataType::UInt32, false),
        Field::new("open", DataType::Float64, false),
        Field::new("high", DataType::Float64, false),
        Field::new("low", DataType::Float64, false),
        Field::new("close", DataType::Float64, false),
        Field::new("vwap", DataType::Float64, false),
        Field::new("volume", DataType::Float64, false),
        Field::new("buy_volume", DataType::Float64, true),
        Field::new("sell_volume", DataType::Float64, true),
        Field::new("trade_count", DataType::UInt32, false),
        Field::new("bid", DataType::Float64, true),
        Field::new("ask", DataType::Float64, true),
        Field::new("spread", DataType::Float64, true),
        Field::new("is_gap", DataType::Boolean, false),
        Field::new("gap_fill_method", DataType::UInt8, false),
    ]))
}

pub fn canonical_bars_1m_schema_json() -> serde_json::Value {
    schema_as_json(canonical_bars_1m_schema().as_ref())
}

pub fn schema_as_json(schema: &Schema) -> serde_json::Value {
    let fields = schema
        .fields()
        .iter()
        .map(|field| {
            json!({
                "name": field.name(),
                "data_type": format!("{:?}", field.data_type()),
                "nullable": field.is_nullable()
            })
        })
        .collect::<Vec<_>>();

    json!({ "fields": fields })
}

pub fn validate_bars_1m_schema(schema: &Schema) -> Result<()> {
    let expected = canonical_bars_1m_schema();

    if schema.fields().len() != expected.fields().len() {
        return Err(BtError::SchemaMismatch {
            expected: format!("{} fields", expected.fields().len()),
            got: format!("{} fields", schema.fields().len()),
        });
    }

    for (idx, (expected_field, got_field)) in
        expected.fields().iter().zip(schema.fields()).enumerate()
    {
        let same_name = expected_field.name() == got_field.name();
        let same_type = expected_field.data_type() == got_field.data_type();
        let same_nullability = expected_field.is_nullable() == got_field.is_nullable();

        if !(same_name && same_type && same_nullability) {
            return Err(BtError::SchemaMismatch {
                expected: format!("index {idx}: {expected_field:?}"),
                got: format!("index {idx}: {got_field:?}"),
            });
        }
    }

    Ok(())
}

pub fn validate_second_aligned_timestamps(batch: &RecordBatch) -> Result<()> {
    let column = batch
        .column_by_name("timestamp")
        .ok_or_else(|| BtError::Data("missing timestamp column".to_string()))?;

    let timestamps = column
        .as_any()
        .downcast_ref::<TimestampNanosecondArray>()
        .ok_or_else(|| BtError::Data("timestamp must be TimestampNanosecondArray".to_string()))?;

    for idx in 0..timestamps.len() {
        if timestamps.is_null(idx) {
            return Err(BtError::Data(format!("timestamp at row {idx} is null")));
        }

        let value = timestamps.value(idx);
        if value % NANOS_PER_SECOND != 0 {
            return Err(BtError::Data(format!(
                "timestamp at row {idx} is not aligned to second: {value}"
            )));
        }
    }

    Ok(())
}

pub fn validate_bars_1m_batch(batch: &RecordBatch) -> Result<()> {
    validate_bars_1m_schema(batch.schema().as_ref())?;
    validate_second_aligned_timestamps(batch)?;
    Ok(())
}

pub fn schema_hash(schema: &Schema) -> String {
    let mut hasher = Sha256::new();
    for field in schema.fields() {
        hasher.update(field.name().as_bytes());
        hasher.update(format!("{:?}", field.data_type()).as_bytes());
        hasher.update([u8::from(field.is_nullable())]);
    }

    let digest = hasher.finalize();
    let mut out = String::with_capacity(digest.len() * 2);
    for byte in digest {
        out.push_str(&format!("{byte:02x}"));
    }
    out
}
