import Combine
import Foundation

/// Sends a finished recording to Steward.
///
/// A BACKGROUND URLSession, deliberately. A watch app is suspended the moment
/// you drop your wrist, and a foreground request dies with it — which on a
/// watch is the common case, not the edge case. A background upload task is
/// handed to the system, survives the app being suspended or killed, and
/// retries on its own when the watch next has a route to the phone or WiFi.
///
/// That is also why the body is written to a file first: background uploads
/// must be fromFile, never from Data.
final class Uploader: NSObject, ObservableObject {
    @Published private(set) var state: State = .idle

    enum State: Equatable {
        case idle
        case uploading
        case queued          // handed to the system; it will finish without us
        case failed(String)
    }

    static let shared = Uploader()

    private lazy var session: URLSession = {
        let config = URLSessionConfiguration.background(withIdentifier: "com.erictran.stewardwatch.upload")
        config.isDiscretionary = false          // send now, not when the system feels like it
        config.sessionSendsLaunchEvents = true  // wake us to report the result
        return URLSession(configuration: config, delegate: self, delegateQueue: nil)
    }()

    /// Build the multipart body on disk and hand it off. Returns once queued,
    /// not once delivered — the system owns it from here.
    func upload(_ audioURL: URL, durationMs: Int) {
        guard Config.captureToken != "PASTE_YOUR_CAPTURE_TOKEN_HERE", !Config.captureToken.isEmpty else {
            state = .failed("No token set in Config.swift")
            return
        }
        state = .uploading
        do {
            let boundary = "Boundary-\(UUID().uuidString)"
            let bodyURL = FileManager.default.temporaryDirectory
                .appendingPathComponent("upload-\(UUID().uuidString).multipart")

            let audio = try Data(contentsOf: audioURL)
            var body = Data()
            func append(_ s: String) { body.append(s.data(using: .utf8)!) }

            // Duration first: Steward reads it off the form, and a short field
            // ahead of a large one keeps the part boundaries easy to eyeball when
            // something goes wrong.
            append("--\(boundary)\r\n")
            append("Content-Disposition: form-data; name=\"durationMs\"\r\n\r\n")
            append("\(durationMs)\r\n")

            append("--\(boundary)\r\n")
            append("Content-Disposition: form-data; name=\"audio\"; filename=\"\(audioURL.lastPathComponent)\"\r\n")
            append("Content-Type: audio/mp4\r\n\r\n")
            body.append(audio)
            append("\r\n--\(boundary)--\r\n")
            try body.write(to: bodyURL)

            var req = URLRequest(url: Config.endpoint)
            req.httpMethod = "POST"
            req.setValue("Bearer \(Config.captureToken)", forHTTPHeaderField: "Authorization")
            req.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

            let task = session.uploadTask(with: req, fromFile: bodyURL)
            task.taskDescription = bodyURL.path   // so the delegate can clean up
            task.resume()

            // The recording is the system's problem now; ours is already copied.
            try? FileManager.default.removeItem(at: audioURL)
            DispatchQueue.main.async { self.state = .queued }
        } catch {
            DispatchQueue.main.async { self.state = .failed(error.localizedDescription) }
        }
    }
}

extension Uploader: URLSessionTaskDelegate {
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let path = task.taskDescription {
            try? FileManager.default.removeItem(at: URL(fileURLWithPath: path))
        }
        DispatchQueue.main.async {
            if let error {
                self.state = .failed(error.localizedDescription)
                return
            }
            let code = (task.response as? HTTPURLResponse)?.statusCode ?? 0
            switch code {
            case 200...299: self.state = .idle          // Steward will notify; nothing more to show
            case 401:       self.state = .failed("Token rejected")
            default:        self.state = .failed("Server said \(code)")
            }
        }
    }
}
