// Bridge between Rust and Apple's Vision framework.
//
// Exposes a small C ABI that car-vision's `ocr` module links against.
// The whole file is no-op on non-macOS targets — the Rust side is
// cfg-gated to aarch64-apple-darwin so the extern "C" declarations
// never show up there either.
//
// Entry points exposed:
//   - car_vision_ocr              text recognition
//   - car_vision_detect_faces     face rectangles (no identity)
//   - car_vision_detect_barcodes  barcodes / QR codes (payload + bbox)
//   - car_vision_classify_image   top-N image classifications (confidence)
// More requests get added here as callers need them.

import Foundation

#if canImport(Vision)
import Vision
import CoreImage
import CoreGraphics
import ImageIO
#endif

// MARK: - C string helpers

private func cString(_ s: String) -> UnsafeMutablePointer<CChar>? {
    return strdup(s)
}

private func writeError(_ outErr: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?, _ msg: String) {
    guard let outErr = outErr else { return }
    outErr.pointee = cString(msg)
}

@_cdecl("car_vision_free_string")
public func car_vision_free_string(_ ptr: UnsafeMutablePointer<CChar>?) {
    guard let ptr = ptr else { return }
    free(ptr)
}

// MARK: - Availability

@_cdecl("car_vision_is_available")
public func car_vision_is_available() -> Int32 {
    #if canImport(Vision)
    return 1
    #else
    return 0
    #endif
}

// MARK: - OCR
//
// Synchronous text recognition. The Rust side hands us a path to an
// image file (PNG/JPEG/HEIC — anything CGImageSource opens) and a
// JSON config blob with a few knobs. We return a JSON array of
// observations: { "text", "confidence", "x", "y", "w", "h" } where
// (x,y,w,h) is normalized to [0,1] in Vision's bottom-left origin.
//
// Errors come back as a JSON object: { "error": "<message>" }.

@_cdecl("car_vision_ocr")
public func car_vision_ocr(
    _ imagePathC: UnsafePointer<CChar>?,
    _ configJsonC: UnsafePointer<CChar>?,
    _ outJson: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?,
    _ outErr: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
) -> Int32 {
    guard let imagePathC = imagePathC else {
        writeError(outErr, "image path is null")
        return 1
    }
    let imagePath = String(cString: imagePathC)
    let configJson = configJsonC.flatMap { String(cString: $0) } ?? "{}"

    #if canImport(Vision)
    do {
        let cfg = OcrConfig.parse(configJson)
        let image = try loadCGImage(path: imagePath)
        let observations = try recognizeText(image: image, config: cfg)
        let json = encodeObservations(observations)
        if let outJson = outJson {
            outJson.pointee = cString(json)
        }
        return 0
    } catch {
        writeError(outErr, "\(error)")
        return 2
    }
    #else
    writeError(outErr, "Vision framework not available on this build")
    return 3
    #endif
}

#if canImport(Vision)

// Configuration knobs accepted from the Rust caller.
private struct OcrConfig {
    var fastPath: Bool = false       // VNRequestTextRecognitionLevel.fast vs accurate
    var languages: [String] = []     // BCP-47, e.g. ["en-US"]; empty = auto
    var languageCorrection: Bool = true
    var minimumTextHeight: Float = 0 // normalized; 0 = no floor

    static func parse(_ json: String) -> OcrConfig {
        var cfg = OcrConfig()
        guard let data = json.data(using: .utf8),
              let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            return cfg
        }
        if let v = obj["fast_path"] as? Bool { cfg.fastPath = v }
        if let v = obj["languages"] as? [String] { cfg.languages = v }
        if let v = obj["language_correction"] as? Bool { cfg.languageCorrection = v }
        if let v = obj["minimum_text_height"] as? Double { cfg.minimumTextHeight = Float(v) }
        return cfg
    }
}

private struct OcrObservation {
    let text: String
    let confidence: Float
    let x: Double
    let y: Double
    let w: Double
    let h: Double
}

private enum VisionError: Error, CustomStringConvertible {
    case imageOpenFailed(String)
    case requestFailed(String)

    var description: String {
        switch self {
        case .imageOpenFailed(let m): return "open image failed: \(m)"
        case .requestFailed(let m): return "vision request failed: \(m)"
        }
    }
}

private func loadCGImage(path: String) throws -> CGImage {
    let url = URL(fileURLWithPath: path)
    guard let src = CGImageSourceCreateWithURL(url as CFURL, nil) else {
        throw VisionError.imageOpenFailed("CGImageSourceCreateWithURL returned nil for \(path)")
    }
    guard let img = CGImageSourceCreateImageAtIndex(src, 0, nil) else {
        throw VisionError.imageOpenFailed("CGImageSourceCreateImageAtIndex returned nil for \(path)")
    }
    // Color-space note: Vision normalizes incoming images to its own
    // working space, so passing the raw CGImage with whatever ICC
    // profile the file declared (sRGB, Display P3, Adobe RGB, …) is
    // generally correct. There's a documented Vision edge case where
    // wide-gamut screenshots can produce subtly different OCR
    // bounding boxes than their sRGB-converted equivalents — if a
    // reproducer surfaces, redraw `img` into a `CGColorSpace.sRGB`
    // bitmap context here before returning. Doing the redraw
    // unconditionally costs ~50ms/MP on a 4K screenshot, so it's
    // gated on a real reproducer rather than precautionary.
    return img
}

private func recognizeText(image: CGImage, config: OcrConfig) throws -> [OcrObservation] {
    let request = VNRecognizeTextRequest()
    request.recognitionLevel = config.fastPath ? .fast : .accurate
    request.usesLanguageCorrection = config.languageCorrection
    if !config.languages.isEmpty {
        request.recognitionLanguages = config.languages
    }
    if config.minimumTextHeight > 0 {
        request.minimumTextHeight = config.minimumTextHeight
    }

    let handler = VNImageRequestHandler(cgImage: image, options: [:])
    do {
        try handler.perform([request])
    } catch {
        throw VisionError.requestFailed("\(error)")
    }

    guard let results = request.results else { return [] }
    return results.compactMap { obs -> OcrObservation? in
        guard let candidate = obs.topCandidates(1).first else { return nil }
        let bbox = obs.boundingBox
        return OcrObservation(
            text: candidate.string,
            confidence: candidate.confidence,
            x: Double(bbox.origin.x),
            y: Double(bbox.origin.y),
            w: Double(bbox.size.width),
            h: Double(bbox.size.height)
        )
    }
}

private func encodeObservations(_ observations: [OcrObservation]) -> String {
    let arr: [[String: Any]] = observations.map { obs in
        [
            "text": obs.text,
            "confidence": obs.confidence,
            "x": obs.x,
            "y": obs.y,
            "w": obs.w,
            "h": obs.h,
        ]
    }
    if let data = try? JSONSerialization.data(withJSONObject: arr, options: []),
       let str = String(data: data, encoding: .utf8) {
        return str
    }
    return "[]"
}

// MARK: - Face detection
//
// Synchronous face rectangle detection via `VNDetectFaceRectanglesRequest`.
// Returns one observation per detected face: confidence + normalized
// bounding box. Identity / landmark detection are NOT included —
// those have privacy implications that are an agent-bundle
// capability decision, not a default.

private struct FaceObservation {
    let confidence: Float
    let x: Double
    let y: Double
    let w: Double
    let h: Double
}

private func detectFaces(image: CGImage) throws -> [FaceObservation] {
    let request = VNDetectFaceRectanglesRequest()
    let handler = VNImageRequestHandler(cgImage: image, options: [:])
    do {
        try handler.perform([request])
    } catch {
        throw VisionError.requestFailed("\(error)")
    }
    guard let results = request.results else { return [] }
    return results.map { obs in
        let bbox = obs.boundingBox
        return FaceObservation(
            confidence: obs.confidence,
            x: Double(bbox.origin.x),
            y: Double(bbox.origin.y),
            w: Double(bbox.size.width),
            h: Double(bbox.size.height)
        )
    }
}

private func encodeFaces(_ observations: [FaceObservation]) -> String {
    let arr: [[String: Any]] = observations.map { obs in
        [
            "confidence": obs.confidence,
            "x": obs.x,
            "y": obs.y,
            "w": obs.w,
            "h": obs.h,
        ]
    }
    if let data = try? JSONSerialization.data(withJSONObject: arr, options: []),
       let str = String(data: data, encoding: .utf8) {
        return str
    }
    return "[]"
}

// MARK: - Barcode / QR detection

private struct BarcodeObservation {
    let payload: String
    let symbology: String
    let confidence: Float
    let x: Double
    let y: Double
    let w: Double
    let h: Double
}

private func detectBarcodes(image: CGImage) throws -> [BarcodeObservation] {
    let request = VNDetectBarcodesRequest()
    let handler = VNImageRequestHandler(cgImage: image, options: [:])
    do {
        try handler.perform([request])
    } catch {
        throw VisionError.requestFailed("\(error)")
    }
    guard let results = request.results else { return [] }
    return results.compactMap { obs in
        let bbox = obs.boundingBox
        // payloadStringValue is the decoded text/URL/etc.; nil for
        // unsupported symbologies. Skip those.
        guard let payload = obs.payloadStringValue else { return nil }
        return BarcodeObservation(
            payload: payload,
            symbology: obs.symbology.rawValue,
            confidence: obs.confidence,
            x: Double(bbox.origin.x),
            y: Double(bbox.origin.y),
            w: Double(bbox.size.width),
            h: Double(bbox.size.height)
        )
    }
}

private func encodeBarcodes(_ observations: [BarcodeObservation]) -> String {
    let arr: [[String: Any]] = observations.map { obs in
        [
            "payload": obs.payload,
            "symbology": obs.symbology,
            "confidence": obs.confidence,
            "x": obs.x,
            "y": obs.y,
            "w": obs.w,
            "h": obs.h,
        ]
    }
    if let data = try? JSONSerialization.data(withJSONObject: arr, options: []),
       let str = String(data: data, encoding: .utf8) {
        return str
    }
    return "[]"
}

// MARK: - Image classification
//
// Apple's built-in image classifier ranks ~1300 categories.
// `topK` caps the number of results returned per image; pass 0 for
// "all". Confidence is in [0, 1].

private struct ClassifyObservation {
    let identifier: String
    let confidence: Float
}

private func classifyImage(image: CGImage, topK: Int) throws -> [ClassifyObservation] {
    let request = VNClassifyImageRequest()
    let handler = VNImageRequestHandler(cgImage: image, options: [:])
    do {
        try handler.perform([request])
    } catch {
        throw VisionError.requestFailed("\(error)")
    }
    guard let results = request.results else { return [] }
    let sorted = results.sorted { $0.confidence > $1.confidence }
    let limit = topK > 0 ? min(topK, sorted.count) : sorted.count
    return sorted.prefix(limit).map { obs in
        ClassifyObservation(identifier: obs.identifier, confidence: obs.confidence)
    }
}

private func encodeClassifications(_ observations: [ClassifyObservation]) -> String {
    let arr: [[String: Any]] = observations.map { obs in
        [
            "identifier": obs.identifier,
            "confidence": obs.confidence,
        ]
    }
    if let data = try? JSONSerialization.data(withJSONObject: arr, options: []),
       let str = String(data: data, encoding: .utf8) {
        return str
    }
    return "[]"
}

#endif // canImport(Vision)

@_cdecl("car_vision_detect_faces")
public func car_vision_detect_faces(
    _ imagePathC: UnsafePointer<CChar>?,
    _ outJson: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?,
    _ outErr: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
) -> Int32 {
    guard let imagePathC = imagePathC else {
        writeError(outErr, "image path is null")
        return 1
    }
    let imagePath = String(cString: imagePathC)

    #if canImport(Vision)
    do {
        let image = try loadCGImage(path: imagePath)
        let observations = try detectFaces(image: image)
        let json = encodeFaces(observations)
        if let outJson = outJson {
            outJson.pointee = cString(json)
        }
        return 0
    } catch {
        writeError(outErr, "\(error)")
        return 2
    }
    #else
    writeError(outErr, "Vision framework not available on this build")
    return 3
    #endif
}

@_cdecl("car_vision_detect_barcodes")
public func car_vision_detect_barcodes(
    _ imagePathC: UnsafePointer<CChar>?,
    _ outJson: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?,
    _ outErr: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
) -> Int32 {
    guard let imagePathC = imagePathC else {
        writeError(outErr, "image path is null")
        return 1
    }
    let imagePath = String(cString: imagePathC)

    #if canImport(Vision)
    do {
        let image = try loadCGImage(path: imagePath)
        let observations = try detectBarcodes(image: image)
        let json = encodeBarcodes(observations)
        if let outJson = outJson {
            outJson.pointee = cString(json)
        }
        return 0
    } catch {
        writeError(outErr, "\(error)")
        return 2
    }
    #else
    writeError(outErr, "Vision framework not available on this build")
    return 3
    #endif
}

@_cdecl("car_vision_classify_image")
public func car_vision_classify_image(
    _ imagePathC: UnsafePointer<CChar>?,
    _ topK: Int32,
    _ outJson: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?,
    _ outErr: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
) -> Int32 {
    guard let imagePathC = imagePathC else {
        writeError(outErr, "image path is null")
        return 1
    }
    let imagePath = String(cString: imagePathC)

    #if canImport(Vision)
    do {
        let image = try loadCGImage(path: imagePath)
        let observations = try classifyImage(image: image, topK: Int(topK))
        let json = encodeClassifications(observations)
        if let outJson = outJson {
            outJson.pointee = cString(json)
        }
        return 0
    } catch {
        writeError(outErr, "\(error)")
        return 2
    }
    #else
    writeError(outErr, "Vision framework not available on this build")
    return 3
    #endif
}
