use crate::span::Span;
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use std::fmt;

/// The result type tag for metadata purposes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResultType {
    Number,
    UnitValue,
    CurrencyValue,
    DateTime,
    TimeDelta,
    Boolean,
    Empty,
    NonCalculable,
    Error,
}

impl fmt::Display for ResultType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ResultType::Number => write!(f, "Number"),
            ResultType::UnitValue => write!(f, "UnitValue"),
            ResultType::CurrencyValue => write!(f, "CurrencyValue"),
            ResultType::DateTime => write!(f, "DateTime"),
            ResultType::TimeDelta => write!(f, "TimeDelta"),
            ResultType::Boolean => write!(f, "Boolean"),
            ResultType::Empty => write!(f, "Empty"),
            ResultType::NonCalculable => write!(f, "NonCalculable"),
            ResultType::Error => write!(f, "Error"),
        }
    }
}

/// Serializable span for JSON output.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct SerializableSpan {
    pub start: usize,
    pub end: usize,
}

impl From<Span> for SerializableSpan {
    fn from(s: Span) -> Self {
        SerializableSpan {
            start: s.start,
            end: s.end,
        }
    }
}

/// Metadata attached to every evaluation result.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ResultMetadata {
    /// The source span of the expression that produced this result.
    pub span: SerializableSpan,
    /// The type tag of the result.
    pub result_type: ResultType,
    /// A display-formatted string of the result.
    pub display: String,
    /// The raw numeric value, if applicable.
    pub raw_value: Option<f64>,
}

/// The value payload of a calculation result.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum CalcValue {
    Number { value: f64 },
    UnitValue { value: f64, unit: String },
    CurrencyValue { amount: f64, currency: String },
    DateTime { date: String },
    TimeDelta { days: i64, description: String },
    Boolean { value: bool },
    Error { message: String, span: SerializableSpan },
}

/// A complete calculation result: value + metadata.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CalcResult {
    pub value: CalcValue,
    pub metadata: ResultMetadata,
}

impl CalcResult {
    pub fn number(val: f64, span: Span) -> Self {
        let display = format_number(val);
        CalcResult {
            value: CalcValue::Number { value: val },
            metadata: ResultMetadata {
                span: span.into(),
                result_type: ResultType::Number,
                display,
                raw_value: Some(val),
            },
        }
    }

    pub fn unit_value(val: f64, unit: &str, span: Span) -> Self {
        let display = format!("{} {}", format_number(val), unit);
        CalcResult {
            value: CalcValue::UnitValue {
                value: val,
                unit: unit.to_string(),
            },
            metadata: ResultMetadata {
                span: span.into(),
                result_type: ResultType::UnitValue,
                display,
                raw_value: Some(val),
            },
        }
    }

    pub fn currency_value(amount: f64, currency: &str, span: Span) -> Self {
        let display = format_currency(amount, currency);
        CalcResult {
            value: CalcValue::CurrencyValue {
                amount,
                currency: currency.to_string(),
            },
            metadata: ResultMetadata {
                span: span.into(),
                result_type: ResultType::CurrencyValue,
                display,
                raw_value: Some(amount),
            },
        }
    }

    pub fn datetime(date: NaiveDate, span: Span) -> Self {
        let display = date.format("%Y-%m-%d").to_string();
        CalcResult {
            value: CalcValue::DateTime {
                date: display.clone(),
            },
            metadata: ResultMetadata {
                span: span.into(),
                result_type: ResultType::DateTime,
                display,
                raw_value: None,
            },
        }
    }

    pub fn time_delta(days: i64, description: &str, span: Span) -> Self {
        let display = description.to_string();
        CalcResult {
            value: CalcValue::TimeDelta {
                days,
                description: description.to_string(),
            },
            metadata: ResultMetadata {
                span: span.into(),
                result_type: ResultType::TimeDelta,
                display,
                raw_value: Some(days as f64),
            },
        }
    }

    pub fn boolean(val: bool, span: Span) -> Self {
        let display = val.to_string();
        CalcResult {
            value: CalcValue::Boolean { value: val },
            metadata: ResultMetadata {
                span: span.into(),
                result_type: ResultType::Boolean,
                display,
                raw_value: None,
            },
        }
    }

    pub fn empty(span: Span) -> Self {
        CalcResult {
            value: CalcValue::Error {
                message: "empty input".to_string(),
                span: span.into(),
            },
            metadata: ResultMetadata {
                span: span.into(),
                result_type: ResultType::Empty,
                display: String::new(),
                raw_value: None,
            },
        }
    }

    pub fn non_calculable(span: Span) -> Self {
        CalcResult {
            value: CalcValue::Error {
                message: "no expression found".to_string(),
                span: span.into(),
            },
            metadata: ResultMetadata {
                span: span.into(),
                result_type: ResultType::NonCalculable,
                display: String::new(),
                raw_value: None,
            },
        }
    }

    pub fn error(message: &str, span: Span) -> Self {
        CalcResult {
            value: CalcValue::Error {
                message: message.to_string(),
                span: span.into(),
            },
            metadata: ResultMetadata {
                span: span.into(),
                result_type: ResultType::Error,
                display: format!("Error: {}", message),
                raw_value: None,
            },
        }
    }

    pub fn result_type(&self) -> ResultType {
        self.metadata.result_type
    }

    /// Returns true if this result is a computed value (not empty, non-calculable, or error).
    pub fn is_calculable(&self) -> bool {
        !matches!(
            self.metadata.result_type,
            ResultType::Empty | ResultType::NonCalculable | ResultType::Error
        )
    }
}

fn format_number(val: f64) -> String {
    if val == val.floor() && val.abs() < 1e15 {
        format!("{}", val as i64)
    } else {
        // Round to 10 significant figures to avoid f64 noise
        let abs = val.abs();
        let magnitude = if abs > 0.0 { abs.log10().floor() as i32 } else { 0 };
        let precision = (10 - magnitude - 1).max(0) as usize;
        let formatted = format!("{:.prec$}", val, prec = precision);
        // Strip trailing zeros after decimal point
        if formatted.contains('.') {
            formatted
                .trim_end_matches('0')
                .trim_end_matches('.')
                .to_string()
        } else {
            formatted
        }
    }
}

fn format_currency(amount: f64, currency: &str) -> String {
    let symbol = match currency {
        "USD" => "$",
        "EUR" => "€",
        "GBP" => "£",
        "JPY" => "¥",
        "BRL" => "R$",
        _ => "",
    };

    if symbol.is_empty() {
        format!("{:.2} {}", amount, currency)
    } else {
        format!("{}{:.2}", symbol, amount)
    }
}
