import AppKit
import Foundation
import UniformTypeIdentifiers

// MARK: - The drag types
//
// `UTType.laneworkCards` — a drag carrying board **cards**, live faces or trash rows
// (04-interactions.md ▸ Drag and drop, ▸ The trash) — and `.laneworkLanes`, the strip's own reorder
// and the cross-board lane transfer. Declared in `Info.plist` beside the clipboard type, because a
// system drag session is what crosses window boundaries, shows the copy badge, and gives the
// full-size replica preview (DRAG-REORDER.md § Cross-board sessions). The constants themselves live
// with the clipboard type in `PasteboardTypes.swift`, exported once by the app that owns them.

// MARK: - What a drag carries

/// Which of the board's two levels a drag session carries. The selection is homogeneous by kind
/// (04-interactions.md § Selection), so a session is one or the other and never both.
enum DragKind: String, Codable, Sendable, Equatable {
    case cards
    case lanes

    var type: UTType {
        switch self {
        case .cards: .laneworkCards
        case .lanes: .laneworkLanes
        }
    }
}

/// The JSON a drag session puts on the pasteboard.
///
/// **Same-app transfer is the only real consumer.** Both boards are open in this app, so the hover
/// path reads identity from `DragSession` — captured synchronously at drag start — and never decodes
/// anything mid-flight; the payload is the formal drop data, and what makes the session a *system*
/// session at all (which is what crosses windows and draws the badge). It carries folder URLs
/// because that is what the destination store's commits take (`receiveCards`, `receiveLanes`), and
/// the source board root beside them because locality — the Finder volume model — is a comparison of
/// roots (04-interactions.md ▸ Drag and drop).
///
/// A **plain-text secondary representation** (the dragged titles, newline-joined) rides along so a
/// stray drop into a text editor does something sane rather than nothing.
struct DragPayload: Codable, Sendable, Equatable {

    /// One dragged item: its UUID, its folder on disk, and its title for the text representation.
    struct Item: Codable, Sendable, Equatable {
        var id: String
        var folder: String
        var title: String?
    }

    /// The **source** board's root folder — the left-hand side of the locality comparison.
    var boardRoot: String

    var kind: DragKind

    /// Which container the drag started in — a trash card's drag is a card drag from `.trash`, which
    /// is the whole of what makes its within-board drop a restore (04-interactions.md ▸ The trash).
    /// `ItemContainer` is `String`-backed and `Codable` precisely so it can ride a pasteboard.
    var container: ItemContainer

    /// The dragged items **in flatten order** — "lane `order` first, then card `order`"
    /// (04-interactions.md ▸ Drag and drop). The drop commits trust this order rather than
    /// re-deriving it, because a cross-board destination has no flatten order for items it does not
    /// hold.
    var items: [Item]

    var ids: [ItemID] { items.map { ItemID(rawValue: $0.id) } }

    var folders: [URL] { items.map { URL(fileURLWithPath: $0.folder, isDirectory: true) } }

    var rootURL: URL { URL(fileURLWithPath: boardRoot, isDirectory: true) }

    /// The secondary representation: one title per line, untitled items rendered as they are on the
    /// board (03-board-ui.md § Card face — "Untitled" is a rendering, never a value).
    var plainText: String {
        items.map { $0.title ?? "Untitled" }.joined(separator: "\n")
    }

    // MARK: Coding

    func encoded() -> Data? {
        try? JSONEncoder().encode(self)
    }

    init(boardRoot: URL, kind: DragKind, container: ItemContainer, items: [Item]) {
        self.boardRoot = boardRoot.path
        self.kind = kind
        self.container = container
        self.items = items
    }

    init?(data: Data) {
        guard let decoded = try? JSONDecoder().decode(DragPayload.self, from: data) else { return nil }
        self = decoded
    }

    /// The item provider a `.onDrag` hands back: the JSON under this session's own type, plus the
    /// plain-text fallback.
    ///
    /// The custom type is registered `.ownProcess` deliberately — the payload names folders inside
    /// the user's boards, and no other app has any business reading it; the *text* is the
    /// representation other apps get.
    func itemProvider() -> NSItemProvider {
        let provider = NSItemProvider()
        if let data = encoded() {
            provider.registerDataRepresentation(
                forTypeIdentifier: kind.type.identifier,
                visibility: .ownProcess
            ) { completion in
                completion(data, nil)
                return nil
            }
        }
        let text = Data(plainText.utf8)
        provider.registerDataRepresentation(
            forTypeIdentifier: UTType.utf8PlainText.identifier,
            visibility: .all
        ) { completion in
            completion(text, nil)
            return nil
        }
        return provider
    }
}
