import SwiftUI

// MARK: - MessageImageView

/// Displays an image attachment inside a message bubble.
///
/// Android parity: `AttachmentComponents.kt` — blurhash placeholder, download button,
/// spinner overlay, upload spinner for outgoing photos, error retry button.
///
/// States:
/// 1. **Cached + Delivered** — full image, no overlay
/// 2. **Cached + Uploading** — full image + dark overlay + upload spinner (outgoing only)
/// 3. **Downloading** — blurhash + dark overlay + spinner
/// 4. **Not Downloaded** — blurhash + dark overlay + download arrow button
/// 5. **Error** — blurhash + dark overlay + red retry button
struct MessageImageView: View {

    let attachment: MessageAttachment
    let message: ChatMessage
    let outgoing: Bool

    /// When set, the image fills this exact frame (used inside PhotoCollageView).
    var collageSize: CGSize? = nil

    let maxWidth: CGFloat

    var onImageTap: ((UIImage) -> Void)?

    @State private var image: UIImage?
    @State private var blurImage: UIImage?
    @State private var isDownloading = false
    @State private var downloadError = false

    private var isCollageCell: Bool { collageSize != nil }

    // Telegram-style constraints (standalone mode)
    private let maxImageWidth: CGFloat = 270
    private let maxImageHeight: CGFloat = 320
    private let minImageWidth: CGFloat = 140
    private let minImageHeight: CGFloat = 100
    private let placeholderWidth: CGFloat = 200
    private let placeholderHeight: CGFloat = 200

    /// Android parity: outgoing photo is "uploading" when cached but not yet delivered.
    private var isUploading: Bool {
        outgoing && image != nil && message.deliveryStatus == .waiting
    }

    var body: some View {
        ZStack {
            if let image {
                imageContent(image)
            } else {
                placeholderView
            }

            // Android parity: full-image dark overlay + centered control
            if isUploading {
                uploadingOverlay
            } else if isDownloading {
                downloadingOverlay
            } else if downloadError {
                errorOverlay
            } else if image == nil {
                downloadArrowOverlay
            }
        }
        .task {
            await loadFromCache()
            if image == nil {
                await decodeBlurHash()
            }
        }
        .onReceive(NotificationCenter.default.publisher(for: .triggerAttachmentDownload)) { notif in
            if let id = notif.object as? String, id == attachment.id, image == nil {
                downloadImage()
            }
        }
    }

    // MARK: - Android-Style Overlays

    /// Android: outgoing photo uploading — 28dp spinner, 0.35 alpha overlay.
    private var uploadingOverlay: some View {
        overlayContainer(alpha: 0.35) {
            ProgressView()
                .tint(.white)
                .scaleEffect(0.8)
                .frame(width: 28, height: 28)
        }
    }

    /// Android: downloading — 36dp spinner, 0.3 alpha overlay.
    private var downloadingOverlay: some View {
        overlayContainer(alpha: 0.3) {
            ProgressView()
                .tint(.white)
                .scaleEffect(1.0)
                .frame(width: 36, height: 36)
        }
    }

    /// Android: error — red 48dp button with refresh icon + label.
    private var errorOverlay: some View {
        overlayContainer(alpha: 0.3) {
            VStack(spacing: 8) {
                Circle()
                    .fill(Color(red: 0.9, green: 0.22, blue: 0.21).opacity(0.8))
                    .frame(width: 48, height: 48)
                    .overlay {
                        Image(systemName: "arrow.clockwise")
                            .font(.system(size: 20, weight: .medium))
                            .foregroundStyle(.white)
                    }
                Text("Error")
                    .font(.system(size: 12))
                    .foregroundStyle(.white)
            }
        }
    }

    /// Android: not downloaded — 48dp button with download arrow.
    private var downloadArrowOverlay: some View {
        overlayContainer(alpha: 0.3) {
            Circle()
                .fill(Color.black.opacity(0.5))
                .frame(width: 48, height: 48)
                .overlay {
                    Image(systemName: "arrow.down")
                        .font(.system(size: 20, weight: .medium))
                        .foregroundStyle(.white)
                }
        }
    }

    /// Android parity: full-image semi-transparent dark overlay with centered content.
    private func overlayContainer<Content: View>(alpha: Double, @ViewBuilder content: () -> Content) -> some View {
        ZStack {
            Color.black.opacity(alpha)
            content()
        }
    }

    // MARK: - Image Content

    @ViewBuilder
    private func imageContent(_ img: UIImage) -> some View {
        if let size = collageSize {
            Image(uiImage: img)
                .resizable()
                .scaledToFill()
                .frame(width: size.width, height: size.height)
                .clipped()
                .contentShape(Rectangle())
                .onTapGesture { onImageTap?(img) }
        } else {
            let size = constrainedSize(for: img)
            Image(uiImage: img)
                .resizable()
                .scaledToFill()
                .frame(width: size.width, height: size.height)
                .clipped()
                .contentShape(Rectangle())
                .onTapGesture { onImageTap?(img) }
        }
    }

    private func constrainedSize(for img: UIImage) -> CGSize {
        let constrainedWidth = min(maxImageWidth, maxWidth)
        guard img.size.width > 0, img.size.height > 0 else {
            return CGSize(width: min(placeholderWidth, constrainedWidth), height: placeholderHeight)
        }
        let aspectRatio = img.size.width / img.size.height
        let displayWidth = min(constrainedWidth, max(minImageWidth, img.size.width))
        let displayHeight = min(maxImageHeight, max(minImageHeight, displayWidth / max(aspectRatio, 0.01)))
        let finalWidth = min(constrainedWidth, displayHeight * aspectRatio)
        return CGSize(width: max(finalWidth, 1), height: max(displayHeight, 1))
    }

    // MARK: - Placeholder

    @ViewBuilder
    private var placeholderView: some View {
        let size = resolvedPlaceholderSize
        if let blurImage {
            Image(uiImage: blurImage)
                .resizable()
                .scaledToFill()
                .frame(width: size.width, height: size.height)
                .clipped()
        } else {
            Rectangle()
                .fill(Color.white.opacity(0.08))
                .frame(width: size.width, height: size.height)
        }
    }

    private var resolvedPlaceholderSize: CGSize {
        if let size = collageSize { return size }
        let w = min(placeholderWidth, min(maxImageWidth, maxWidth))
        return CGSize(width: w, height: w)
    }

    // MARK: - BlurHash Decoding

    @MainActor private static var blurHashCache: [String: UIImage] = [:]

    private func decodeBlurHash() async {
        let hash = extractBlurHash(from: attachment.preview)
        guard !hash.isEmpty else { return }
        // Fast path: cache hit (synchronous)
        if let cached = Self.blurHashCache[hash] {
            blurImage = cached
            return
        }
        // Slow path: DCT decode off main thread
        let result = await Task.detached(priority: .userInitiated) {
            UIImage.fromBlurHash(hash, width: 32, height: 32)
        }.value
        guard !Task.isCancelled, let result else { return }
        if Self.blurHashCache.count > 200 {
            let keysToRemove = Array(Self.blurHashCache.keys.prefix(100))
            for key in keysToRemove { Self.blurHashCache.removeValue(forKey: key) }
        }
        Self.blurHashCache[hash] = result
        blurImage = result
    }

    // MARK: - Download

    private func loadFromCache() async {
        PerformanceLogger.shared.track("image.cacheLoad")
        // Fast path: memory-only NSCache hit (no disk/crypto on main thread).
        if let cached = AttachmentCache.shared.cachedImage(forAttachmentId: attachment.id) {
            image = cached
            return
        }
        // Slow path: disk I/O + crypto — run off main thread with semaphore
        // Android parity: ImageLoadSemaphore limits to 3 concurrent decode ops
        let attachmentId = attachment.id
        await ImageLoadLimiter.shared.acquire()
        let loaded = await Task.detached(priority: .userInitiated) {
            AttachmentCache.shared.loadImage(forAttachmentId: attachmentId)
        }.value
        await ImageLoadLimiter.shared.release()
        if !Task.isCancelled, let loaded {
            image = loaded
        }
    }

    private func downloadImage() {
        guard !isDownloading, image == nil else { return }

        let tag = attachment.effectiveDownloadTag
        guard !tag.isEmpty else {
            downloadError = true
            return
        }

        guard let storedPassword = message.attachmentPassword, !storedPassword.isEmpty else {
            downloadError = true
            return
        }

        isDownloading = true
        downloadError = false

        let server = attachment.transportServer
        Task {
            do {
                let encryptedData = try await TransportManager.shared.downloadFile(tag: tag, server: server)
                let encryptedString = String(decoding: encryptedData, as: UTF8.self)

                let passwords = MessageCrypto.attachmentPasswordCandidates(from: storedPassword)
                let downloadedImage = decryptAndParseImage(
                    encryptedString: encryptedString, passwords: passwords
                )

                await MainActor.run {
                    if let downloadedImage {
                        image = downloadedImage
                        AttachmentCache.shared.saveImage(downloadedImage, forAttachmentId: attachment.id)
                    } else {
                        downloadError = true
                    }
                    isDownloading = false
                }
            } catch {
                await MainActor.run {
                    downloadError = true
                    isDownloading = false
                }
            }
        }
    }

    private func decryptAndParseImage(encryptedString: String, passwords: [String]) -> UIImage? {
        let crypto = CryptoManager.shared
        for password in passwords {
            do {
                let data = try crypto.decryptWithPassword(
                    encryptedString, password: password, requireCompression: true
                )
                if let img = parseImageData(data) { return img }
            } catch {
                continue
            }
        }
        for password in passwords {
            do {
                let data = try crypto.decryptWithPassword(
                    encryptedString, password: password
                )
                if let img = parseImageData(data) { return img }
            } catch {
                continue
            }
        }
        return nil
    }

    /// Android parity: `base64ToBitmap()` with subsampling to max 4096px.
    private func parseImageData(_ data: Data) -> UIImage? {
        if let str = String(data: data, encoding: .utf8) {
            if str.hasPrefix("data:"),
               let commaIndex = str.firstIndex(of: ",") {
                let base64Part = String(str[str.index(after: commaIndex)...])
                if let imageData = Data(base64Encoded: base64Part),
                   let img = AttachmentCache.downsampledImage(from: imageData) {
                    return img
                }
            } else if let imageData = Data(base64Encoded: str),
                      let img = AttachmentCache.downsampledImage(from: imageData) {
                return img
            }
        }
        return AttachmentCache.downsampledImage(from: data)
    }

    // MARK: - Preview Parsing

    private func extractTag(from preview: String) -> String {
        AttachmentPreviewCodec.downloadTag(from: preview)
    }

    private func extractBlurHash(from preview: String) -> String {
        AttachmentPreviewCodec.blurHash(from: preview)
    }
}
