import CryptoKit
import Foundation

enum ImitationAssessorError: LocalizedError {
    case invalidHash
    case cannotReadRecording

    var errorDescription: String? {
        switch self {
        case .invalidHash:
            return "视频 id 必须是 64 位 SHA-256 十六进制字符串"
        case .cannotReadRecording:
            return "无法读取录音文件"
        }
    }
}

struct DubShareUploadRequest {
    let videoHash: String
    let title: String
    let segments: [(index: Int, audioURL: URL)]
    let scores: [Int: ImitationAssessmentResult]
}

enum ImitationAssessor {
    /// 分块计算文件的 SHA-256（与安卓端一致，适合大视频文件）。
    static func sha256Hex(of fileURL: URL) throws -> String {
        let handle: FileHandle
        do {
            handle = try FileHandle(forReadingFrom: fileURL)
        } catch {
            throw ImitationAssessorError.cannotReadRecording
        }
        defer { try? handle.close() }
        var hasher = SHA256()
        while true {
            guard let data = try? handle.read(upToCount: 1 << 20), !data.isEmpty else {
                break
            }
            hasher.update(data: data)
        }
        return hasher.finalize().map { String(format: "%02x", $0) }.joined()
    }

    /// 复制到缓存目录的同时计算 SHA-256（单次读取完成两件事）。
    static func copyAndHash(from source: URL, to destination: URL) throws -> String {
        let input: FileHandle
        do {
            input = try FileHandle(forReadingFrom: source)
        } catch {
            throw ImitationAssessorError.cannotReadRecording
        }
        defer { try? input.close() }
        try? FileManager.default.removeItem(at: destination)
        FileManager.default.createFile(atPath: destination.path, contents: nil)
        let output = try FileHandle(forWritingTo: destination)
        defer { try? output.close() }
        var hasher = SHA256()
        while true {
            guard let data = try input.read(upToCount: 1 << 20), !data.isEmpty else {
                break
            }
            hasher.update(data: data)
            try output.write(contentsOf: data)
        }
        try output.synchronize()
        return hasher.finalize().map { String(format: "%02x", $0) }.joined()
    }

    /// 上传一句录音到服务端 MOSS 评测接口（multipart/form-data，与安卓端字段一致）。
    static func assess(request: ImitationAssessmentRequest) async throws -> ImitationAssessmentResult {
        let hash = request.mediaId.lowercased()
        guard hash.range(of: AppConfig.sha256Pattern, options: .regularExpression) != nil else {
            throw ImitationAssessorError.invalidHash
        }
        let path = "api/v1/videos/\(hash)/sentences/\(request.sentence.index)/assessments"
        let boundary = "----OralTrainer-\(UUID().uuidString)"
        var body = Data()
        appendField(&body, boundary: boundary, name: "language", value: request.locale ?? request.sentence.language ?? "")
        let recordingData: Data
        do {
            recordingData = try Data(contentsOf: request.recordingURL)
        } catch {
            throw ImitationAssessorError.cannotReadRecording
        }
        let filename = request.recordingURL.lastPathComponent
        let mimeType = request.recordingURL.pathExtension.lowercased() == "wav" ? "audio/wav" : "audio/m4a"
        appendFile(&body, boundary: boundary, name: "audio", filename: filename, mimeType: mimeType, data: recordingData)
        body.append(Data("--\(boundary)--\r\n".utf8))

        let result = try await HTTPClient.execute(
            baseURL: AppConfig.serverBaseURL,
            path: path,
            method: "POST",
            timeout: AppConfig.assessmentTimeout,
            headers: [
                "Accept": "application/json",
                "Content-Type": "multipart/form-data; boundary=\(boundary)",
                "X-Client-Key": AppConfig.assessmentAPIKey,
            ],
            body: body
        )
        guard (200...299).contains(result.statusCode) else {
            let responseBody = String(data: result.body, encoding: .utf8) ?? ""
            throw HTTPClientError.badStatus(result.statusCode, responseBody)
        }
        return try JSONDecoder().decode(ImitationAssessmentResult.self, from: result.body)
    }

    static func uploadDubShare(request: DubShareUploadRequest) async throws -> String {
        let hash = request.videoHash.lowercased()
        guard hash.range(of: AppConfig.sha256Pattern, options: .regularExpression) != nil else {
            throw ImitationAssessorError.invalidHash
        }
        guard !request.segments.isEmpty else {
            throw ImitationAssessorError.cannotReadRecording
        }

        let boundary = "----OralTrainer-\(UUID().uuidString)"
        var body = Data()
        appendField(&body, boundary: boundary, name: "video_hash", value: hash)
        appendField(&body, boundary: boundary, name: "title", value: request.title.isEmpty ? "我的口语配音" : request.title)

        let segmentItems = request.segments
            .sorted { $0.index < $1.index }
            .map { "{\"sentence_index\":\($0.index)}" }
        appendField(&body, boundary: boundary, name: "segments", value: "[\(segmentItems.joined(separator: ","))]")

        let scoreItems = request.segments
            .sorted { $0.index < $1.index }
            .compactMap { segment -> String? in
                guard let score = request.scores[segment.index] else { return nil }
                var fields = ["\"sentence_index\":\(segment.index)"]
                for (key, value) in [
                    ("overall_score", score.overallScore as Float?),
                    ("content_score", score.contentScore),
                    ("fluency_score", score.fluencyScore),
                    ("duration_score", score.durationScore),
                    ("pause_score", score.pauseScore),
                    ("speech_rate_score", score.speechRateScore),
                ] {
                    if let value = value {
                        fields.append("\"\(key)\":\(value)")
                    }
                }
                if let text = score.recognizedText {
                    let escaped = text
                        .replacingOccurrences(of: "\\", with: "\\\\")
                        .replacingOccurrences(of: "\"", with: "\\\"")
                    fields.append("\"recognized_text\":\"\(escaped)\"")
                }
                return "{\(fields.joined(separator: ","))}"
            }
        appendField(&body, boundary: boundary, name: "scores", value: "[\(scoreItems.joined(separator: ","))]")

        for segment in request.segments.sorted(by: { $0.index < $1.index }) {
            let audioData: Data
            do {
                audioData = try Data(contentsOf: segment.audioURL)
            } catch {
                throw ImitationAssessorError.cannotReadRecording
            }
            appendFile(
                &body,
                boundary: boundary,
                name: "files",
                filename: "dub-\(segment.index).m4a",
                mimeType: "audio/mp4",
                data: audioData
            )
        }
        body.append(Data("--\(boundary)--\r\n".utf8))

        let result = try await HTTPClient.execute(
            baseURL: AppConfig.serverBaseURL,
            path: "api/v1/dub-shares",
            method: "POST",
            timeout: 120,
            headers: [
                "Accept": "application/json",
                "Content-Type": "multipart/form-data; boundary=\(boundary)",
            ],
            body: body
        )
        guard (200...299).contains(result.statusCode) else {
            let responseBody = String(data: result.body, encoding: .utf8) ?? ""
            throw HTTPClientError.badStatus(result.statusCode, responseBody)
        }
        struct ShareResponse: Decodable {
            let shareId: String
            private enum CodingKeys: String, CodingKey {
                case shareId = "share_id"
            }
        }
        let response = try JSONDecoder().decode(ShareResponse.self, from: result.body)
        let base = AppConfig.serverBaseURL.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
        return "\(base)/dub-shares/\(response.shareId)"
    }

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

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