import Foundation

enum OpenAITranscriptionClientError: Error {
    case invalidResponse
    case httpStatus(Int, String)
    case missingTranscript
}

final class OpenAITranscriptionClient {
    private let session: URLSession

    init(session: URLSession = .shared) {
        self.session = session
    }

    func transcribeAudioFile(fileURL: URL, apiKey: String) async throws -> String {
        let boundary = "Boundary-\(UUID().uuidString)"
        var request = URLRequest(url: URL(string: "https://api.openai.com/v1/audio/transcriptions")!)
        request.httpMethod = "POST"
        request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
        request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
        request.httpBody = try makeMultipartBody(fileURL: fileURL, boundary: boundary)

        let (data, response) = try await session.data(for: request)
        guard let http = response as? HTTPURLResponse else {
            throw OpenAITranscriptionClientError.invalidResponse
        }
        guard (200..<300).contains(http.statusCode) else {
            let body = String(data: data, encoding: .utf8) ?? "<no body>"
            throw OpenAITranscriptionClientError.httpStatus(http.statusCode, body)
        }

        struct TranscriptionResponse: Decodable {
            let text: String?
        }

        let decoded = try JSONDecoder().decode(TranscriptionResponse.self, from: data)
        guard let text = decoded.text?.trimmingCharacters(in: .whitespacesAndNewlines), text.isEmpty == false else {
            throw OpenAITranscriptionClientError.missingTranscript
        }
        return text
    }

    private func makeMultipartBody(fileURL: URL, boundary: String) throws -> Data {
        let fileData = try Data(contentsOf: fileURL)
        var body = Data()
        let filename = fileURL.lastPathComponent
        let mimeType = "audio/mp4"

        appendField(name: "model", value: "gpt-4o-mini-transcribe", boundary: boundary, into: &body)
        appendField(name: "response_format", value: "json", boundary: boundary, into: &body)
        appendFile(name: "file", filename: filename, mimeType: mimeType, data: fileData, boundary: boundary, into: &body)
        body.append("--\(boundary)--\r\n".data(using: .utf8) ?? Data())
        return body
    }

    private func appendField(name: String, value: String, boundary: String, into body: inout Data) {
        body.append("--\(boundary)\r\n".data(using: .utf8) ?? Data())
        body.append("Content-Disposition: form-data; name=\"\(name)\"\r\n\r\n".data(using: .utf8) ?? Data())
        body.append("\(value)\r\n".data(using: .utf8) ?? Data())
    }

    private func appendFile(name: String, filename: String, mimeType: String, data: Data, boundary: String, into body: inout Data) {
        body.append("--\(boundary)\r\n".data(using: .utf8) ?? Data())
        body.append("Content-Disposition: form-data; name=\"\(name)\"; filename=\"\(filename)\"\r\n".data(using: .utf8) ?? Data())
        body.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8) ?? Data())
        body.append(data)
        body.append("\r\n".data(using: .utf8) ?? Data())
    }
}

extension OpenAITranscriptionClientError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .invalidResponse:
            return "OpenAI transcription returned an invalid response."
        case let .httpStatus(status, body):
            return "OpenAI transcription failed (\(status)): \(body)"
        case .missingTranscript:
            return "OpenAI transcription returned no text."
        }
    }
}
