use crate::context::EvalContext;
use crate::pipeline;
use crate::span::Span;
use crate::types::CalcResult;
use serde::{Deserialize, Serialize};
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::panic;
use std::ptr;

/// Versioned FFI response wrapper for JSON serialization.
/// The schema_version field allows Swift to handle backward-compatible changes.
#[derive(Serialize, Deserialize)]
pub struct FfiResponse {
    /// Schema version for forward compatibility.
    pub schema_version: String,
    /// The calculation result.
    pub result: CalcResult,
}

/// Versioned FFI response for sheet (multi-line) evaluation.
#[derive(Serialize, Deserialize)]
pub struct FfiSheetResponse {
    /// Schema version for forward compatibility.
    pub schema_version: String,
    /// Array of calculation results, one per line.
    pub results: Vec<CalcResult>,
}

const SCHEMA_VERSION: &str = "1.0";

fn make_error_json(message: &str) -> *mut c_char {
    let response = FfiResponse {
        schema_version: SCHEMA_VERSION.to_string(),
        result: CalcResult::error(message, Span::new(0, 0)),
    };
    match serde_json::to_string(&response) {
        Ok(json) => match CString::new(json) {
            Ok(cs) => cs.into_raw(),
            Err(_) => ptr::null_mut(),
        },
        Err(_) => ptr::null_mut(),
    }
}

/// Evaluate a single line of CalcPad input.
///
/// # Safety
/// - `input` must be a valid, null-terminated C string.
/// - The returned pointer is heap-allocated and must be freed by calling
///   `calcpad_free_result`.
///
/// Returns a JSON-serialized string with schema version, result type,
/// display value, raw value, and any error information.
#[no_mangle]
pub unsafe extern "C" fn calcpad_eval_line(input: *const c_char) -> *mut c_char {
    // Catch any panics to prevent unwinding into Swift
    let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
        if input.is_null() {
            return make_error_json("null input pointer");
        }

        let c_str = match CStr::from_ptr(input).to_str() {
            Ok(s) => s,
            Err(_) => return make_error_json("invalid UTF-8 input"),
        };

        let mut ctx = EvalContext::new();
        let calc_result = pipeline::eval_line(c_str, &mut ctx);

        let response = FfiResponse {
            schema_version: SCHEMA_VERSION.to_string(),
            result: calc_result,
        };

        match serde_json::to_string(&response) {
            Ok(json) => match CString::new(json) {
                Ok(cs) => cs.into_raw(),
                Err(_) => make_error_json("result contains null byte"),
            },
            Err(e) => make_error_json(&format!("serialization error: {}", e)),
        }
    }));

    match result {
        Ok(ptr) => ptr,
        Err(_) => make_error_json("internal panic caught"),
    }
}

/// Evaluate multiple lines of CalcPad input as a sheet.
///
/// Variable assignments on earlier lines are visible to later lines.
///
/// # Safety
/// - `lines` must be an array of `count` valid, null-terminated C strings.
/// - `count` must accurately reflect the number of elements in the array.
/// - The returned pointer is heap-allocated and must be freed by calling
///   `calcpad_free_result`.
///
/// Returns a JSON-serialized string containing an array of results.
#[no_mangle]
pub unsafe extern "C" fn calcpad_eval_sheet(
    lines: *const *const c_char,
    count: i32,
) -> *mut c_char {
    let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
        if lines.is_null() || count <= 0 {
            return make_error_json("null or empty input");
        }

        let mut rust_lines: Vec<String> = Vec::with_capacity(count as usize);

        for i in 0..count as usize {
            let line_ptr = *lines.add(i);
            if line_ptr.is_null() {
                rust_lines.push(String::new());
                continue;
            }
            match CStr::from_ptr(line_ptr).to_str() {
                Ok(s) => rust_lines.push(s.to_string()),
                Err(_) => rust_lines.push(String::new()),
            }
        }

        let line_refs: Vec<&str> = rust_lines.iter().map(|s| s.as_str()).collect();
        let mut ctx = EvalContext::new();
        let results = pipeline::eval_sheet(&line_refs, &mut ctx);

        let response = FfiSheetResponse {
            schema_version: SCHEMA_VERSION.to_string(),
            results,
        };

        match serde_json::to_string(&response) {
            Ok(json) => match CString::new(json) {
                Ok(cs) => cs.into_raw(),
                Err(_) => make_error_json("result contains null byte"),
            },
            Err(e) => make_error_json(&format!("serialization error: {}", e)),
        }
    }));

    match result {
        Ok(ptr) => ptr,
        Err(_) => make_error_json("internal panic caught"),
    }
}

/// Free a result string previously returned by `calcpad_eval_line` or
/// `calcpad_eval_sheet`.
///
/// # Safety
/// - `ptr` must be a pointer previously returned by `calcpad_eval_line` or
///   `calcpad_eval_sheet`, or null.
/// - After calling this function, the pointer is invalid and must not be used.
/// - Passing null is safe and results in a no-op.
#[no_mangle]
pub unsafe extern "C" fn calcpad_free_result(ptr: *mut c_char) {
    if ptr.is_null() {
        return;
    }
    // Reconstruct the CString so Rust's allocator can free the memory
    drop(CString::from_raw(ptr));
}
