import Foundation

// URLSession-backed HTTP client. One instance per active account: the
// watch's home is "look at one feed for one account". Account switches
// rebuild the client.
//
// Caching: in-memory + on-disk URLCache, scoped by accountId so signing
// out wipes the cached responses for that account. Tapestry bucket
// responses are short-lived (<1 minute), home timeline pages are
// immutable past their max_id so caching them aggressively is fine —
// the server's Cache-Control headers govern.

actor WatchAPIClient {
    private let session: URLSession
    private let decoder: JSONDecoder
    // `nonisolated let` so `AuthState.apply` (running on the main actor)
    // can call `clearCache()` synchronously when rebuilding a client on
    // account switch. URLCache itself is documented thread-safe.
    nonisolated let urlCache: URLCache

    let instance: String
    let accessToken: String
    let accountType: String

    init(instance: String, accessToken: String, accountType: String) {
        self.instance = instance
        self.accessToken = accessToken
        self.accountType = accountType
        let config = URLSessionConfiguration.default
        config.requestCachePolicy = .useProtocolCachePolicy
        config.timeoutIntervalForRequest = 12
        config.timeoutIntervalForResource = 30
        let cache = URLCache(
            memoryCapacity: 10 * 1024 * 1024,
            diskCapacity: 50 * 1024 * 1024,
            diskPath: "watch-api-\(instance)"
        )
        config.urlCache = cache
        self.urlCache = cache
        self.session = URLSession(configuration: config)
        self.decoder = JSONDecoder()
    }

    /// Drop every cached response. Called from `AuthState.apply` on
    /// account switch when the previous and new instances match, so a
    /// stale `Authorization`-bound response from the old token can't be
    /// served to the new one. Cross-instance switches don't need this:
    /// each instance gets its own `diskPath` and the new client starts
    /// against an empty in-memory cache.
    nonisolated func clearCache() {
        urlCache.removeAllCachedResponses()
    }

    func request<T: Decodable>(
        _ url: URL,
        method: String = "GET",
        body: Data? = nil,
        contentType: String? = nil,
        decode: T.Type
    ) async throws -> (value: T, response: HTTPURLResponse) {
        var req = URLRequest(url: url)
        req.httpMethod = method
        for (k, v) in HTTPHeaders.authed(token: accessToken, accountType: accountType) {
            req.setValue(v, forHTTPHeaderField: k)
        }
        if let contentType {
            req.setValue(contentType, forHTTPHeaderField: "Content-Type")
        }
        if let body {
            req.httpBody = body
        }
        let start = await MainActor.run {
            DebugRecorder.shared.requestStart(method: method, url: url)
        }
        let data: Data
        let resp: URLResponse
        do {
            (data, resp) = try await session.data(for: req)
        } catch {
            let msg = error.localizedDescription
            await MainActor.run {
                DebugRecorder.shared.requestFailure(start: start, error: "transport: \(msg)")
            }
            throw WatchAPIError.transport(error)
        }
        guard let http = resp as? HTTPURLResponse else {
            await MainActor.run {
                DebugRecorder.shared.requestFailure(start: start, error: "non-HTTP response")
            }
            throw WatchAPIError.http(0, nil)
        }
        guard (200..<300).contains(http.statusCode) else {
            let body = String(data: data, encoding: .utf8)
            await MainActor.run {
                let snippet = body.map { String($0.prefix(60)) } ?? ""
                DebugRecorder.shared.requestFailure(
                    start: start,
                    error: "http \(http.statusCode) \(snippet)"
                )
            }
            throw WatchAPIError.http(http.statusCode, body)
        }
        do {
            let decoded = try decoder.decode(T.self, from: data)
            await MainActor.run {
                DebugRecorder.shared.requestSuccess(start: start, status: http.statusCode)
            }
            return (decoded, http)
        } catch {
            await MainActor.run {
                DebugRecorder.shared.requestFailure(
                    start: start,
                    error: "decode: \(error.localizedDescription)"
                )
            }
            throw WatchAPIError.decode(error)
        }
    }

    /// Fetches raw image bytes through the same `URLSession` as the
    /// authenticated routes — same User-Agent, timeouts, on-disk
    /// URLCache. Logs through `DebugRecorder.img*` so failure
    /// observability stays unified (one log surface, not two parallel
    /// AsyncImage + probe paths). Replaces the retired
    /// `ProbedAsyncImage`: when the primary fetch is the diagnostic,
    /// the parallel probe is obsolete.
    ///
    /// Two failure shapes worth preserving in the log line:
    ///   - HTTP non-2xx → `imgFailure(status: ...)` so 403/404/5xx is
    ///     greppable as "server said no" vs "transport said no".
    ///   - URLError → prefix the log with `URLError(<code.rawValue>)`
    ///     so transport classes (TLS, timeout, ECONNRESET) are
    ///     discriminable from each other; the platform's
    ///     `localizedDescription` alone collapses them all into the
    ///     same content-free English string.
    ///
    /// Cancellation (URLError.cancelled or Swift's CancellationError)
    /// is NOT logged as a failure — it's the cell-scrolled-off path.
    /// The caller (`MediaImageStore.load`) maps it back to `.idle`.
    func fetchImage(url: URL) async throws -> Data {
        var req = URLRequest(url: url)
        req.setValue("image/*", forHTTPHeaderField: "Accept")
        req.setValue(Branding.userAgent, forHTTPHeaderField: "User-Agent")
        let start = await MainActor.run {
            DebugRecorder.shared.imgStart(url: url)
        }
        let data: Data
        let resp: URLResponse
        do {
            (data, resp) = try await session.data(for: req)
        } catch {
            let isCancellation =
                error is CancellationError
                || (error as? URLError)?.code == .cancelled
            if !isCancellation {
                let msg: String
                if let u = error as? URLError {
                    msg = "URLError(\(u.code.rawValue)) \(u.localizedDescription)"
                } else {
                    msg = error.localizedDescription
                }
                await MainActor.run {
                    DebugRecorder.shared.imgFailure(
                        start: start,
                        error: msg,
                        status: nil,
                        url: url
                    )
                }
            }
            throw error
        }
        let status = (resp as? HTTPURLResponse)?.statusCode ?? 0
        guard (200..<300).contains(status) else {
            await MainActor.run {
                DebugRecorder.shared.imgFailure(
                    start: start,
                    error: "http \(status)",
                    status: status,
                    url: url
                )
            }
            throw WatchAPIError.http(status, nil)
        }
        await MainActor.run {
            DebugRecorder.shared.imgSuccess(start: start, status: status, url: url)
        }
        return data
    }

    func raw(
        _ url: URL,
        method: String = "GET",
        body: Data? = nil,
        contentType: String? = nil
    ) async throws -> (data: Data, response: HTTPURLResponse) {
        var req = URLRequest(url: url)
        req.httpMethod = method
        for (k, v) in HTTPHeaders.authed(token: accessToken, accountType: accountType) {
            req.setValue(v, forHTTPHeaderField: k)
        }
        if let contentType {
            req.setValue(contentType, forHTTPHeaderField: "Content-Type")
        }
        if let body {
            req.httpBody = body
        }
        let start = await MainActor.run {
            DebugRecorder.shared.requestStart(method: method, url: url)
        }
        let data: Data
        let resp: URLResponse
        do {
            (data, resp) = try await session.data(for: req)
        } catch {
            let msg = error.localizedDescription
            await MainActor.run {
                DebugRecorder.shared.requestFailure(start: start, error: "transport: \(msg)")
            }
            throw WatchAPIError.transport(error)
        }
        guard let http = resp as? HTTPURLResponse else {
            await MainActor.run {
                DebugRecorder.shared.requestFailure(start: start, error: "non-HTTP response")
            }
            throw WatchAPIError.http(0, nil)
        }
        guard (200..<300).contains(http.statusCode) else {
            let body = String(data: data, encoding: .utf8)
            await MainActor.run {
                let snippet = body.map { String($0.prefix(60)) } ?? ""
                DebugRecorder.shared.requestFailure(
                    start: start,
                    error: "http \(http.statusCode) \(snippet)"
                )
            }
            throw WatchAPIError.http(http.statusCode, body)
        }
        await MainActor.run {
            DebugRecorder.shared.requestSuccess(start: start, status: http.statusCode)
        }
        return (data, http)
    }
}
