import Foundation

// Tapestry-specific API surface. Both the bucket-tail (3s polling) and
// the page-fetch (Digital Crown scroll-back) are GETs returning a list
// of `{ bucket, letters }` objects. Emit is a POST of 1–256 letters in
// `text/plain` body with the canonical alphabet — see TapestryLetters.

/// Distinguishes the two interesting failure modes of the `since`
/// endpoint from generic transport / decode errors so the caller can
/// pick the right fallback path. Hard-coded server cap (matches
/// `TapestryController::MAX_SINCE_MINUTES`).
enum TapestrySinceError: Error {
    /// 404 — server hasn't deployed `/api/v1/tapestry/since` yet.
    /// Caller should fall back to the per-page forward walk.
    case notSupported
    /// 422 `since_too_old` — caller should drop the persisted cache
    /// and re-bootstrap from `currentPage`. 48 h server-side cap.
    case tooOld
}

extension WatchAPIClient {
    static let maxSinceMinutes = 2880

    func fetchTapestryBucket(_ minute: String) async throws -> [TapestryBucketResponse] {
        guard let url = Endpoints.tapestryBucket(instance, minute: minute) else {
            throw WatchAPIError.invalidURL
        }
        let (value, _) = try await request(url, decode: [TapestryBucketResponse].self)
        return value
    }

    func fetchTapestryPage(_ page: String) async throws -> [TapestryBucketResponse] {
        guard let url = Endpoints.tapestryPage(instance, page: page) else {
            throw WatchAPIError.invalidURL
        }
        let (value, _) = try await request(url, decode: [TapestryBucketResponse].self)
        return value
    }

    /// Bulk gap-fill from `bucket` (inclusive) through `now` in one
    /// MGET-backed round-trip. Throws `TapestrySinceError.notSupported`
    /// on 404 (older server) and `TapestrySinceError.tooOld` on the
    /// 422 `since_too_old` payload (cache exceeds 48 h staleness).
    func fetchTapestrySince(_ bucket: String) async throws -> [TapestryBucketResponse] {
        guard let url = Endpoints.tapestrySince(instance, bucket: bucket) else {
            throw WatchAPIError.invalidURL
        }
        do {
            let (value, _) = try await request(url, decode: [TapestryBucketResponse].self)
            return value
        } catch WatchAPIError.http(404, _) {
            throw TapestrySinceError.notSupported
        } catch WatchAPIError.http(422, let body) {
            // Server emits `{"error":"since_too_old","max_minutes":2880}`.
            // String-match for the discriminator — the body shape is a
            // stable contract; a parse here would be overkill.
            if let body, body.contains("since_too_old") {
                throw TapestrySinceError.tooOld
            }
            throw WatchAPIError.http(422, body)
        }
    }

    /// Fire-and-forget tapestry letter emit. The phone-side `useLikeMutation`
    /// emits `'L'` / `'u'` per gesture; the watch mirrors that without the
    /// 5s/50-letter batching since its volume is "one letter per long-press".
    /// Errors are swallowed by callers (the weave can tolerate occasional
    /// drop; UX never blocks on this).
    func emitTapestryLetter(_ letter: Character) async throws {
        guard TapestryLetters.alphabetSet.contains(letter) else {
            // Caller bug; refuse rather than poison the weave.
            throw WatchAPIError.invalidURL
        }
        guard let url = Endpoints.tapestryPost(instance) else {
            throw WatchAPIError.invalidURL
        }
        let body = String(letter).data(using: .utf8) ?? Data()
        _ = try await raw(url, method: "POST", body: body, contentType: "text/plain")
    }
}
