import SwiftUI
import UniformTypeIdentifiers

// MARK: - The payload split

/// **Drop precedence is split by payload** (05-card-window.md ▸ Attachments, settled): "file drops
/// import as attachments anywhere in the window — Edit mode included, the text editor never
/// intercepts a file drop; dragged *text* lands in the Edit editor at the caret within its bounds as
/// ordinary insertion, and is inert elsewhere in the window."
///
/// ### The split is enforced from both ends, and neither end knows about the other
///
/// - **The editor declines files.** `CardBodyTextView.acceptableDragTypes` filters `public.file-url`
///   (and the Carbon-era `NSFilenamesPboardType` AppKit still puts beside it) out of what the text
///   view registers for, so AppKit's drag hit-test walks *past* the text view to the window-level
///   drop target above it. That is the whole of "the text editor never intercepts a file drop" — a
///   deregistration, not a handler that re-dispatches.
/// - **The window declines text.** The delegate below is attached with `.onDrop(of: [.fileURL], …)`
///   and its `validateDrop` asks the predicate here, so a text drag never matches it at all and
///   AppKit offers the drag to the text view instead — which takes it as an ordinary insertion at
///   the caret, `NSTextView`'s own behaviour, untouched. Outside the editor's bounds nothing accepts
///   it, which is 05's "inert everywhere else".
///
/// Two deregistrations meeting in the middle, rather than one arbiter deciding: there is no third
/// state where both accept, and no ordering between them to get wrong.
enum CardWindowDrop {

    /// Whether a drag's declared types make it a **file** payload — the window's — as opposed to
    /// text, which is the editor's.
    ///
    /// Conformance to `public.file-url`, not equality: a Finder drag registers the concrete type
    /// (`public.png`) beside the file URL, and a synthetic drag may register a subtype of it. A URL
    /// dragged out of a browser is `public.url`, which does *not* conform to `public.file-url` — so
    /// it stays the editor's, exactly as the rule says a dragged link should.
    nonisolated static func isFilePayload(typeIdentifiers: [String]) -> Bool {
        typeIdentifiers.contains { identifier in
            UTType(identifier)?.conforms(to: .fileURL) ?? false
        }
    }

    /// Whether the window will import this drag: **at least one payload that is a file and not a
    /// directory.**
    ///
    /// The folder half is `FinderDrop.isDirectory(typeIdentifiers:)` — the board's own hover read,
    /// called rather than re-derived, so "a package is a directory" cannot come to mean two things
    /// in one app. A folders-only drag answers `false` here and is therefore an incompatible payload
    /// at the cursor: no highlight, a refusal cursor, and nothing written. A mixed drag answers
    /// `true`, imports its files, and names the folders it skipped in a loss row (`FinderDrop.land`)
    /// — the board-side refusal semantics, applied here because the importer they protect is the
    /// same one.
    nonisolated static func accepts(payloads: [[String]]) -> Bool {
        payloads.contains { types in
            isFilePayload(typeIdentifiers: types) && !FinderDrop.isDirectory(typeIdentifiers: types)
        }
    }
}

// MARK: - The window-wide drop delegate

/// **The drop surface is the whole window** (05-card-window.md ▸ Attachments) — one delegate over
/// the card window's entire content area, body column and sidebar alike, Edit mode and the
/// raw-source outlet included.
///
/// It is attached at the top of `CardWindowView`'s body rather than to the attachments section,
/// which is what the design asks for and what makes the rule cheap: there is exactly one drop region
/// in this window, so there is no single-target-dispatch problem to solve here at all (contrast the
/// board, where lane, card and strip regions overlap — `BoardDrops`). The text view is *inside* this
/// region and simply does not accept the file types, so the drag falls through to it.
///
/// The write is `BoardStore.importAttachments(_:toCard:)`, reached through `FinderDrop.land` — the
/// same store method, the same Finder-style collision rename, the same banners as the board window's
/// drop onto a card face. Nothing about importing an attachment is re-implemented here; only *which
/// card* is decided differently, and in a card window that is not a decision at all.
struct CardWindowDropDelegate: DropDelegate {

    let store: BoardStore
    let cardID: ItemID

    /// **The mutating-gesture rule, applied to the one gesture that arrives from outside the app**
    /// (`BoardDropContext.acceptsFileDrops`): under the read-only lock a file drop refuses at the
    /// window, with no proposal — the board's own posture, and the same predicate. The board's
    /// second clause (an inline title editor focused) has no card-window counterpart: this window's
    /// editors are the body and the raw-source outlet, and 05 puts a file drop *through* both of
    /// them on purpose.
    private var acceptsFileDrops: Bool {
        !store.isReadOnly
    }

    func validateDrop(info: DropInfo) -> Bool {
        guard acceptsFileDrops else { return false }
        return CardWindowDrop.accepts(
            payloads: info.itemProviders(for: [.fileURL]).map(\.registeredTypeIdentifiers)
        )
    }

    /// Always `.copy` for a payload we will take — importing a file leaves the original where it
    /// was, which is what the badge should say — and `.cancel` for one we will not.
    func dropUpdated(info: DropInfo) -> DropProposal? {
        DropProposal(operation: validateDrop(info: info) ? .copy : .cancel)
    }

    /// Commits the drop. `BoardDropContext.commitFileDrop`'s shape, minus the board's hover state:
    /// a provider's file URL loads asynchronously (it is never synchronous for a Finder drag), the
    /// store call happens back on the main actor, and the resolved URLs — not the declared types —
    /// are the authority on what is a folder.
    func performDrop(info: DropInfo) -> Bool {
        guard acceptsFileDrops else { return false }
        let providers = info.itemProviders(for: [.fileURL])
        guard !providers.isEmpty else { return false }

        let store = store
        let cardID = cardID
        Task { @MainActor in
            var urls: [URL] = []
            for provider in providers {
                if let url = await FileDropLoading.url(from: provider) { urls.append(url) }
            }
            guard !urls.isEmpty else { return }

            // The sandbox's half: a Finder drag hands the app an extension for what it dropped, and
            // the copy is the read that needs it. `start…` answers false for a URL that carries no
            // scope of its own, so only the ones that opened are closed again.
            let scoped = urls.filter { $0.startAccessingSecurityScopedResource() }
            defer { for url in scoped { url.stopAccessingSecurityScopedResource() } }

            FinderDrop.land(urls, landing: .attach(cardID: cardID), into: store)
        }
        return true
    }
}
