import CCalcPadEngine
import Foundation

/// Calculation engine backed by the Rust CalcPad engine via C FFI.
///
/// Thread safety: Each call creates its own `EvalContext` on the Rust side,
/// so concurrent calls from different threads are safe. The C FFI functions
/// are documented as safe to call from any thread.
final class RustCalculationEngine: CalculationEngine, @unchecked Sendable {

    private let decoder = JSONDecoder()

    func evaluateLine(_ line: String) -> LineResult {
        evaluateSingleLine(line, lineNumber: 1)
    }

    func evaluateSheet(_ text: String) -> [LineResult] {
        let lines = text.components(separatedBy: "\n")

        // Build C string array for the FFI call
        let cStrings = lines.map { strdup($0) }
        defer { cStrings.forEach { free($0) } }

        var pointers = cStrings.map { UnsafePointer<CChar>($0) }

        let resultPtr = pointers.withUnsafeMutableBufferPointer { buffer -> UnsafeMutablePointer<CChar>? in
            calcpad_eval_sheet(buffer.baseAddress, Int32(lines.count))
        }

        guard let resultPtr else {
            return lines.enumerated().map { index, line in
                LineResult(id: index + 1, expression: line, result: "Error", isError: true)
            }
        }
        defer { calcpad_free_result(resultPtr) }

        let jsonString = String(cString: resultPtr)

        guard let jsonData = jsonString.data(using: .utf8),
              let response = try? decoder.decode(FFISheetResponse.self, from: jsonData) else {
            return lines.enumerated().map { index, line in
                LineResult(id: index + 1, expression: line, result: "Error", isError: true)
            }
        }

        return response.results.enumerated().map { index, calcResult in
            mapToLineResult(calcResult, expression: lines[index], lineNumber: index + 1)
        }
    }

    // MARK: - Private

    private func evaluateSingleLine(_ line: String, lineNumber: Int) -> LineResult {
        let resultPtr = calcpad_eval_line(line)

        guard let resultPtr else {
            return LineResult(id: lineNumber, expression: line, result: "Error", isError: true)
        }
        defer { calcpad_free_result(resultPtr) }

        let jsonString = String(cString: resultPtr)

        guard let jsonData = jsonString.data(using: .utf8),
              let response = try? decoder.decode(FFIResponse.self, from: jsonData) else {
            return LineResult(id: lineNumber, expression: line, result: "Error", isError: true)
        }

        return mapToLineResult(response.result, expression: line, lineNumber: lineNumber)
    }

    private func mapToLineResult(_ calcResult: FFICalcResult, expression: String, lineNumber: Int) -> LineResult {
        switch calcResult.value {
        case .error(let message):
            // Blank/comment lines produce "empty expression" errors in the Rust engine.
            // Map these to nil result (matching the protocol's convention for non-evaluable lines).
            if isEmptyExpressionError(message) {
                return LineResult(id: lineNumber, expression: expression, result: nil, isError: false)
            }
            return LineResult(id: lineNumber, expression: expression, result: "Error: \(message)", isError: true)

        default:
            return LineResult(id: lineNumber, expression: expression, result: calcResult.metadata.display, isError: false)
        }
    }

    /// The Rust engine returns an error for blank lines, comments, and other
    /// non-evaluable input. We detect these to return nil (matching protocol convention).
    private func isEmptyExpressionError(_ message: String) -> Bool {
        let lower = message.lowercased()
        return lower.contains("empty") || lower.contains("no expression") || lower.contains("comment")
    }
}
