import SwiftUI
import UIKit

// Per-cell image loader for the home feed. AsyncImage exposes failure
// only inside the view body (no callback, no binding), which makes
// "filter posts whose image failed" impossible at the model layer.
// The documented escape hatch — Apple's UIKit-era guide *Asynchronously
// loading images into table and collection views* — is to fetch via
// URLSession into a model, decode to UIImage, hand the image to the
// cell, and cancel per-cell on disappear. That's this type.
//
// Two-tier cache: WatchAPIClient's URLSession already owns a 50 MB
// on-disk URLCache per instance (shared with the authenticated routes,
// so UA / timeouts match). On top of that, `decoded` keeps the latest
// UIImage decode in memory so re-entering a cell doesn't pay the JPEG
// decode again. NSCache auto-purges under memory pressure — exactly
// the eviction profile a 49 mm Watch needs.
//
// @Observable per Apple's recommended migration path (watchOS 10+;
// per-property dependency tracking). The watch's older models
// (AuthState, DebugRecorder, TapestryFeedModel) stay on
// ObservableObject — migrate them if perf profiling ever justifies
// the consumer-side churn. The `states` dictionary is a single
// @Observable property, so any mutation re-invalidates every cell
// reading `state(for:)`. With the 20-cell home-feed cap that's
// cheap; splitting into per-row reference types is a follow-up if
// profiling on a 49 mm device says otherwise.

@Observable
@MainActor
final class MediaImageStore {
    enum LoadState: Equatable {
        case idle
        case loading
        case loaded(UIImage)
        case failed
    }

    private(set) var states: [String: LoadState] = [:]
    // Decoded-image cache is a perf concern, not a UI input — keep it
    // out of the @Observable surface so cache fills don't churn views.
    @ObservationIgnored private let decoded = NSCache<NSString, UIImage>()

    func state(for id: String) -> LoadState { states[id] ?? .idle }

    /// Wipe both tiers. Called on account switch from MediaFeedView so
    /// the previous account's decoded thumbnails don't sit in memory
    /// after a credential rotation (Pixelfed media URLs are content-
    /// hashed and cross-account-safe by URL, but the per-statusId state
    /// map is account-scoped and shouldn't leak).
    func reset() {
        states.removeAll()
        decoded.removeAllObjects()
    }

    /// Fetch + decode for one status. Idempotent: a duplicate `.task`
    /// firing while a fetch is in flight is a no-op against the
    /// already-loading state; a hit on the decoded cache short-circuits
    /// straight to `.loaded`.
    func load(statusId: String, url: URL, via client: WatchAPIClient) async {
        if let image = decoded.object(forKey: statusId as NSString) {
            if case .loaded = states[statusId] {
                return
            }
            states[statusId] = .loaded(image)
            return
        }
        if case .loading = states[statusId] { return }
        states[statusId] = .loading
        do {
            let data = try await client.fetchImage(url: url)
            guard let image = UIImage(data: data) else {
                states[statusId] = .failed
                return
            }
            decoded.setObject(image, forKey: statusId as NSString, cost: data.count)
            states[statusId] = .loaded(image)
        } catch {
            // URLSession surfaces task cancellation as URLError.cancelled,
            // not Swift's CancellationError. Treat both the same: drop
            // back to .idle so a cell re-entry retries cleanly. Anything
            // else is a real failure that should bubble into the
            // renderable filter and remove the post from the list.
            let isCancellation =
                error is CancellationError
                || (error as? URLError)?.code == .cancelled
            if isCancellation {
                if case .loading = states[statusId] {
                    states[statusId] = .idle
                }
                return
            }
            states[statusId] = .failed
        }
    }
}
