import SwiftUI
import AppKit
import UniformTypeIdentifiers
import NucleicCore

/// A file or piece of media staged in a composer but not yet sent. Backs the attachment chips
/// and is converted to a `PendingAttachment` (raw bytes) at send time — when a file source is
/// finally read off disk — so large media isn't held in memory while the user keeps typing.
struct ComposerAttachment: Identifiable, Equatable, Sendable {
    let id: UUID
    /// Display name and the base for the on-disk name; sanitized again in Core before it's written.
    let filename: String
    /// Where the bytes live: a file already on disk (attach button / dropped or pasted file), or
    /// in-memory data captured from a pasted/dragged image that has no file backing.
    enum Payload: Equatable, Sendable {
        case fileURL(URL)
        case data(Data)
    }
    let payload: Payload
    /// Best-known type, used to decide image-ness and pick a chip thumbnail.
    let utType: UTType?

    init(id: UUID = UUID(), filename: String, payload: Payload, utType: UTType?) {
        self.id = id
        self.filename = filename
        self.payload = payload
        self.utType = utType
    }

    var isImage: Bool { utType?.conforms(to: .image) ?? false }

    /// Read the bytes for sending. Reads a file source off disk (which can throw / be slow), so
    /// it's called on the send path, not during ingestion.
    func pending() throws -> PendingAttachment {
        switch payload {
        case .data(let data):
            return PendingAttachment(filename: filename, data: data)
        case .fileURL(let url):
            // Harmless when the app isn't sandboxed (returns false); needed if it ever is.
            let scoped = url.startAccessingSecurityScopedResource()
            defer { if scoped { url.stopAccessingSecurityScopedResource() } }
            return PendingAttachment(filename: filename, data: try Data(contentsOf: url))
        }
    }

    /// A small preview for the chip: the image itself for media, else the system file-type icon.
    @MainActor
    func thumbnail() -> NSImage {
        switch payload {
        case .data(let data):
            if let image = NSImage(data: data) { return image }
        case .fileURL(let url):
            if isImage, let image = NSImage(contentsOf: url) { return image }
            return NSWorkspace.shared.icon(forFile: url.path)
        }
        return NSWorkspace.shared.icon(for: utType ?? .data)
    }
}

// MARK: - Ingestion

extension ComposerAttachment {
    /// Wrap file URLs (from the attach panel, a dropped file, or a copied Finder item).
    static func fromFileURLs(_ urls: [URL]) -> [ComposerAttachment] {
        urls.filter(\.isFileURL).map { url in
            ComposerAttachment(
                filename: url.lastPathComponent,
                payload: .fileURL(url),
                utType: UTType(filenameExtension: url.pathExtension))
        }
    }

    /// Pull attachments off a pasteboard (a ⌘V or a drop). Prefers file URLs — keeping a file
    /// reference rather than copying bytes — and falls back to raster images (e.g. a screenshot
    /// or an image copied out of Preview), which arrive as `NSImage` with no file behind them.
    @MainActor
    static func fromPasteboard(_ pasteboard: NSPasteboard) -> [ComposerAttachment] {
        if let urls = pasteboard.readObjects(
            forClasses: [NSURL.self], options: [.urlReadingFileURLsOnly: true]) as? [URL],
            !urls.isEmpty
        {
            return fromFileURLs(urls)
        }
        if let images = pasteboard.readObjects(forClasses: [NSImage.self]) as? [NSImage],
            !images.isEmpty
        {
            return images.enumerated().compactMap { index, image in
                guard let data = image.pngData() else { return nil }
                let name = index == 0 ? "pasted-image.png" : "pasted-image-\(index + 1).png"
                return ComposerAttachment(filename: name, payload: .data(data), utType: .png)
            }
        }
        return []
    }

    /// Whether a pasteboard carries anything we'd take as an attachment — file URLs, or an image
    /// with no plain text alongside it. The text guard keeps an ordinary rich-text paste (which
    /// can also carry an image rep) flowing into the field as text instead of being hijacked.
    @MainActor
    static func pasteboardHasAttachments(_ pasteboard: NSPasteboard) -> Bool {
        let hasFileURLs = pasteboard.canReadObject(
            forClasses: [NSURL.self], options: [.urlReadingFileURLsOnly: true])
        if hasFileURLs { return true }
        let hasText = pasteboard.string(forType: .string)?.isEmpty == false
        return !hasText && pasteboard.canReadObject(forClasses: [NSImage.self])
    }

    // MARK: Drag

    /// Cheap type-only check (no image decode) of whether a *drag* carries media we'd take —
    /// file URLs or an image. Unlike the paste check there's no text guard: dragging in media is
    /// always intentional, even when the source also offers a URL string alongside it. Used to
    /// gate the drag cursor on every `draggingUpdated`, so it must stay cheap.
    @MainActor
    static func pasteboardHasDraggableMedia(_ pasteboard: NSPasteboard) -> Bool {
        if pasteboard.canReadObject(
            forClasses: [NSURL.self], options: [.urlReadingFileURLsOnly: true]) { return true }
        return pasteboard.canReadObject(forClasses: [NSImage.self])
    }
}

private extension NSImage {
    /// PNG encoding of the image's bitmap, for staging a pasted/dragged raster image.
    func pngData() -> Data? {
        guard let tiff = tiffRepresentation, let rep = NSBitmapImageRep(data: tiff) else { return nil }
        return rep.representation(using: .png, properties: [:])
    }
}

// MARK: - Views

/// The row of staged-attachment chips shown above the text field while the composer has any.
struct ComposerAttachmentBar: View {
    @Binding var attachments: [ComposerAttachment]

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 8) {
                ForEach(attachments) { attachment in
                    ComposerAttachmentChip(attachment: attachment) {
                        attachments.removeAll { $0.id == attachment.id }
                    }
                }
            }
            .padding(.vertical, 2)
        }
    }
}

/// A single attachment chip: a thumbnail, the (truncated) name, and a remove button.
struct ComposerAttachmentChip: View {
    let attachment: ComposerAttachment
    var onRemove: () -> Void
    @State private var hovering = false

    var body: some View {
        HStack(spacing: 6) {
            Image(nsImage: attachment.thumbnail())
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(width: 26, height: 26)
                .clipShape(RoundedRectangle(cornerRadius: 4))
                .overlay(RoundedRectangle(cornerRadius: 4).strokeBorder(AppTheme.hairline, lineWidth: 0.5))
            Text(attachment.filename)
                .font(.caption)
                .lineLimit(1)
                .truncationMode(.middle)
                .frame(maxWidth: 130, alignment: .leading)
            Button(action: onRemove) {
                Image(systemName: "xmark.circle.fill")
                    .foregroundStyle(.secondary)
            }
            .buttonStyle(.plain)
            .help("Remove attachment")
        }
        .padding(.leading, 5)
        .padding(.trailing, 6)
        .padding(.vertical, 4)
        .background(AppTheme.surface, in: .rect(cornerRadius: 8))
        .overlay(RoundedRectangle(cornerRadius: 8).strokeBorder(AppTheme.hairline, lineWidth: 1))
        .opacity(hovering ? 0.85 : 1)
        .onHover { hovering = $0 }
        .help(attachment.filename)
    }
}

/// The paperclip button that opens a file picker to attach files. Shared by both composers.
struct ComposerAttachButton: View {
    var isEnabled: Bool
    var onPick: ([ComposerAttachment]) -> Void
    @State private var importing = false

    var body: some View {
        Button {
            importing = true
        } label: {
            Image(systemName: "paperclip")
                .font(.system(size: 15))
                .foregroundStyle(.secondary)
                .frame(width: 24, height: 24)
                .contentShape(Rectangle())
        }
        .buttonStyle(.plain)
        .disabled(!isEnabled)
        .help("Attach files or images")
        .fileImporter(
            isPresented: $importing,
            allowedContentTypes: [.item],
            allowsMultipleSelection: true
        ) { result in
            if case .success(let urls) = result {
                onPick(ComposerAttachment.fromFileURLs(urls))
            }
        }
    }
}
