import SwiftUI
import UIKit
import UniformTypeIdentifiers

struct ContentView: View {
    @State private var selectedImage: UIImage?
    @State private var queuedCaptures: [QueuedCapture] = []
    @State private var pickerSource: UIImagePickerController.SourceType = .photoLibrary
    @State private var isPickerPresented = false
    @State private var isFilePickerPresented = false
    @State private var activeSet: ObjectSet?
    @State private var debugCaptureID: UUID?
    @State private var hasSeededBundledObjects = false
    @State private var isRefreshingRecent = false
    @State private var refreshSpinID = 0
    @State private var isMotorReachable = true

    var body: some View {
        ZStack {
            Color(red: 0.955, green: 0.94, blue: 0.905).ignoresSafeArea()

            VStack(spacing: 0) {
                Spacer(minLength: 16)
                capturePanel
                    .padding(.horizontal, 18)
                Spacer(minLength: 10)
                queueStrip
                    .padding(.horizontal, 14)
                    .padding(.bottom, 12)
            }
        }
        .sheet(isPresented: $isPickerPresented) {
            CameraPicker(sourceType: pickerSource) { image in
                enqueueImage(image)
            }
        }
        .fullScreenCover(item: $activeSet) { objectSet in
            ObjectExperienceView(objectSet: objectSet)
        }
        .fileImporter(isPresented: $isFilePickerPresented, allowedContentTypes: [.image]) { result in
            guard let url = try? result.get() else { return }
            let scoped = url.startAccessingSecurityScopedResource()
            defer {
                if scoped { url.stopAccessingSecurityScopedResource() }
            }
            if let data = try? Data(contentsOf: url), let image = UIImage(data: data) {
                enqueueImage(image)
            }
        }
        .onAppear {
            theObjectCheck("app_appear build=deterministic_frontdoor_v3 server=\(TheObjectIngestClient.debugServerBaseString)")
        }
        .task {
            theObjectCheck("app_started build=deterministic_frontdoor_v3 server=\(TheObjectIngestClient.debugServerBaseString)")
            loadStoredRecentCaptures()
            await refreshRecentCaptures(userInitiated: false)
            await runAutotestIfRequested()
        }
    }

    @ViewBuilder
    private var capturePanel: some View {
        if let selectedImage {
            VStack(spacing: 18) {
                Image(uiImage: selectedImage)
                    .resizable()
                    .scaledToFill()
                    .frame(maxWidth: .infinity)
                    .frame(height: 360)
                    .clipShape(RoundedRectangle(cornerRadius: 28, style: .continuous))
                    .clipped()
                    .shadow(color: .black.opacity(0.10), radius: 26, x: 0, y: 16)

                HStack(spacing: 18) {
                    iconAction(systemName: "xmark", role: .secondary) {
                        SoundController.shared.playSecondaryButton()
                        self.selectedImage = nil
                    }
                    iconAction(systemName: "checkmark", role: .primary) {
                        SoundController.shared.playPrimaryButton()
                        enqueueSelectedImage()
                    }
                }
                .frame(height: 72)
            }
        } else {
            HStack(spacing: 10) {
                sourceButton(systemName: "camera.viewfinder", source: .camera, primary: true)
                sourceButton(systemName: "photo.on.rectangle.angled", source: .photoLibrary, primary: false)
                fileButton()
            }
            .frame(maxWidth: .infinity)
            .frame(height: 118)
        }
    }

    private var visibleQueuedCaptures: [QueuedCapture] {
        queuedCaptures
    }

    private var queueStrip: some View {
        ScrollView(.vertical, showsIndicators: false) {
            VStack(spacing: 12) {
                if !isMotorReachable {
                    Text("Lokaler Motor nicht erreichbar")
                        .font(.system(size: 15, weight: .semibold))
                        .foregroundStyle(Color(red: 0.56, green: 0.08, blue: 0.08))
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .padding(.horizontal, 14)
                        .padding(.vertical, 12)
                        .background(Color(red: 0.98, green: 0.90, blue: 0.88))
                        .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
                }

                if isRefreshingRecent {
                    ProgressView()
                        .progressViewStyle(.circular)
                        .tint(.gray.opacity(0.72))
                        .scaleEffect(0.92)
                        .id(refreshSpinID)
                        .frame(height: 30)
                        .transition(.move(edge: .top).combined(with: .opacity))
                }

                LazyVGrid(columns: [GridItem(.adaptive(minimum: 104), spacing: 12)], alignment: .leading, spacing: 14) {
                    ForEach(visibleQueuedCaptures) { item in
                        QueueCard(item: item) {
                            if let result = item.result {
                                theObjectCheck("open_object id=\(result.id) orientation=\(result.displayOrientation)")
                                // Als gesehen markieren, Blau-Markierung entfernen
                                let captureKey = item.remoteCaptureId ?? result.id
                                SeenCaptureStore.markSeen(captureKey)
                                if item.isNewlyGenerated {
                                    updateQueuedCapture(item.id) { capture in
                                        capture.isNewlyGenerated = false
                                    }
                                }
                                activeSet = result
                            } else {
                                SoundController.shared.playSecondaryButton()
                            }
                        }
                    }
                }
                .frame(maxWidth: .infinity, alignment: .topLeading)
                .padding(.vertical, visibleQueuedCaptures.isEmpty ? 0 : 4)
            }
            .animation(.spring(response: 0.30, dampingFraction: 0.78), value: isRefreshingRecent)
        }
        .frame(maxHeight: .infinity, alignment: .top)
        .refreshable {
            await refreshRecentCaptures(userInitiated: true)
        }
    }

    private func sourceButton(systemName: String, source: UIImagePickerController.SourceType, primary: Bool) -> some View {
        Button {
            if primary {
                SoundController.shared.playPrimaryButton()
            } else {
                SoundController.shared.playSecondaryButton()
            }
            pickerSource = source
            isPickerPresented = true
            theObjectCheck("picker_present source=\(source == .camera ? "camera" : "photoLibrary")")
        } label: {
            Image(systemName: systemName)
                .font(.system(size: 36, weight: .light))
                .symbolRenderingMode(.hierarchical)
                .foregroundStyle(primary ? .white : .black.opacity(0.78))
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(primary ? Color.black : Color.white.opacity(0.74))
                .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
                .overlay {
                    RoundedRectangle(cornerRadius: 22, style: .continuous)
                        .stroke(primary ? Color.white.opacity(0.08) : Color.black.opacity(0.06), lineWidth: 1)
                }
                .shadow(color: .black.opacity(primary ? 0.14 : 0.06), radius: primary ? 16 : 10, x: 0, y: primary ? 10 : 6)
        }
        .buttonStyle(PressScaleButtonStyle())
        .accessibilityLabel(source == .camera ? "Kamera" : "Fotomediathek")
    }

    private func fileButton() -> some View {
        Button {
            SoundController.shared.playSecondaryButton()
            isFilePickerPresented = true
        } label: {
            Image(systemName: "folder")
                .font(.system(size: 34, weight: .light))
                .symbolRenderingMode(.hierarchical)
                .foregroundStyle(.black.opacity(0.78))
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(Color.white.opacity(0.74))
                .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
                .overlay {
                    RoundedRectangle(cornerRadius: 22, style: .continuous)
                        .stroke(Color.black.opacity(0.06), lineWidth: 1)
                }
                .shadow(color: .black.opacity(0.06), radius: 10, x: 0, y: 6)
        }
        .buttonStyle(PressScaleButtonStyle())
        .accessibilityLabel("Dateien")
    }

    private enum ActionRole {
        case primary
        case secondary
    }

    private func iconAction(systemName: String, role: ActionRole, action: @escaping () -> Void) -> some View {
        Button(action: action) {
            Image(systemName: systemName)
                .font(.system(size: 28, weight: .semibold))
                .foregroundStyle(role == .primary ? .white : .black.opacity(0.72))
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(role == .primary ? Color.black : Color.white.opacity(0.72))
                .clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
                .overlay {
                    RoundedRectangle(cornerRadius: 24, style: .continuous)
                        .stroke(Color.black.opacity(role == .primary ? 0 : 0.06), lineWidth: 1)
                }
        }
        .buttonStyle(PressScaleButtonStyle())
        .accessibilityLabel(systemName == "checkmark" ? "In die Schlange" : "Verwerfen")
    }

    private func enqueueSelectedImage() {
        guard let selectedImage else { return }
        enqueueImage(selectedImage)
    }

    private func enqueueImage(_ image: UIImage) {
        let item = QueuedCapture(
            image: image,
            isPortrait: image.size.height > image.size.width,
            result: nil
        )
        queuedCaptures.insert(item, at: 0)
        self.selectedImage = nil
        theObjectCheck("upload_enqueue id=\(item.id.uuidString) portrait=\(item.isPortrait)")
        uploadCapture(item)
    }

    private func seedBundledObjectsIfNeeded() {
        guard !hasSeededBundledObjects else { return }
        hasSeededBundledObjects = true
        var bundledItems: [QueuedCapture] = []
        for objectSet in demoObjectSets.reversed() {
            let alreadyQueued = queuedCaptures.contains { capture in
                capture.result?.id == objectSet.id
            }
            guard !alreadyQueued else { continue }
            let item = QueuedCapture(
                image: bundledPreviewImage(for: objectSet),
                isPortrait: objectSet.displayOrientation == .portrait,
                uploadState: .caching,
                result: objectSet
            )
            bundledItems.append(item)
        }
        queuedCaptures.append(contentsOf: bundledItems)
    }

    private func bundledPreviewImage(for objectSet: ObjectSet) -> UIImage {
        for name in [objectSet.originalImage, objectSet.heroImage] {
            if let image = imageFromBundle(named: name) {
                return image
            }
        }
        return placeholderImage(isPortrait: objectSet.displayOrientation == .portrait)
    }

    private func imageFromBundle(named name: String) -> UIImage? {
        if let image = UIImage(named: name) {
            return image
        }
        for ext in ["png", "jpg", "jpeg"] {
            if let url = bundleURL(for: name, extension: ext),
               let image = UIImage(contentsOfFile: url.path) {
                return image
            }
        }
        return nil
    }

    private func bundleURL(for name: String, extension ext: String) -> URL? {
        if name.contains("/") {
            let url = URL(fileURLWithPath: name)
            let resource = url.lastPathComponent
            let subdirectory = url.deletingLastPathComponent().relativePath
            return Bundle.main.url(forResource: resource, withExtension: ext, subdirectory: subdirectory)
        }
        return Bundle.main.url(forResource: name, withExtension: ext)
    }

    private func uploadCapture(_ item: QueuedCapture) {
        Task {
            do {
                theObjectCheck("upload_start id=\(item.id.uuidString)")
                let response = try await TheObjectIngestClient.upload(image: item.image, captureId: item.id)
                await MainActor.run {
                    isMotorReachable = true
                    updateQueuedCapture(item.id) { capture in
                        capture.uploadState = .heroQueued(response.jobId)
                    }
                }
                theObjectCheck("upload_ok id=\(item.id.uuidString) job=\(response.jobId ?? "none")")
                await pollResult(for: item.id, isPortrait: item.isPortrait)
            } catch {
                theObjectCheck("upload_failed id=\(item.id.uuidString) error=\(error.localizedDescription)")
                await MainActor.run {
                    isMotorReachable = false
                    updateQueuedCapture(item.id) { capture in
                        capture.uploadState = .failed
                    }
                    SoundController.shared.playSecondaryButton()
                }
            }
        }
    }

    private func pollResult(for captureId: UUID, isPortrait: Bool) async {
        var missingOrErrorCount = 0
        let maxPolls = 150
        var polls = 0
        while !Task.isCancelled {
            try? await Task.sleep(nanoseconds: 8_000_000_000)
            polls += 1
            do {
                let status = try await TheObjectIngestClient.status(captureId: captureId)
                await MainActor.run {
                    isMotorReachable = true
                }
                guard status.ok else {
                    if status.status == "failed" {
                        await markCaptureFailed(captureId)
                        return
                    }
                    missingOrErrorCount += 1
                    if missingOrErrorCount >= 4 {
                        await markCaptureWaiting(captureId)
                        return
                    }
                    continue
                }
                missingOrErrorCount = 0
                if status.ready, let objectSet = objectSet(from: status, captureId: captureId.uuidString, isPortrait: isPortrait) {
                    let pollBadges = generatorBadges(from: status)
                    let isNew = !SeenCaptureStore.isSeen(captureId.uuidString)
                    await MainActor.run {
                        updateQueuedCapture(captureId) { capture in
                            capture.uploadState = .caching
                            capture.result = objectSet
                            capture.isPartialResult = false
                            capture.generatorBadges = pollBadges
                            if isNew { capture.isNewlyGenerated = true }
                        }
                        persistReadyCaptures()
                    }
                    let cachedSet = try? await LocalObjectCache.shared.cachedObjectSet(objectSet)
                    await MainActor.run {
                        updateQueuedCapture(captureId) { capture in
                            if let cachedSet {
                                capture.result = cachedSet
                            } else {
                                theObjectCheck("poll_cache_deferred id=\(captureId.uuidString)")
                            }
                        }
                        persistReadyCaptures()
                        SoundController.shared.playPrimaryButton()
                    }
                    return
                }
            } catch {
                await MainActor.run {
                    isMotorReachable = false
                }
                missingOrErrorCount += 1
                if missingOrErrorCount >= 4 {
                    await markCaptureWaiting(captureId)
                    return
                }
            }
            if polls >= maxPolls {
                await markCaptureWaiting(captureId)
                return
            }
        }
    }

    @MainActor
    private func markCaptureFailed(_ captureId: UUID) {
        updateQueuedCapture(captureId) { capture in
            capture.uploadState = .failed
        }
        SoundController.shared.playSecondaryButton()
    }

    @MainActor
    private func markCaptureWaiting(_ captureId: UUID) {
        updateQueuedCapture(captureId) { capture in
            if capture.result == nil {
                capture.uploadState = .heroQueued(nil)
            }
        }
    }

    private func loadStoredRecentCaptures() {
        let storedObjects = RecentObjectStore.load()
        guard !storedObjects.isEmpty else { return }
        for stored in storedObjects {
            let objectSet = stored.objectSet
            let queueId = UUID(uuidString: stored.remoteCaptureId) ?? UUID()
            guard !queuedCaptures.contains(where: { $0.remoteCaptureId == stored.remoteCaptureId || $0.result?.id == objectSet.id }) else { continue }
            queuedCaptures.append(QueuedCapture(
                id: queueId,
                remoteCaptureId: stored.remoteCaptureId,
                image: previewImage(for: objectSet),
                isPortrait: stored.isPortrait,
                uploadState: .caching,
                result: objectSet
            ))
        }
        theObjectCheck("gallery_restore_cached count=\(storedObjects.count)")
    }

    private func refreshRecentCaptures(userInitiated: Bool) async {
        if userInitiated {
            await MainActor.run {
                SoundController.shared.playRefreshSnap()
                refreshSpinID += 1
                withAnimation(.spring(response: 0.24, dampingFraction: 0.72)) {
                    isRefreshingRecent = true
                }
            }
        }
        defer {
            if userInitiated {
                Task { @MainActor in
                    withAnimation(.spring(response: 0.34, dampingFraction: 0.82)) {
                        isRefreshingRecent = false
                    }
                    SoundController.shared.playRefreshSettle()
                }
            }
        }

        do {
            let recent = try await TheObjectIngestClient.recent()
            guard recent.ok else { return }
            await MainActor.run {
                isMotorReachable = true
            }
            theObjectCheck("gallery_recent ok=true count=\(recent.captures.count)")
            var serverOrder: [String] = []
            var seenServerIds = Set<String>()
            for status in recent.captures {
                guard let captureId = status.captureId else {
                    continue
                }
                let queueId = UUID(uuidString: captureId) ?? UUID()
                let isPortrait = status.orientation != "landscape"
                let image = placeholderImage(isPortrait: isPortrait)
                serverOrder.append(captureId)
                seenServerIds.insert(captureId)
                let remoteState: QueuedCaptureState
                if status.status == "failed" {
                    remoteState = .failed
                } else if status.ready {
                    remoteState = .caching
                } else {
                    remoteState = .heroQueued(nil as String?)
                }

                let remoteSet = objectSet(from: status, captureId: captureId, isPortrait: isPortrait)

                guard let remoteSet else {
                    await MainActor.run {
                        if let existing = queuedCaptures.firstIndex(where: { $0.remoteCaptureId == captureId || $0.id == queueId }) {
                            queuedCaptures[existing].uploadState = remoteState
                            queuedCaptures[existing].glows = status.glow == true
                            queuedCaptures[existing].isPartialResult = false
                        } else {
                            theObjectCheck("gallery_pending id=\(captureId) status=\(status.status) videos=\(status.doneVideos ?? 0)/\(status.neededVideos ?? 6) orientation=\(status.orientation ?? "unknown")")
                            queuedCaptures.insert(QueuedCapture(
                                id: queueId,
                                remoteCaptureId: captureId,
                                image: image,
                                isPortrait: isPortrait,
                                uploadState: remoteState,
                                result: nil,
                                glows: status.glow == true,
                                isPartialResult: false
                            ), at: 0)
                        }
                    }
                    if let assets = status.assets {
                        Task {
                            if let thumbnail = await downloadedImage(from: [assets.originalImage, assets.heroImage]) {
                                await MainActor.run {
                                    updateQueuedCapture(remoteCaptureId: captureId, fallbackId: queueId) { capture in
                                        capture.image = thumbnail
                                    }
                                }
                            }
                        }
                    }
                    continue
                }
                let badges = generatorBadges(from: status)
                let isNew = status.ready && !SeenCaptureStore.isSeen(captureId)
                await MainActor.run {
                    if let existing = queuedCaptures.firstIndex(where: { $0.remoteCaptureId == captureId || $0.result?.id == captureId || $0.id == queueId }) {
                        queuedCaptures[existing].uploadState = remoteState
                        queuedCaptures[existing].glows = status.glow == true
                        queuedCaptures[existing].isPartialResult = !status.ready
                        queuedCaptures[existing].result = remoteSet
                        // Badges + Neu-Status nur setzen wenn noch nicht gesehen
                        if !queuedCaptures[existing].generatorBadges.isEmpty || !badges.isEmpty {
                            queuedCaptures[existing].generatorBadges = badges
                        }
                        if isNew { queuedCaptures[existing].isNewlyGenerated = true }
                    } else {
                        theObjectCheck("gallery_enqueue id=\(captureId) videos=\(status.assets?.videos.count ?? 0) orientation=\(status.orientation ?? "unknown") badges=\(badges.joined(separator: ","))")
                        queuedCaptures.insert(QueuedCapture(
                            id: queueId,
                            remoteCaptureId: captureId,
                            image: image,
                            isPortrait: isPortrait,
                            uploadState: remoteState,
                            result: remoteSet,
                            glows: status.glow == true,
                            isPartialResult: !status.ready,
                            generatorBadges: badges,
                            isNewlyGenerated: isNew
                        ), at: 0)
                    }
                    persistReadyCaptures()
                }
                Task {
                    if let assets = status.assets,
                       let thumbnail = await downloadedImage(from: [assets.originalImage, assets.heroImage]) {
                        await MainActor.run {
                            updateQueuedCapture(remoteCaptureId: captureId, fallbackId: queueId) { capture in
                                capture.image = thumbnail
                            }
                        }
                    }
                    let cachedObjectSet = try? await LocalObjectCache.shared.cachedObjectSet(remoteSet)
                    await MainActor.run {
                        updateQueuedCapture(remoteCaptureId: captureId, fallbackId: queueId) { capture in
                            if let cachedObjectSet {
                                let videoCount = [
                                    cachedObjectSet.realityToHero,
                                    cachedObjectSet.heroScrub,
                                    cachedObjectSet.heroToExploded,
                                    cachedObjectSet.explodedScrub,
                                    cachedObjectSet.explodedToNeatify,
                                    cachedObjectSet.heroToUnrealExploded,
                                    cachedObjectSet.unrealExplodedScrub,
                                    cachedObjectSet.secretExplodedToSecretDetail
                                ].compactMap { $0 }.count
                                theObjectCheck("gallery_cached id=\(cachedObjectSet.id) videos=\(videoCount) realityToHero=\(cachedObjectSet.realityToHero)")
                                capture.result = cachedObjectSet
                                capture.image = previewImage(for: cachedObjectSet)
                                capture.isPartialResult = !status.ready
                                persistReadyCaptures()
                            } else {
                                theObjectCheck("gallery_cache_deferred id=\(captureId)")
                                capture.result = remoteSet
                                capture.uploadState = remoteState
                                capture.isPartialResult = !status.ready
                                persistReadyCaptures()
                            }
                        }
                    }
                }
            }
            await MainActor.run {
                queuedCaptures.removeAll { capture in
                    guard let remoteId = capture.remoteCaptureId ?? capture.result?.id else {
                        return false
                    }
                    return !seenServerIds.contains(remoteId)
                }
                reorderQueue(serverOrder: serverOrder)
            }
        } catch {
            await MainActor.run {
                isMotorReachable = false
            }
            if userInitiated {
                await MainActor.run {
                    SoundController.shared.playSecondaryButton()
                }
            }
            return
        }
    }

    @MainActor
    private func reorderQueue(serverOrder: [String]) {
        let priority = Dictionary(uniqueKeysWithValues: serverOrder.enumerated().map { ($0.element, $0.offset) })
        queuedCaptures.sort { left, right in
            let leftKey = left.remoteCaptureId ?? left.result?.id
            let rightKey = right.remoteCaptureId ?? right.result?.id
            let leftRank = leftKey.flatMap { priority[$0] } ?? Int.max
            let rightRank = rightKey.flatMap { priority[$0] } ?? Int.max
            if leftRank != rightRank { return leftRank < rightRank }
            return left.startedAt > right.startedAt
        }
    }

    @MainActor
    private func persistReadyCaptures() {
        let readyObjects = queuedCaptures.compactMap { capture -> StoredRecentObject? in
            guard let result = capture.result else { return nil }
            return StoredRecentObject(objectSet: result, remoteCaptureId: capture.remoteCaptureId ?? result.id, isPortrait: capture.isPortrait)
        }
        RecentObjectStore.save(readyObjects)
    }

    private func previewImage(for objectSet: ObjectSet) -> UIImage {
        for value in [objectSet.heroImage, objectSet.originalImage, objectSet.explodedImage] {
            if let url = URL(string: value), url.isFileURL, let image = UIImage(contentsOfFile: url.path) {
                return image
            }
            if let image = imageFromBundle(named: value) {
                return image
            }
        }
        return placeholderImage(isPortrait: objectSet.displayOrientation == .portrait)
    }

    private func runAutotestIfRequested() async {
        guard let objectId = ProcessInfo.processInfo.environment["THE_OBJECT_AUTOTEST_OBJECT_ID"],
              !objectId.isEmpty else {
            return
        }
        for _ in 0..<80 {
            if let result = await MainActor.run(body: {
                queuedCaptures.first(where: { $0.result?.id == objectId })?.result
            }) {
                await MainActor.run {
                    theObjectCheck("autotest_open id=\(result.id)")
                    activeSet = result
                }
                return
            }
            try? await Task.sleep(nanoseconds: 250_000_000)
        }
        theObjectCheck("autotest_open_failed id=\(objectId)")
    }

    private func downloadedImage(from candidates: [String?]) async -> UIImage? {
        for candidate in candidates.compactMap({ $0 }) {
            guard let url = URL(string: candidate) else { continue }
            var request = URLRequest(url: url)
            request.timeoutInterval = 6
            if let (data, response) = try? await URLSession.shared.data(for: request),
               let http = response as? HTTPURLResponse,
               (200..<300).contains(http.statusCode),
               let image = UIImage(data: data) {
                return image
            }
        }
        return nil
    }

    private func placeholderImage(isPortrait: Bool) -> UIImage {
        let size = isPortrait ? CGSize(width: 900, height: 1200) : CGSize(width: 1200, height: 900)
        let renderer = UIGraphicsImageRenderer(size: size)
        return renderer.image { context in
            UIColor(red: 0.955, green: 0.94, blue: 0.905, alpha: 1).setFill()
            context.fill(CGRect(origin: .zero, size: size))
            UIColor.black.withAlphaComponent(0.08).setFill()
            let rect = CGRect(x: size.width * 0.36, y: size.height * 0.36, width: size.width * 0.28, height: size.width * 0.28)
            UIBezierPath(ovalIn: rect).fill()
        }
    }

    /// Liest die Generator-Badges (z.B. ["K", "S"]) aus dem ersten Filmslot des Status-Assets
    private func generatorBadges(from status: TheObjectStatusResponse) -> [String] {
        status.assets?.availableBadges(forFilm: "film_01_original_to_hero") ?? []
    }

    private func objectSet(from status: TheObjectStatusResponse, captureId: String, isPortrait: Bool) -> ObjectSet? {
        func video(_ keys: String...) -> String? {
            for key in keys {
                if let value = status.assets?.videos[key] {
                    return value
                }
            }
            return nil
        }

        guard let assets = status.assets,
              let original = assets.originalImage,
              let hero = assets.heroImage else {
            theObjectCheck("object_mapping_failed id=\(captureId) videoKeys=\(status.assets?.videos.keys.sorted().joined(separator: ",") ?? "none")")
            return nil
        }
        let realExploded = assets.realExplodedImage ?? hero
        let unrealExploded = assets.unrealExplodedImage ?? realExploded
        guard let fallbackVideo = video(
            "film_01_original_to_hero", "G", "01_a_to_b_reality_to_hero",
            "film_02_hero_orbit_360", "H", "04a_hero_360_clockwork_asmr",
            "film_03_hero_to_exploded_real", "I", "02_b_to_z_real_explosion",
            "film_04_exploded_real_orbit_360", "J", "04b_exploded_real_360_clockwork_asmr",
            "film_06_hero_to_exploded_secret", "L", "03_b_to_z_unreal_explosion",
            "film_07_exploded_secret_orbit_360", "M", "04c_exploded_unreal_360_clockwork_asmr"
        ) else {
            theObjectCheck("object_mapping_waiting_for_first_video id=\(captureId)")
            return nil
        }
        let realityToHero = video("film_01_original_to_hero", "G", "01_a_to_b_reality_to_hero") ?? fallbackVideo
        let hero360 = video("film_02_hero_orbit_360", "H", "04a_hero_360_clockwork_asmr") ?? realityToHero
        let realExplosion = video("film_03_hero_to_exploded_real", "I", "02_b_to_z_real_explosion") ?? realityToHero
        let real360 = video("film_04_exploded_real_orbit_360", "J", "04b_exploded_real_360_clockwork_asmr") ?? hero360
        let unrealExplosion = video("film_06_hero_to_exploded_secret", "L", "03_b_to_z_unreal_explosion") ?? realExplosion
        let unreal360 = video("film_07_exploded_secret_orbit_360", "M", "04c_exploded_unreal_360_clockwork_asmr") ?? real360
        let realToNeatify = video("film_05_exploded_real_to_neatify", "K")
        let secretToDetail = video("film_08_exploded_secret_to_secret_detail", "N")

        // Generator-Badges aus videoVariants lesen (erstes Film-Slot G=film_01_original_to_hero)
        let badges = assets.availableBadges(forFilm: "film_01_original_to_hero")

        theObjectCheck("object_mapping_ok id=\(captureId) videos=\(status.assets?.videos.count ?? 0) canonicalDetail=\(realToNeatify != nil && secretToDetail != nil) badges=\(badges.joined(separator: ","))")
        return ObjectSet(
            id: captureId,
            title: title(for: captureId),
            displayOrientation: isPortrait ? .portrait : .landscape,
            originalImage: original,
            heroImage: hero,
            explodedImage: realExploded,
            realityToHero: realityToHero,
            heroScrub: hero360,
            heroToExploded: realExplosion,
            explodedScrub: real360,
            neatifyImage: assets.neatifyImage,
            explodedToNeatify: realToNeatify,
            unrealExplodedImage: unrealExploded,
            heroToUnrealExploded: unrealExplosion,
            unrealExplodedScrub: unreal360,
            secretDetailImage: assets.secretDetailImage,
            secretExplodedToSecretDetail: secretToDetail
        )
    }

    private func title(for captureId: String) -> String {
        switch captureId {
        case "OLIVE-KAROTTE-HERO-28":
            return "OLIVE KAROTTE 28"
        case "ALT-FILM-NEW-FORMAT-SAMPLE":
            return "ALT-FILM SAMPLE"
        default:
            return "Live Object"
        }
    }

    private func updateQueuedCapture(_ id: UUID, mutate: (inout QueuedCapture) -> Void) {
        guard let index = queuedCaptures.firstIndex(where: { $0.id == id }) else { return }
        mutate(&queuedCaptures[index])
    }

    private func updateQueuedCapture(remoteCaptureId: String, fallbackId: UUID, mutate: (inout QueuedCapture) -> Void) {
        guard let index = queuedCaptures.firstIndex(where: { $0.remoteCaptureId == remoteCaptureId || $0.id == fallbackId }) else { return }
        mutate(&queuedCaptures[index])
    }
}

private struct StoredRecentObject: Codable {
    let id: String
    let title: String
    let displayOrientation: String
    let originalImage: String
    let heroImage: String
    let explodedImage: String
    let realityToHero: String
    let heroScrub: String
    let heroToExploded: String
    let explodedScrub: String
    let neatifyImage: String?
    let explodedToNeatify: String?
    let unrealExplodedImage: String?
    let heroToUnrealExploded: String?
    let unrealExplodedScrub: String?
    let secretDetailImage: String?
    let secretExplodedToSecretDetail: String?
    let remoteCaptureId: String
    let isPortrait: Bool

    init(objectSet: ObjectSet, remoteCaptureId: String, isPortrait: Bool) {
        self.id = objectSet.id
        self.title = objectSet.title
        self.displayOrientation = objectSet.displayOrientation == .portrait ? "portrait" : "landscape"
        self.originalImage = objectSet.originalImage
        self.heroImage = objectSet.heroImage
        self.explodedImage = objectSet.explodedImage
        self.realityToHero = objectSet.realityToHero
        self.heroScrub = objectSet.heroScrub
        self.heroToExploded = objectSet.heroToExploded
        self.explodedScrub = objectSet.explodedScrub
        self.neatifyImage = objectSet.neatifyImage
        self.explodedToNeatify = objectSet.explodedToNeatify
        self.unrealExplodedImage = objectSet.unrealExplodedImage
        self.heroToUnrealExploded = objectSet.heroToUnrealExploded
        self.unrealExplodedScrub = objectSet.unrealExplodedScrub
        self.secretDetailImage = objectSet.secretDetailImage
        self.secretExplodedToSecretDetail = objectSet.secretExplodedToSecretDetail
        self.remoteCaptureId = remoteCaptureId
        self.isPortrait = isPortrait
    }

    var objectSet: ObjectSet {
        ObjectSet(
            id: id,
            title: title,
            displayOrientation: displayOrientation == "portrait" ? .portrait : .landscape,
            originalImage: originalImage,
            heroImage: heroImage,
            explodedImage: explodedImage,
            realityToHero: realityToHero,
            heroScrub: heroScrub,
            heroToExploded: heroToExploded,
            explodedScrub: explodedScrub,
            neatifyImage: neatifyImage,
            explodedToNeatify: explodedToNeatify,
            unrealExplodedImage: unrealExplodedImage,
            heroToUnrealExploded: heroToUnrealExploded,
            unrealExplodedScrub: unrealExplodedScrub,
            secretDetailImage: secretDetailImage,
            secretExplodedToSecretDetail: secretExplodedToSecretDetail
        )
    }
}

private enum RecentObjectStore {
    private static let key = "theobject.recent.ready.objects.v3"

    static func load() -> [StoredRecentObject] {
        guard let data = UserDefaults.standard.data(forKey: key),
              let objects = try? JSONDecoder().decode([StoredRecentObject].self, from: data) else {
            return []
        }
        return objects
    }

    static func save(_ objects: [StoredRecentObject]) {
        let uniqueObjects = objects.reduce(into: [StoredRecentObject]()) { partial, object in
            guard !partial.contains(where: { $0.remoteCaptureId == object.remoteCaptureId || $0.id == object.id }) else { return }
            partial.append(object)
        }
        guard let data = try? JSONEncoder().encode(uniqueObjects) else { return }
        UserDefaults.standard.set(data, forKey: key)
    }
}

struct QueueCard: View {
    let item: QueuedCapture
    let openFinished: () -> Void

    var body: some View {
        Button {
            if isFinished {
                SoundController.shared.playPrimaryButton()
                openFinished()
            }
        } label: {
            Image(uiImage: item.image)
                .resizable()
                .scaledToFill()
                .frame(width: 100, height: 100)
                .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
                .background {
                    if item.glows {
                        RoundedRectangle(cornerRadius: 24, style: .continuous)
                            .fill(markerColor.opacity(0.22))
                            .blur(radius: 16)
                            .scaleEffect(1.10)
                    }
                }
                // Blauer Rahmen für neu generierte Filme (höchste Priorität, überschreibt andere Borders)
                .overlay {
                    RoundedRectangle(cornerRadius: 20, style: .continuous)
                        .stroke(borderColor.opacity(borderOpacity), lineWidth: borderWidth)
                }
                // Blauer Glow-Schatten für neu generierte Filme
                .shadow(color: item.isNewlyGenerated ? Color.blue.opacity(0.45) : .clear, radius: 10, x: 0, y: 0)
                .overlay(alignment: .topLeading) {
                    if showsStatusMarker {
                        Image(systemName: statusMarkerSymbol)
                            .font(.system(size: 14, weight: .bold))
                            .foregroundStyle(.white)
                            .frame(width: 22, height: 22)
                            .background(statusMarkerColor)
                            .clipShape(Circle())
                            .padding(8)
                    }
                }
                .overlay(alignment: .topTrailing) {
                    if isMarkedTarget {
                        Text("28")
                            .font(.system(size: 22, weight: .black, design: .rounded))
                            .foregroundStyle(.black)
                            .frame(width: 42, height: 42)
                            .background(markerColor)
                            .clipShape(Circle())
                            .shadow(color: .black.opacity(0.24), radius: 8, x: 0, y: 4)
                            .offset(x: 8, y: -8)
                    }
                }
                .overlay(alignment: .bottom) {
                    if isMarkedTarget {
                        Text("OLIVE")
                            .font(.system(size: 12, weight: .black))
                            .foregroundStyle(.black)
                            .padding(.horizontal, 9)
                            .padding(.vertical, 5)
                            .background(markerColor)
                            .clipShape(Capsule())
                            .offset(y: 9)
                    }
                }
                .overlay {
                    if isActivelyRunning {
                        WaitingGlyph()
                            .frame(width: 34, height: 34)
                            .padding(9)
                            .background(.black.opacity(0.34))
                            .clipShape(Circle())
                    }
                }
                // S/K-Generator-Badge unten rechts (nur wenn mindestens ein Generator bekannt)
                .overlay(alignment: .bottomTrailing) {
                    if let badgeText = generatorBadgeLabel {
                        Text(badgeText)
                            .font(.system(size: 11, weight: .bold, design: .monospaced))
                            .foregroundStyle(.white)
                            .padding(.horizontal, 6)
                            .padding(.vertical, 3)
                            .background(Color.black.opacity(0.62))
                            .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
                            .padding(6)
                    }
                }
                .shadow(color: markerColor.opacity(item.glows ? 0.30 : 0), radius: item.glows ? 20 : 0, x: 0, y: 0)
                .shadow(color: .black.opacity(isFinished ? 0.10 : 0.06), radius: isFinished ? 12 : 8, x: 0, y: isFinished ? 7 : 4)
                .opacity(isFinished || item.isPartialResult || isFailed ? 1 : 0.58)
        }
        .buttonStyle(PressScaleButtonStyle())
        .accessibilityLabel(item.result?.title ?? (isFailed ? "Objekt haengt" : "Film noch nicht fertig"))
    }

    private var isFinished: Bool {
        item.result != nil
    }

    private var isNewAndWaiting: Bool {
        item.result == nil
    }

    private var isFailed: Bool {
        if case .failed = item.uploadState { return true }
        return false
    }

    private var isActivelyRunning: Bool {
        if item.result != nil { return false }
        if case .heroQueued = item.uploadState { return true }
        if case .uploading = item.uploadState { return true }
        return false
    }

    private var isMarkedTarget: Bool {
        item.result?.id == "OLIVE-KAROTTE-HERO-28" || item.remoteCaptureId == "OLIVE-KAROTTE-HERO-28"
    }

    /// Label für den Generator-Badge: "S+K" wenn beide da, "S" oder "K" wenn nur einer
    private var generatorBadgeLabel: String? {
        guard isFinished, !item.generatorBadges.isEmpty else { return nil }
        return item.generatorBadges.joined(separator: "+")
    }

    private var markerColor: Color {
        if isFailed {
            return Color(red: 0.78, green: 0.18, blue: 0.18)
        }
        if item.isPartialResult {
            return Color(red: 0.88, green: 0.56, blue: 0.16)
        }
        return isMarkedTarget ? Color(red: 1.0, green: 0.66, blue: 0.12) : .white
    }

    private var showsStatusMarker: Bool {
        isFailed || item.isPartialResult
    }

    private var statusMarkerSymbol: String {
        isFailed ? "exclamationmark" : "sparkles"
    }

    private var statusMarkerColor: Color {
        isFailed ? Color(red: 0.74, green: 0.18, blue: 0.16) : Color(red: 0.86, green: 0.56, blue: 0.14)
    }

    private var borderColor: Color {
        // Blau hat höchste Priorität — neu generiert
        if item.isNewlyGenerated && isFinished {
            return Color.blue
        }
        if isMarkedTarget || item.glows {
            return markerColor
        }
        if isFailed || item.isPartialResult {
            return markerColor
        }
        return .white
    }

    private var borderOpacity: Double {
        if item.isNewlyGenerated && isFinished {
            return 1
        }
        if isMarkedTarget || item.glows {
            return 1
        }
        if isFailed || item.isPartialResult {
            return 0.78
        }
        return isFinished ? 0.92 : 0.62
    }

    private var borderWidth: CGFloat {
        if item.isNewlyGenerated && isFinished {
            return 3
        }
        if isMarkedTarget || item.glows {
            return 4
        }
        if isFailed || item.isPartialResult {
            return 1.5
        }
        return isFinished ? 2 : 1
    }
}

struct WaitingGlyph: View {
    var body: some View {
        TimelineView(.animation) { context in
            let t = context.date.timeIntervalSinceReferenceDate
            Canvas { canvas, size in
                let center = CGPoint(x: size.width / 2, y: size.height / 2)
                for index in 0..<5 {
                    let phase = t * 0.75 + Double(index) * 1.256
                    let radius = 4.2 + sin(phase * 1.7) * 1.4
                    let point = CGPoint(
                        x: center.x + cos(phase) * radius,
                        y: center.y + sin(phase) * radius
                    )
                    let opacity = 0.38 + 0.42 * (0.5 + 0.5 * sin(phase + 0.8))
                    canvas.fill(
                        Path(ellipseIn: CGRect(x: point.x - 1.35, y: point.y - 1.35, width: 2.7, height: 2.7)),
                        with: .color(.white.opacity(opacity))
                    )
                }
            }
        }
    }
}

struct WaitingParticleSwarm: View {
    var body: some View {
        TimelineView(.animation) { context in
            let t = context.date.timeIntervalSinceReferenceDate
            Canvas { canvas, size in
                for index in 0..<18 {
                    let seed = Double(index)
                    let phase = t * (0.18 + seed.truncatingRemainder(dividingBy: 4) * 0.018) + seed * 1.71
                    let x = size.width * (0.50 + 0.34 * cos(phase * 0.91 + seed))
                    let y = size.height * (0.50 + 0.32 * sin(phase * 1.13 + seed * 0.8))
                    let dot = 1.1 + 0.9 * (0.5 + 0.5 * sin(phase * 1.9))
                    let alpha = 0.10 + 0.16 * (0.5 + 0.5 * cos(phase))
                    let rect = CGRect(x: x - dot / 2, y: y - dot / 2, width: dot, height: dot)
                    let color = index % 3 == 0
                        ? Color.white.opacity(alpha)
                        : Color(red: 0.74, green: 0.69, blue: 0.58).opacity(alpha)
                    canvas.fill(Path(ellipseIn: rect), with: .color(color))
                }
            }
        }
        .allowsHitTesting(false)
    }
}


struct PressScaleButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .scaleEffect(configuration.isPressed ? 0.975 : 1)
            .animation(.spring(response: 0.22, dampingFraction: 0.78), value: configuration.isPressed)
    }
}
