import AppKit
import SwiftUI
import UniformTypeIdentifiers

// MARK: - The shared authoring chrome

/// What the composer and an inline comment edit session **both** are: a Markdown editor, the
/// surface's own attachment chips with remove, a quiet paperclip, and a drop target aimed at this
/// surface's folder (05-card-window.md ▸ The comments column, ruled 2026-07-29 — "the same pair
/// applies within an inline comment edit session, targeting that comment's").
///
/// It is one view rather than two near-copies because the design states the composer's rules and then
/// says "and the same for an inline edit". Two implementations of that sentence would be two places
/// for the paperclip to open a different panel or for a drop to land in the wrong folder — and the
/// wrong folder is not a bug a user can see until they go looking in Finder.
///
/// What differs between the two surfaces arrives as parameters and nothing more: which buffer, what
/// ⌘↩ means, what Escape means, the placeholder, the height, and the buttons underneath.
struct CommentAuthoringSurface<Actions: View>: View {

    let comments: CardComments
    /// Which folder this surface's files land in — the draft's, or the comment being edited.
    let target: CommentTarget
    let text: String
    /// Shown over an empty editor. `nil` on the inline editor, which opens over text that exists.
    var placeholder: String?
    let height: CGFloat
    var focusRequest: Int = 0
    let attachments: [String]
    let thumbnails: AttachmentThumbnailCache
    let onEdit: (String) -> Void
    let onCommandReturn: () -> Void
    let onEscape: () -> Void
    var onBlur: () -> Void = {}
    @ViewBuilder var actions: Actions

    private var pointSize: CGFloat { CardWindowMetrics.bodyPointSize }
    private var padding: CGFloat { CardWindowMetrics.previewPadding(bodyPointSize: pointSize) }

    /// What the pointer is over while this surface is under it — the carve-out's input, derived from
    /// the surface's own identity rather than passed in beside it, so a surface can only ever report
    /// being itself (`CommentDropCarveOut`).
    private var hover: CommentDropCarveOut.Hover {
        switch target {
        case .draft: CommentDropCarveOut.Hover(isOverComposer: true)
        case let .comment(id): CommentDropCarveOut.Hover(inlineEdit: id)
        }
    }

    var body: some View {
        VStack(alignment: .leading, spacing: padding) {
            editor
            if !attachments.isEmpty {
                CommentAttachmentChips(
                    names: attachments,
                    url: { comments.attachmentURL($0, in: target) },
                    thumbnails: thumbnails,
                    // **Authoring chips carry remove** — to the system Trash, never a hard delete.
                    onRemove: comments.isEditable ? { comments.removeFile(named: $0, from: target) } : nil
                )
            }
            HStack(spacing: padding) {
                paperclip
                Spacer(minLength: 0)
                actions
            }
        }
        // The carve-out's whole mechanism: a drop target *inside* the window-wide one, so SwiftUI
        // offers this surface the drag first (`CommentAttachmentDropDelegate`).
        .onDrop(
            of: [.fileURL],
            delegate: CommentAttachmentDropDelegate(comments: comments, hover: hover)
        )
    }

    // MARK: Editor

    private var editor: some View {
        ZStack(alignment: .topLeading) {
            CommentTextEditor(
                text: text,
                // Under the read-only lock the buffer stays alive and only its saves suspend
                // (02-architecture.md § the lock's scope) — but a *composer* under the lock has
                // nothing to suspend into, so the editor disables in place like every other
                // mutation entry point in this window.
                isEditable: comments.isEditable,
                onEdit: onEdit,
                onCommandReturn: onCommandReturn,
                onEscape: onEscape,
                onBlur: onBlur,
                focusRequest: focusRequest,
                // Both authoring surfaces report themselves to the pane's focus register, which is
                // what routes ⌘F here to the editor's own find bar rather than to the thread's
                // (05 ▸ Preview; `CardWindowFind.route`).
                focus: comments
            )
            .frame(height: height)

            if let placeholder, text.isEmpty {
                Text(placeholder)
                    .font(.body)
                    .foregroundStyle(.tertiary)
                    .padding(.horizontal, padding + 5)
                    .padding(.vertical, padding)
                    // A label, not a control: clicks belong to the editor underneath it.
                    .allowsHitTesting(false)
            }
        }
        .background(.background.secondary, in: RoundedRectangle(cornerRadius: 6, style: .continuous))
        .overlay(
            RoundedRectangle(cornerRadius: 6, style: .continuous)
                .strokeBorder(.quaternary)
        )
    }

    /// The **quiet paperclip** — "a pointer twin" of nothing at all in the menu bar, deliberately:
    /// "File ▸ Add Attachment… stays card-scoped" (05 ▸ The comments column), so this surface's
    /// no-drag path is the affordance and only the affordance. It opens the same panel the sidebar's
    /// plus does (`AttachmentPanel`), differing in one line of guidance.
    private var paperclip: some View {
        Button {
            comments.addAttachments(to: target)
        } label: {
            Image(systemName: "paperclip")
                .font(.caption.weight(.semibold))
                .foregroundStyle(.secondary)
        }
        .buttonStyle(.plain)
        .disabled(!comments.isEditable)
        .help("Attach Files…")
        .accessibilityLabel(AccessibilityPhrases.commentAttachFilesLabel)
    }
}

// MARK: - The composer

/// **The composer** — an always-visible text area whose backing file is `comments/.draft/`
/// (05-card-window.md ▸ The comments column).
///
/// ### Restore-on-reopen is not implemented here, and that is the design
///
/// > The composer edits `comments/.draft/` … Restore-on-reopen falls out for free (the composer just
/// > reads its file).
///
/// The window's open reads the draft (`CardComments.reload`) and the session adopts it; there is no
/// restore path, no per-window memory, and nothing to clear. A draft written on another machine and
/// synced in arrives the same way, because it is the same read.
///
/// ### Escape never discards
///
/// "Escape moves focus out of the composer, draft untouched" (ruled 2026-07-29). Resigning first
/// responder is *also* a blur, which is one of the four cadence moments — so Escape saves the draft
/// rather than losing it, which is the exact opposite of what Escape means in a transient bubble and
/// is why the design had to say so out loud.
struct CommentComposerView: View {

    let comments: CardComments
    let thumbnails: AttachmentThumbnailCache

    private var session: CommentDraftSession { comments.composer }
    private var pointSize: CGFloat { CardWindowMetrics.bodyPointSize }

    var body: some View {
        CommentAuthoringSurface(
            comments: comments,
            target: .draft,
            text: session.text,
            placeholder: "Add a comment…",
            height: CardWindowMetrics.composerHeight(bodyPointSize: pointSize),
            focusRequest: comments.focusComposerRequests,
            attachments: session.attachments,
            thumbnails: thumbnails,
            onEdit: { session.edited($0) },
            onCommandReturn: post,
            onEscape: Self.resignFocus,
            onBlur: { session.blurred() }
        ) {
            // **The Comment button twins ⌘↩** (05) — one act, two pointers at it, so the button calls
            // exactly what the chord calls.
            //
            // Prominent styling rather than `.defaultAction`, deliberately: the chord this gesture
            // owns is ⌘↩ (11-command-nexus.md's grammar table), which the editor's own text view
            // intercepts, and a default-action binding would additionally claim plain Return in a
            // window that already has two authoring surfaces able to claim it at once.
            Button("Comment", action: post)
                .buttonStyle(.borderedProminent)
                .controlSize(.small)
                .disabled(!comments.isEditable || !session.canPost)
        }
        // "The composer is a labeled text field (⌘↩ posts)" — 10-accessibility.md ▸ Comments. The
        // label is the placeholder's own sentence, so what a sighted user reads in the empty editor
        // and what VoiceOver announces are the same invitation.
        .accessibilityLabel(AccessibilityPhrases.commentComposerLabel)
    }

    /// Post, then re-read: the rename moved a folder into the thread, and the pane shows the thread.
    ///
    /// The re-read is explicit rather than left to the watcher's reload because a post is a gesture
    /// with a visible result — the comment appearing — and waiting a debounce for FSEvents would make
    /// the app look like it had not heard the ⌘↩. The reload lands afterwards and finds the same
    /// thing.
    private func post() {
        guard comments.isEditable, session.canPost else { return }
        guard session.postNow() != nil else { return }
        comments.reload()
    }

    /// Escape's whole implementation: **move focus out**, which the blur then saves. Nothing is
    /// discarded, because there is nothing here that could be — the draft is a durable file.
    private static func resignFocus() {
        NSApp.keyWindow?.makeFirstResponder(nil)
    }
}
