import ExpoModulesCore

public class ZkExpoModule: Module {

    public func definition() -> ModuleDefinition {

        Name("ZkExpo")

        AsyncFunction("groth16ProveV2") { (a: String, b: String, c: String) -> String in
            try self.guardPath(a, label: "graph_path")
            try self.guardPath(b, label: "zkey_path")

            let result = self.string(from: groth16_prove_v2(a, b, c))
            try self.throwIfError(result, fn: "groth16ProveV2")
            return result
        }

        AsyncFunction("groth16ProveV2Base64") { (a: String, b: String, c: String) -> String in
            let result = self.string(from: groth16_prove_v2_base64(a, b, c))
            try self.throwIfError(result, fn: "groth16ProveV2Base64")
            return result
        }

        AsyncFunction("addToCache") {
            (functionName: String, path1: String, path2: String) -> Void in
            try self.guardPath(path1, label: "path1")
            try self.guardPath(path2, label: "path2")

            let errorPtr = add_to_cache(functionName, path1, path2)
            if let ptr = errorPtr {
                let msg = self.string(from: ptr)
                throw NSError(
                    domain: "ZkExpoModule",
                    code: 2,
                    userInfo: [NSLocalizedDescriptionKey: msg]
                )
            }
        }

        AsyncFunction("clearCache") {
            () -> Void in
            clear_cache()
        }
    }

    // Converts a C string pointer to Swift String and frees the Rust memory.
    @inline(__always)
    private func string(from cstr: UnsafePointer<CChar>?) -> String {
        guard let cstr = cstr else { return "" }
        defer { free_string(UnsafeMutablePointer(mutating: cstr)) }
        return String(cString: cstr)
    }

    // Checks whether a result string is a Rust error JSON and throws if so.
    private func throwIfError(_ result: String, fn: String) throws {
        guard result.hasPrefix("{\"error\"") else { return }
        throw NSError(
            domain: "ZkExpoModule",
            code: 3,
            userInfo: [NSLocalizedDescriptionKey: "[\(fn)] \(result)"]
        )
    }

    // Validates a file path before handing it to Rust.
    private func guardPath(_ path: String, label: String) throws {
        if path.contains("%") {
            throw NSError(
                domain: "ZkExpoModule",
                code: 4,
                userInfo: [NSLocalizedDescriptionKey:
                    "\(label) appears URL-encoded ('\(path)'). " +
                    "Strip the 'file://' prefix and call decodeURIComponent() " +
                    "before passing the path to zk-expo."]
            )
        }
        guard FileManager.default.fileExists(atPath: path) else {
            throw NSError(
                domain: "ZkExpoModule",
                code: 1,
                userInfo: [NSLocalizedDescriptionKey: "File not found at \(label): \(path)"]
            )
        }
    }
}
