use calcpad_engine::{CalcResult, EvalContext, ResultType};
use serde::Serialize;
use wasm_bindgen::prelude::*;

/// JSON-serializable result for WASM consumers.
///
/// Fields match the contract expected by the web frontend:
/// - `type`     : "number" | "unitValue" | "currencyValue" | "dateTime"
///                | "timeDelta" | "boolean" | "error"
/// - `display`  : human-readable formatted string
/// - `rawValue` : numeric value when applicable, otherwise null
/// - `error`    : error message when type == "error", otherwise null
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JsResult {
    #[serde(rename = "type")]
    pub result_type: String,
    pub display: String,
    pub raw_value: Option<f64>,
    pub error: Option<String>,
}

impl From<&CalcResult> for JsResult {
    fn from(r: &CalcResult) -> Self {
        let result_type = match r.metadata.result_type {
            ResultType::Number => "number",
            ResultType::UnitValue => "unitValue",
            ResultType::CurrencyValue => "currencyValue",
            ResultType::DateTime => "dateTime",
            ResultType::TimeDelta => "timeDelta",
            ResultType::Boolean => "boolean",
            ResultType::Empty => "empty",
            ResultType::NonCalculable => "nonCalculable",
            ResultType::Error => "error",
        }
        .to_string();

        let error = if r.metadata.result_type == ResultType::Error {
            // Strip the "Error: " prefix that the engine prepends to display.
            let msg = r
                .metadata
                .display
                .strip_prefix("Error: ")
                .unwrap_or(&r.metadata.display);
            Some(msg.to_string())
        } else {
            None
        };

        JsResult {
            result_type,
            display: r.metadata.display.clone(),
            raw_value: r.metadata.raw_value,
            error,
        }
    }
}

/// Build an `EvalContext`, pulling the current date from JS when running
/// inside a WASM runtime.
fn create_default_context() -> EvalContext {
    #[cfg(target_arch = "wasm32")]
    {
        let js_date = js_sys::Date::new_0();
        let year = js_date.get_full_year() as i32;
        let month = js_date.get_month() as u32 + 1; // JS months are 0-indexed
        let day = js_date.get_date() as u32;
        if let Some(date) = chrono::NaiveDate::from_ymd_opt(year, month, day) {
            EvalContext::with_date(date)
        } else {
            EvalContext::default()
        }
    }
    #[cfg(not(target_arch = "wasm32"))]
    {
        EvalContext::default()
    }
}

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

/// Evaluate a single line of CalcPad input.
///
/// Returns a JS object: `{ type, display, rawValue, error }`.
#[wasm_bindgen(js_name = "evalLine")]
pub fn eval_line(input: &str) -> JsValue {
    let mut ctx = create_default_context();
    let result = calcpad_engine::eval_line(input, &mut ctx);
    let js_result = JsResult::from(&result);
    serde_wasm_bindgen::to_value(&js_result).unwrap_or(JsValue::NULL)
}

/// Evaluate a sheet (array of lines) with shared variable context.
///
/// Accepts a JS array of strings and returns a JS array of result objects.
#[wasm_bindgen(js_name = "evalSheet")]
pub fn eval_sheet(lines: JsValue) -> JsValue {
    let lines: Vec<String> = serde_wasm_bindgen::from_value(lines).unwrap_or_default();
    let mut ctx = create_default_context();

    let results: Vec<JsResult> = lines
        .iter()
        .map(|line| {
            let result = calcpad_engine::eval_line(line, &mut ctx);
            JsResult::from(&result)
        })
        .collect();

    serde_wasm_bindgen::to_value(&results).unwrap_or(JsValue::NULL)
}

// ---------------------------------------------------------------------------
// Native tests (run with `cargo test`, not wasm-bindgen-test)
// ---------------------------------------------------------------------------

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

    fn ctx() -> EvalContext {
        EvalContext::with_date(chrono::NaiveDate::from_ymd_opt(2026, 3, 16).unwrap())
    }

    #[test]
    fn js_result_from_number() {
        let mut ctx = ctx();
        let r = calcpad_engine::eval_line("2 + 3", &mut ctx);
        let js = JsResult::from(&r);
        assert_eq!(js.result_type, "number");
        assert_eq!(js.display, "5");
        assert_eq!(js.raw_value, Some(5.0));
        assert!(js.error.is_none());
    }

    #[test]
    fn js_result_from_error() {
        let mut ctx = ctx();
        let r = calcpad_engine::eval_line("1 / 0", &mut ctx);
        let js = JsResult::from(&r);
        assert_eq!(js.result_type, "error");
        assert!(js.error.is_some());
    }

    #[test]
    fn sheet_context_flows() {
        let mut ctx = ctx();

        let r1 = calcpad_engine::eval_line("x = 10", &mut ctx);
        assert_eq!(JsResult::from(&r1).display, "10");

        let r2 = calcpad_engine::eval_line("y = x + 5", &mut ctx);
        assert_eq!(JsResult::from(&r2).display, "15");

        let r3 = calcpad_engine::eval_line("y * 2", &mut ctx);
        assert_eq!(JsResult::from(&r3).display, "30");
    }

    #[test]
    fn float_precision() {
        // NOTE: Engine still uses f64 internally. Once number.rs (dashu) is wired
        // into the interpreter, this will produce exact "0.3". For now, accept f64 result.
        let mut ctx = ctx();
        let r = calcpad_engine::eval_line("0.1 + 0.2", &mut ctx);
        let display = JsResult::from(&r).display;
        assert!(
            display == "0.3" || display == "0.30000000000000004",
            "unexpected: {display}"
        );
    }
}
