import CoreGraphics
import Foundation
import ImageIO
import UniformTypeIdentifiers
import os

// MARK: - PastedImage

/// **A picture on the pasteboard, as a file this app could write** (04-interactions.md ▸ Clipboard,
/// the image-data branch ruled 2026-08-09) — which flavor to take, what the landed file is called,
/// and whether the bytes travel verbatim or are re-encoded on the way.
///
/// ### Why this is a pure rule with no pasteboard in it
///
/// `NewCardTarget` and `PasteTarget`'s reason, one layer over: every clause below is a *decision*
/// about a list of type identifiers, and a decision that can be a value function should be one — the
/// menu item's `disabled`, the paste's own refusal, and the tests all read the same answer instead of
/// three hand-kept-in-sync conditions. The bytes are fetched by whoever owns the pasteboard seam
/// (`ClipboardStore`); nothing here touches `NSPasteboard`.
///
/// ### The precedence, which is the whole of the classification
///
/// 1. **The app's own clipboard type wins outright.** A Lanework copy on the pasteboard is a board
///    payload and pastes as cards or lanes exactly as it always did — an image flavor riding beside
///    it (there is none today, but a future manifest could carry a preview) must never divert ⌘V.
/// 2. **File URLs are somebody else's branch.** A Finder copy puts `public.file-url` down, sometimes
///    with an image flavor beside it, and "the pasteboard's payload is IMAGE DATA (no file URL)" is
///    the ruling's own parenthesis. A file URL is a *reference* to something the user already has
///    filed; taking a second copy of it into `attachments/` behind their back would be a different
///    gesture with a different answer, so this branch declines outright — and `ClipboardStore`'s own
///    file-URL branch (`pasteFiles`, ruled 2026-08-09) takes the reference at its word instead,
///    importing the file itself rather than a re-encoded copy of its bytes. A Finder-copied image
///    file therefore lands as the file it is, never as a second "Pasted Image.png" beside it.
/// 3. **Raw image data is the fallback**, which is the screenshot (⌃⇧⌘4), the browser's Copy Image,
///    and Preview's ⌘C.
///
/// ### The format rule
///
/// **A file-shaped flavor is kept byte for byte** — PNG, JPEG, GIF, HEIC, WebP. These are the
/// encodings a file on disk is already written in, so re-encoding one would cost either fidelity (a
/// JPEG round-tripped through PNG is bigger *and* still carries the original's artefacts) or the
/// picture itself (a GIF's animation does not survive a single-frame decode). The extension is that
/// type's own preferred one, so the landed file opens in Preview and QuickLooks in the sidebar with
/// no ceremony.
///
/// **Everything else is re-encoded to PNG** — in practice `public.tiff`, AppKit's lossless
/// interchange flavor, which is what a screenshot and a Preview copy put down beside their PNG and
/// what a great many apps offer *instead* of one. TIFF is an interchange encoding rather than a file
/// people want sitting in a card's folder: a 5K screenshot is tens of megabytes as TIFF and about a
/// tenth of that as PNG, both lossless. `.bmp` rides the same branch for the same reason.
///
/// The preference order is PNG first and TIFF last, so the overwhelmingly common paste — a screenshot
/// offering `public.png` and `public.tiff` together — lands as the PNG it already is, with no decode
/// and no re-encode at all.
public enum PastedImage {

    // MARK: The name

    /// **The name a pasted image lands under** — Finder's own shape for a file that arrives with no
    /// name of its own ("Pasted Image.png", then "Pasted Image 2.png", …).
    ///
    /// Only the stem is here: the extension is the flavor's (`Flavor.fileExtension`), and the
    /// collision ladder is `BoardWriter.freshName`'s, reached by handing the import path a temporary
    /// file with this name on it. Nothing in this app climbs a second ladder — "Finder-style rename
    /// on collision is one rule wherever the app has to find a free name".
    public static let baseName = "Pasted Image"

    /// **The board backdrop's own stem** — the same rule one level up (03-board-ui.md § Styling ▸
    /// Capabilities), so a pasted background is as recognizable in a board folder as a pasted
    /// attachment is in a card's.
    ///
    /// Its own constant rather than `baseName` reused: the two files land in different folders for
    /// different reasons, and a board folder holding something called "Pasted Image.png" would say
    /// nothing about what it is for. `FacetsGenerator.fileName` is the sibling this is modeled on.
    public static let backgroundBaseName = "Pasted Background"

    // MARK: The flavor

    /// One readable image payload on the pasteboard: where to read it from, what to write, and what
    /// to call it.
    public struct Flavor: Equatable, Sendable {

        /// The pasteboard type identifier the bytes come from.
        public let type: String

        /// The landed file's extension — the flavor's own for a verbatim write, `png` for a
        /// converted one.
        public let fileExtension: String

        /// Whether the bytes are re-encoded on the way (see the type comment's format rule).
        public let convertsToPNG: Bool

        /// The file name a paste of this flavor mints, before the Finder ladder ever sees it.
        public var fileName: String { "\(PastedImage.baseName).\(fileExtension)" }

        /// The board-backdrop file name for the same flavor.
        public var backgroundFileName: String { "\(PastedImage.backgroundBaseName).\(fileExtension)" }
    }

    // MARK: Classification

    /// The flavors kept verbatim, **in this app's preference order** — not the pasteboard's, which
    /// is the *owner's* ranking of what it thinks a taker wants and has no idea a file is about to
    /// be written.
    ///
    /// PNG leads because it is lossless, universally readable, and the flavor a screenshot already
    /// carries. JPEG follows so a photograph copied out of a browser lands as the JPEG it is rather
    /// than as a PNG several times its size. GIF, HEIC and WebP are here so that a source offering
    /// only one of them is still a paste rather than a refusal.
    public static let verbatimTypes: [UTType] = [.png, .jpeg, .gif, .heic, .webP]

    /// The flavors re-encoded to PNG — the interchange bitmaps (see the type comment).
    public static let convertedTypes: [UTType] = [.tiff, .bmp]

    /// What this pasteboard offers the image branch, or `nil` when the branch does not apply.
    ///
    /// - Parameter hasBoardItems: whether the app's own clipboard type is present and readable —
    ///   clause 1 of the precedence. Passed in rather than read here because deciding *that* is
    ///   `ClipboardManifest`'s job and this type has no pasteboard.
    /// - Parameter types: every type identifier the pasteboard currently carries.
    public static func flavor(hasBoardItems: Bool, types: [String]) -> Flavor? {
        guard !hasBoardItems, !carriesFileURL(types) else { return nil }

        let offered = Set(types)
        for type in verbatimTypes where offered.contains(type.identifier) {
            // A registered type with no preferred extension is not something a file can be named
            // after; skipping it lets the ladder fall through to the converted branch rather than
            // minting "Pasted Image." with nothing after the dot.
            guard let ext = type.preferredFilenameExtension else { continue }
            return Flavor(type: type.identifier, fileExtension: ext, convertsToPNG: false)
        }
        for type in convertedTypes where offered.contains(type.identifier) {
            return Flavor(type: type.identifier, fileExtension: "png", convertsToPNG: true)
        }
        return nil
    }

    /// Whether the pasteboard is carrying a file reference — clause 2 of the precedence.
    ///
    /// Conformance rather than equality with `public.file-url`, for `FinderDrop.isDirectory
    /// (typeIdentifiers:)`'s reason: a source is free to declare a subtype of it, and the rule is
    /// about what the value *is*. A type the system does not know is not a file URL, which is the
    /// same optimistic reading a drag's unknown types get.
    public static func carriesFileURL(_ types: [String]) -> Bool {
        types.contains { UTType($0)?.conforms(to: .fileURL) ?? false }
    }

    // MARK: The bytes

    private static let logger = Logger(subsystem: "dev.rzen.indie.Kanban", category: "paste-image")

    /// The bytes this flavor writes, given what the pasteboard handed over — `raw` itself for a
    /// verbatim flavor, a PNG re-encoding for a converted one, and `nil` when the payload turns out
    /// not to be a decodable image at all.
    ///
    /// A `nil` here is the honest refusal: the pasteboard *declared* a type it cannot back up, and
    /// writing an unreadable file into somebody's card folder is worse than doing nothing. The
    /// caller treats it exactly as it treats an empty pasteboard.
    public static func encode(_ raw: Data, as flavor: Flavor) -> Data? {
        guard flavor.convertsToPNG else { return raw.isEmpty ? nil : raw }
        return pngData(from: raw)
    }

    /// A bitmap payload re-encoded as PNG, through ImageIO.
    ///
    /// **ImageIO rather than `NSBitmapImageRep`**, which is the same call `BoardBackdrop.decode`
    /// makes and for the same reasons: it is the framework that actually owns the codecs, it is
    /// `Sendable`-clean and main-actor-free, and it needs no AppKit image cache in the middle. The
    /// full image is decoded rather than a thumbnail — this is a *conversion*, and downsampling a
    /// picture the user pasted would silently cost them resolution they never agreed to lose.
    ///
    /// Alpha survives, because a PNG destination writing a CGImage with an alpha channel keeps it —
    /// which matters for exactly the payload this branch sees most, a screenshot of a rounded window.
    public static func pngData(from data: Data) -> Data? {
        guard let source = CGImageSourceCreateWithData(data as CFData, nil),
              let image = CGImageSourceCreateImageAtIndex(
                  source, 0, [kCGImageSourceShouldCacheImmediately: true] as CFDictionary
              )
        else {
            logger.debug("pasteboard bitmap could not be decoded")
            return nil
        }
        let output = NSMutableData()
        guard let destination = CGImageDestinationCreateWithData(
            output, UTType.png.identifier as CFString, 1, nil
        ) else { return nil }
        CGImageDestinationAddImage(destination, image, nil)
        guard CGImageDestinationFinalize(destination) else {
            logger.debug("pasteboard bitmap could not be re-encoded as PNG")
            return nil
        }
        return output as Data
    }

    // MARK: Which attachments can be a hero

    /// Whether an attachment named `name` is one **"Set as Hero" may point at** — an image, by its
    /// name's own extension (05-card-window.md ▸ Attachments; 03-board-ui.md § Card face ▸ Hero
    /// image).
    ///
    /// **By extension rather than by opening the file**, deliberately. The context menu is built
    /// while the pointer is going down on a row, and the sidebar may be showing a hundred of them;
    /// a per-row `CGImageSourceCreateWithURL` to decide whether a menu row is offered is exactly the
    /// kind of disk touch the card window keeps out of a body evaluation. The cost of being wrong is
    /// nothing either way: a name whose extension lies renders as no band at all (`CardHeroImage`'s
    /// structural degrade), and an image the system does not recognize by extension is simply not
    /// offered the row — the `hero` key is still hand-writable, which is what it was born as.
    public static func isImageName(_ name: String) -> Bool {
        let ext = (name as NSString).pathExtension
        guard !ext.isEmpty, let type = UTType(filenameExtension: ext) else { return false }
        return type.conforms(to: .image)
    }
}
