//! Date and time type implementations

use std::borrow::Cow;
use std::io::{Read, Write};

use chrono::{DateTime, NaiveDate, NaiveDateTime};

use crate::error::{ReadError, WriteError};
use crate::traits::ClickHouseType;

/// Date type (maps to ClickHouse Date or Date32)
pub type Date = NaiveDate;

impl ClickHouseType for NaiveDate {
    fn type_name() -> Cow<'static, str> {
        Cow::Borrowed("Date")
    }

    fn write_binary<W: Write>(&self, writer: &mut W) -> Result<(), WriteError> {
        // Date is stored as days since 1970-01-01
        let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
        let days = self.signed_duration_since(epoch).num_days() as u16;
        writer.write_all(&days.to_le_bytes())?;
        Ok(())
    }

    fn read_binary<R: Read>(reader: &mut R) -> Result<Self, ReadError> {
        let mut buf = [0u8; 2];
        reader.read_exact(&mut buf).map_err(|_| ReadError::UnexpectedEof)?;
        let days = u16::from_le_bytes(buf);

        let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
        epoch
            .checked_add_signed(chrono::Duration::days(days as i64))
            .ok_or_else(|| ReadError::InvalidFormat("Invalid date value".to_string()))
    }

    fn fixed_size() -> Option<usize> {
        Some(2)
    }

    fn to_sql_literal(&self) -> String {
        format!("'{}'", self.format("%Y-%m-%d"))
    }
}

impl ClickHouseType for NaiveDateTime {
    fn type_name() -> Cow<'static, str> {
        Cow::Borrowed("DateTime")
    }

    fn write_binary<W: Write>(&self, writer: &mut W) -> Result<(), WriteError> {
        // DateTime is stored as Unix timestamp (seconds since epoch)
        let timestamp = self.and_utc().timestamp() as u32;
        writer.write_all(&timestamp.to_le_bytes())?;
        Ok(())
    }

    fn read_binary<R: Read>(reader: &mut R) -> Result<Self, ReadError> {
        let mut buf = [0u8; 4];
        reader.read_exact(&mut buf).map_err(|_| ReadError::UnexpectedEof)?;
        let timestamp = u32::from_le_bytes(buf);

        DateTime::from_timestamp(timestamp as i64, 0)
            .map(|dt| dt.naive_utc())
            .ok_or_else(|| ReadError::InvalidFormat("Invalid datetime value".to_string()))
    }

    fn fixed_size() -> Option<usize> {
        Some(4)
    }

    fn to_sql_literal(&self) -> String {
        format!("'{}'", self.format("%Y-%m-%d %H:%M:%S"))
    }
}

/// DateTime64 with precision (0-9 for subsecond precision)
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct DateTime64<const PRECISION: u8> {
    timestamp: i64,
}

impl<const P: u8> DateTime64<P> {
    /// Create from Unix timestamp with subsecond precision
    pub fn from_timestamp(timestamp: i64) -> Self {
        Self { timestamp }
    }

    /// Get the Unix timestamp
    pub fn timestamp(&self) -> i64 {
        self.timestamp
    }

    /// Convert to NaiveDateTime
    pub fn to_naive_datetime(&self) -> Option<NaiveDateTime> {
        let divisor = 10i64.pow(P as u32);
        let secs = self.timestamp / divisor;
        let nanos = ((self.timestamp % divisor) * (1_000_000_000 / divisor)) as u32;
        DateTime::from_timestamp(secs, nanos).map(|dt| dt.naive_utc())
    }

    /// Create from NaiveDateTime
    pub fn from_naive_datetime(dt: NaiveDateTime) -> Self {
        let secs = dt.and_utc().timestamp();
        let nanos = dt.and_utc().timestamp_subsec_nanos();
        let divisor = 10i64.pow(P as u32);
        let subsec = (nanos as i64 * divisor) / 1_000_000_000;
        Self {
            timestamp: secs * divisor + subsec,
        }
    }
}

impl<const P: u8> ClickHouseType for DateTime64<P> {
    fn type_name() -> Cow<'static, str> {
        Cow::Owned(format!("DateTime64({})", P))
    }

    fn write_binary<W: Write>(&self, writer: &mut W) -> Result<(), WriteError> {
        writer.write_all(&self.timestamp.to_le_bytes())?;
        Ok(())
    }

    fn read_binary<R: Read>(reader: &mut R) -> Result<Self, ReadError> {
        let mut buf = [0u8; 8];
        reader.read_exact(&mut buf).map_err(|_| ReadError::UnexpectedEof)?;
        let timestamp = i64::from_le_bytes(buf);
        Ok(Self { timestamp })
    }

    fn fixed_size() -> Option<usize> {
        Some(8)
    }

    fn to_sql_literal(&self) -> String {
        if let Some(dt) = self.to_naive_datetime() {
            format!("'{}'", dt.format("%Y-%m-%d %H:%M:%S"))
        } else {
            format!("{}", self.timestamp)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_date_roundtrip() {
        let date = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
        let mut buf = Vec::new();
        date.write_binary(&mut buf).unwrap();
        let decoded = NaiveDate::read_binary(&mut buf.as_slice()).unwrap();
        assert_eq!(date, decoded);
    }

    #[test]
    fn test_datetime64_roundtrip() {
        let dt = NaiveDateTime::from_timestamp_opt(1705334400, 123_456_789).unwrap();
        let dt64 = DateTime64::<3>::from_naive_datetime(dt);
        let mut buf = Vec::new();
        dt64.write_binary(&mut buf).unwrap();
        let decoded = DateTime64::<3>::read_binary(&mut buf.as_slice()).unwrap();
        assert_eq!(dt64, decoded);
    }
}
