//! Cell-level value formatting for q atoms and vector items.
//!
//! Converts raw q IPC values (CoreValue primitives) to human-readable strings
//! without any Arrow dependency. Null sentinels are rendered as `"null"`.
//! Temporal values use ISO-like formats familiar to both q and Python users.

use chrono::NaiveDate;
use chrono::NaiveDateTime;
use qroissant_core::Atom;
use qroissant_core::VectorData;
use qroissant_kernels::DATE_OFFSET_DAYS;
use qroissant_kernels::MILLIS_PER_DAY;
use qroissant_kernels::Q_NULL_DATE;
use qroissant_kernels::Q_NULL_INT;
use qroissant_kernels::Q_NULL_LONG;
use qroissant_kernels::Q_NULL_MINUTE;
use qroissant_kernels::Q_NULL_MONTH;
use qroissant_kernels::Q_NULL_SECOND;
use qroissant_kernels::Q_NULL_SHORT;
use qroissant_kernels::Q_NULL_TIME;
use qroissant_kernels::Q_NULL_TIMESPAN;
use qroissant_kernels::Q_NULL_TIMESTAMP;
use qroissant_kernels::TIMESTAMP_OFFSET_NS;

pub const MAX_CELL_CHARS: usize = 48;

/// Truncate a string to `MAX_CELL_CHARS` characters, appending `"..."` if cut.
pub fn truncate(s: String) -> String {
    let mut chars = s.chars();
    let head: String = chars.by_ref().take(MAX_CELL_CHARS).collect();
    if chars.next().is_some() {
        format!("{head}...")
    } else {
        head
    }
}

// ---------------------------------------------------------------------------
// Temporal helpers
// ---------------------------------------------------------------------------

fn format_date_days(q_days: i32) -> String {
    // q dates are days since 2000-01-01; NaiveDate::from_ymd uses Unix days
    let unix_days = q_days + DATE_OFFSET_DAYS;
    match NaiveDate::from_num_days_from_ce_opt(unix_days + 719_163) {
        Some(d) => d.format("%Y.%m.%d").to_string(),
        None => format!("<date:{q_days}>"),
    }
}

fn format_timestamp_ns(q_ns: i64) -> String {
    let unix_ns = q_ns.saturating_add(TIMESTAMP_OFFSET_NS);
    let secs = unix_ns.div_euclid(1_000_000_000);
    let nsecs = unix_ns.rem_euclid(1_000_000_000) as u32;
    match NaiveDateTime::from_timestamp_opt(secs, nsecs) {
        Some(dt) => dt.format("%Y.%m.%dT%H:%M:%S.%9f").to_string(),
        None => format!("<timestamp:{q_ns}>"),
    }
}

fn format_month_i32(q_months: i32) -> String {
    // q months are months since 2000-01; month 0 = 2000.01
    let total_months = 2000 * 12 + q_months;
    let year = total_months.div_euclid(12);
    let month = total_months.rem_euclid(12) + 1;
    format!("{year:04}.{month:02}m")
}

fn format_datetime_f64(q_days: f64) -> String {
    let unix_ms = q_days * MILLIS_PER_DAY + 946_684_800_000.0;
    let unix_ms_i64 = unix_ms as i64;
    let secs = unix_ms_i64.div_euclid(1000);
    let ms = unix_ms_i64.rem_euclid(1000) as u32;
    match NaiveDateTime::from_timestamp_opt(secs, ms * 1_000_000) {
        Some(dt) => dt.format("%Y.%m.%dT%H:%M:%S.%3f").to_string(),
        None => format!("<datetime:{q_days}>"),
    }
}

fn format_timespan_ns(q_ns: i64) -> String {
    // Timespans can be negative (use absolute value then sign)
    let (sign, abs_ns) = if q_ns < 0 {
        ("-", (-(q_ns as i128)) as u64)
    } else {
        ("", q_ns as u64)
    };
    let days = abs_ns / 86_400_000_000_000;
    let rem = abs_ns % 86_400_000_000_000;
    let hours = rem / 3_600_000_000_000;
    let rem = rem % 3_600_000_000_000;
    let minutes = rem / 60_000_000_000;
    let rem = rem % 60_000_000_000;
    let secs = rem / 1_000_000_000;
    let ns = rem % 1_000_000_000;
    format!("{sign}{days}D{hours:02}:{minutes:02}:{secs:02}.{ns:09}")
}

fn format_minute_i32(total_minutes: i32) -> String {
    let h = total_minutes / 60;
    let m = total_minutes % 60;
    format!("{h:02}:{m:02}")
}

fn format_second_i32(total_seconds: i32) -> String {
    let h = total_seconds / 3600;
    let m = (total_seconds / 60) % 60;
    let s = total_seconds % 60;
    format!("{h:02}:{m:02}:{s:02}")
}

fn format_time_ms(total_ms: i32) -> String {
    let h = total_ms / 3_600_000;
    let m = (total_ms / 60_000) % 60;
    let s = (total_ms / 1000) % 60;
    let ms = total_ms % 1000;
    format!("{h:02}:{m:02}:{s:02}.{ms:03}")
}

fn format_guid_bytes(bytes: &[u8; 16]) -> String {
    format!(
        "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
        bytes[0],
        bytes[1],
        bytes[2],
        bytes[3],
        bytes[4],
        bytes[5],
        bytes[6],
        bytes[7],
        bytes[8],
        bytes[9],
        bytes[10],
        bytes[11],
        bytes[12],
        bytes[13],
        bytes[14],
        bytes[15],
    )
}

fn format_symbol_bytes(bytes: &[u8]) -> String {
    String::from_utf8_lossy(bytes).into_owned()
}

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/// Format a q atom as a display string (no truncation applied).
pub fn format_atom_raw(atom: &Atom) -> String {
    match atom {
        Atom::Boolean(b) => if *b { "true" } else { "false" }.to_string(),
        Atom::Guid(bytes) => format_guid_bytes(bytes),
        Atom::Byte(b) => format!("0x{b:02x}"),
        Atom::Short(v) => {
            if *v == Q_NULL_SHORT {
                "null".to_string()
            } else {
                v.to_string()
            }
        }
        Atom::Int(v) => {
            if *v == Q_NULL_INT {
                "null".to_string()
            } else {
                v.to_string()
            }
        }
        Atom::Long(v) => {
            if *v == Q_NULL_LONG {
                "null".to_string()
            } else {
                v.to_string()
            }
        }
        Atom::Real(v) => {
            if v.is_nan() {
                "null".to_string()
            } else {
                v.to_string()
            }
        }
        Atom::Float(v) => {
            if v.is_nan() {
                "null".to_string()
            } else {
                v.to_string()
            }
        }
        Atom::Char(b) => {
            let ch = *b as char;
            format!("\"{ch}\"")
        }
        Atom::Symbol(bytes) => format_symbol_bytes(bytes),
        Atom::Timestamp(v) => {
            if *v == Q_NULL_TIMESTAMP {
                "null".to_string()
            } else {
                format_timestamp_ns(*v)
            }
        }
        Atom::Month(v) => {
            if *v == Q_NULL_MONTH {
                "null".to_string()
            } else {
                format_month_i32(*v)
            }
        }
        Atom::Date(v) => {
            if *v == Q_NULL_DATE {
                "null".to_string()
            } else {
                format_date_days(*v)
            }
        }
        Atom::Datetime(v) => {
            if v.is_nan() {
                "null".to_string()
            } else {
                format_datetime_f64(*v)
            }
        }
        Atom::Timespan(v) => {
            if *v == Q_NULL_TIMESPAN {
                "null".to_string()
            } else {
                format_timespan_ns(*v)
            }
        }
        Atom::Minute(v) => {
            if *v == Q_NULL_MINUTE {
                "null".to_string()
            } else {
                format_minute_i32(*v)
            }
        }
        Atom::Second(v) => {
            if *v == Q_NULL_SECOND {
                "null".to_string()
            } else {
                format_second_i32(*v)
            }
        }
        Atom::Time(v) => {
            if *v == Q_NULL_TIME {
                "null".to_string()
            } else {
                format_time_ms(*v)
            }
        }
    }
}

/// Format and truncate a q atom.
pub fn format_atom_cell(atom: &Atom) -> String {
    truncate(format_atom_raw(atom))
}

/// Format a single element from a `VectorData` at `index` (no truncation).
pub fn format_vector_item_raw(data: &VectorData, index: usize) -> String {
    match data {
        VectorData::Boolean(v) => if v[index] != 0 { "true" } else { "false" }.to_string(),
        VectorData::Guid(v) => {
            let chunk: &[u8; 16] = v[index * 16..(index + 1) * 16].try_into().unwrap();
            format_guid_bytes(chunk)
        }
        VectorData::Byte(v) => format!("0x{:02x}", v[index]),
        VectorData::Short(_) => {
            let val = data.as_i16_slice()[index];
            if val == Q_NULL_SHORT {
                "null".to_string()
            } else {
                val.to_string()
            }
        }
        VectorData::Int(_) => {
            let val = data.as_i32_slice()[index];
            if val == Q_NULL_INT {
                "null".to_string()
            } else {
                val.to_string()
            }
        }
        VectorData::Long(_) => {
            let val = data.as_i64_slice()[index];
            if val == Q_NULL_LONG {
                "null".to_string()
            } else {
                val.to_string()
            }
        }
        VectorData::Real(_) => {
            let val = data.as_f32_slice()[index];
            if val.is_nan() {
                "null".to_string()
            } else {
                val.to_string()
            }
        }
        VectorData::Float(_) => {
            let val = data.as_f64_slice()[index];
            if val.is_nan() {
                "null".to_string()
            } else {
                val.to_string()
            }
        }
        VectorData::Char(v) => {
            let ch = v[index] as char;
            ch.to_string()
        }
        VectorData::Symbol(v) => format_symbol_bytes(&v[index]),
        VectorData::Timestamp(_) => {
            let val = data.as_i64_slice()[index];
            if val == Q_NULL_TIMESTAMP {
                "null".to_string()
            } else {
                format_timestamp_ns(val)
            }
        }
        VectorData::Month(_) => {
            let val = data.as_i32_slice()[index];
            if val == Q_NULL_MONTH {
                "null".to_string()
            } else {
                format_month_i32(val)
            }
        }
        VectorData::Date(_) => {
            let val = data.as_i32_slice()[index];
            if val == Q_NULL_DATE {
                "null".to_string()
            } else {
                format_date_days(val)
            }
        }
        VectorData::Datetime(_) => {
            let val = data.as_f64_slice()[index];
            if val.is_nan() {
                "null".to_string()
            } else {
                format_datetime_f64(val)
            }
        }
        VectorData::Timespan(_) => {
            let val = data.as_i64_slice()[index];
            if val == Q_NULL_TIMESPAN {
                "null".to_string()
            } else {
                format_timespan_ns(val)
            }
        }
        VectorData::Minute(_) => {
            let val = data.as_i32_slice()[index];
            if val == Q_NULL_MINUTE {
                "null".to_string()
            } else {
                format_minute_i32(val)
            }
        }
        VectorData::Second(_) => {
            let val = data.as_i32_slice()[index];
            if val == Q_NULL_SECOND {
                "null".to_string()
            } else {
                format_second_i32(val)
            }
        }
        VectorData::Time(_) => {
            let val = data.as_i32_slice()[index];
            if val == Q_NULL_TIME {
                "null".to_string()
            } else {
                format_time_ms(val)
            }
        }
    }
}

/// Format and truncate a single vector item.
pub fn format_vector_item(data: &VectorData, index: usize) -> String {
    truncate(format_vector_item_raw(data, index))
}

/// Format a char vector as a quoted string (e.g. `"abc"`), truncated.
pub fn format_char_vector(data: &[u8]) -> String {
    let s: String = data.iter().map(|&b| b as char).collect();
    truncate(format!("\"{s}\""))
}

/// Return the q primitive label for a `VectorData`.
pub fn primitive_label(data: &VectorData) -> &'static str {
    match data {
        VectorData::Boolean(_) => "boolean",
        VectorData::Guid(_) => "guid",
        VectorData::Byte(_) => "byte",
        VectorData::Short(_) => "short",
        VectorData::Int(_) => "int",
        VectorData::Long(_) => "long",
        VectorData::Real(_) => "real",
        VectorData::Float(_) => "float",
        VectorData::Char(_) => "char",
        VectorData::Symbol(_) => "symbol",
        VectorData::Timestamp(_) => "timestamp",
        VectorData::Month(_) => "month",
        VectorData::Date(_) => "date",
        VectorData::Datetime(_) => "datetime",
        VectorData::Timespan(_) => "timespan",
        VectorData::Minute(_) => "minute",
        VectorData::Second(_) => "second",
        VectorData::Time(_) => "time",
    }
}

/// Return the q primitive label for an `Atom`.
pub fn atom_primitive_label(atom: &Atom) -> &'static str {
    match atom {
        Atom::Boolean(_) => "boolean",
        Atom::Guid(_) => "guid",
        Atom::Byte(_) => "byte",
        Atom::Short(_) => "short",
        Atom::Int(_) => "int",
        Atom::Long(_) => "long",
        Atom::Real(_) => "real",
        Atom::Float(_) => "float",
        Atom::Char(_) => "char",
        Atom::Symbol(_) => "symbol",
        Atom::Timestamp(_) => "timestamp",
        Atom::Month(_) => "month",
        Atom::Date(_) => "date",
        Atom::Datetime(_) => "datetime",
        Atom::Timespan(_) => "timespan",
        Atom::Minute(_) => "minute",
        Atom::Second(_) => "second",
        Atom::Time(_) => "time",
    }
}
