import AppKit
import SwiftUI
import UniformTypeIdentifiers

// MARK: - CardCommentsPane

/// The card window's **comments pane** — the middle of the three componentized panes
/// (05-card-window.md ▸ Composition, ▸ The comments column).
///
/// ### It does not know where it is mounted
///
/// Beside the body or under it, the pane is identical — "the panes are identical in both mounts" —
/// so nothing in this file asks. It fills the frame it is given, scrolls its own content, and the
/// arrangement is `CardWindowView`'s (`CommentsMount`). That is the componentization the 2026-07-29
/// re-composition asks for, stated as an absence: there is no layout parameter here to get wrong.
///
/// ### Header, thread, composer — and the composer is at the newest end
///
/// > The composer sits at the thread's newest end (bottom ascending, top descending) and the window
/// > opens scrolled to it — a thread opens where the conversation is happening.
///
/// Both halves come from one value (`CommentSortDirection.placesComposerFirst`), so the scroll target
/// and the composer's position cannot disagree — a window that opened at the wrong end would be wrong
/// only for the users who had flipped the sort, which is exactly the bug that ships.
struct CardCommentsPane: View {

    let comments: CardComments
    /// The **card**'s folder — what relative images and links in every comment resolve against.
    let cardFolder: URL?
    /// The window's thumbnail memory, shared with the sidebar's attachment rows so a file shown in
    /// both places is rendered once.
    let thumbnails: AttachmentThumbnailCache

    /// **App-wide and persisted** (05 ▸ The comments column; 11-command-nexus.md files the header
    /// control under Configuration controls). Read here rather than mirrored onto the window's handle
    /// because there is exactly one of it and every open pane obeys it.
    @AppStorage(AppPreferences.commentsNewestFirstKey) private var newestFirst = false

    /// The composer's scroll anchor. A constant rather than a generated id because there is one
    /// composer and two possible places for it.
    private static let composerAnchor = "comments.composer"

    private var pointSize: CGFloat { CardWindowMetrics.bodyPointSize }
    private var direction: CommentSortDirection { CommentSortDirection(newestFirst: newestFirst) }
    private var ordered: [Comment] { direction.apply(to: comments.thread.comments) }

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            header
                .padding(.horizontal, CardWindowMetrics.gutter(bodyPointSize: pointSize))
                .padding(.top, CardWindowMetrics.gutter(bodyPointSize: pointSize))

            thread
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
        // 10-accessibility.md's container label for the pane ("Comments, N"). The elements inside it
        // and their custom actions are phase 3's; the container is here because the pane would
        // otherwise be an unnamed region the moment it exists.
        .accessibilityElement(children: .contain)
        .accessibilityLabel("Comments, \(comments.thread.comments.count)")
    }

    // MARK: - Header

    /// "Comments · 3" with the sort control beside it — the sidebar's own small-caps section header,
    /// shared rather than restated so the pane and the sidebar read as one window (05: "the section
    /// header carries the count and the sort-direction control").
    private var header: some View {
        CardSidebarSectionHeader(title: CommentsHeader.title(count: comments.thread.comments.count)) {
            sortControl
        }
    }

    /// The sort-direction control — **Tab-reachable beside the count** (11-command-nexus.md ▸
    /// Configuration controls).
    ///
    /// A button rather than a segmented picker: there are two states and the second one is the
    /// reverse of the first, so a toggle whose glyph says which way the thread currently runs is the
    /// smaller thing that says the same. Its help text and its accessibility label are the same
    /// string (`CommentSortDirection.controlLabel`) — one label, two readers.
    private var sortControl: some View {
        Button {
            newestFirst.toggle()
        } label: {
            Image(systemName: direction == .ascending ? "arrow.down" : "arrow.up")
                .font(.caption.weight(.semibold))
                .foregroundStyle(.secondary)
        }
        .buttonStyle(.plain)
        .help(direction.controlLabel)
        .accessibilityLabel("Sort")
        .accessibilityValue(direction.controlLabel)
    }

    // MARK: - The thread

    private var thread: some View {
        ScrollViewReader { proxy in
            ScrollView(.vertical) {
                LazyVStack(alignment: .leading, spacing: CardWindowMetrics.commentSpacing(bodyPointSize: pointSize)) {
                    if direction.placesComposerFirst {
                        composer.id(Self.composerAnchor)
                    }
                    ForEach(ordered) { comment in
                        CommentRowView(
                            comment: comment,
                            comments: comments,
                            cardFolder: cardFolder,
                            thumbnails: thumbnails
                        )
                        .id(comment.id)
                    }
                    if !direction.placesComposerFirst {
                        composer.id(Self.composerAnchor)
                    }
                }
                .padding(CardWindowMetrics.gutter(bodyPointSize: pointSize))
                .frame(maxWidth: .infinity, alignment: .leading)
            }
            // **The window opens scrolled to the composer** (05). Deferred one turn rather than run
            // inline: `scrollTo` needs the content laid out to have somewhere to scroll to, and a
            // thread's rows measure their own rendered height (`CommentBodyView`).
            .task {
                await Task.yield()
                proxy.scrollTo(Self.composerAnchor, anchor: direction.placesComposerFirst ? .top : .bottom)
            }
            // File ▸ Add Comment focuses the composer — which is no use if the composer is off
            // screen, so the same request scrolls to it. One request, both effects.
            .onChange(of: comments.focusComposerRequests) { _, _ in
                proxy.scrollTo(Self.composerAnchor, anchor: direction.placesComposerFirst ? .top : .bottom)
            }
        }
    }

    // MARK: - The composer

    /// **The composer edits `comments/.draft/`** — always visible, at the thread's newest end
    /// (05 ▸ The comments column).
    private var composer: some View {
        CommentComposerView(comments: comments, thumbnails: thumbnails)
    }
}

// MARK: - The authoring surfaces' drop carve-out

/// **A file dropped within an authoring surface's bounds lands in *that* surface's `attachments/`**
/// (05-card-window.md ▸ Attachments, ruled 2026-07-29 — the hover-target carve-out on the
/// window-wide card default).
///
/// The *arbitration* is SwiftUI's own innermost-target dispatch: this delegate is attached **inside**
/// `CardWindowDropDelegate`'s region, so a drag released over the composer or over an open inline
/// editor is offered here first and never reaches the window's card default.
///
/// The *destination* is `CommentDropCarveOut`, which is why this takes a hover rather than a folder:
/// the surface says what the pointer is over and the pure rule says where the files go, so the ruling
/// — including which authoring surface wins where they would ever overlap — is checkable without a
/// window and cannot drift from what the delegate actually does. A hover the rule resolves to the
/// **card** never reaches here at all (no authoring surface is under the pointer, so no authoring
/// surface has a drop target on screen), and this refuses it rather than guessing a folder.
///
/// Everything else is `CardWindowDropDelegate`'s, deliberately: the same payload predicate (files,
/// not folders, not text), the same read-only refusal, the same `.copy` badge, and the same
/// asynchronous URL load with the sandbox's security scope around it. Only the destination differs,
/// which is the entire point of the carve-out.
struct CommentAttachmentDropDelegate: DropDelegate {

    let comments: CardComments
    /// What the pointer is over, as this surface knows it — see `CommentDropCarveOut.Hover`.
    let hover: CommentDropCarveOut.Hover

    /// Where the rule says the files go, or `nil` for the window-wide card default.
    private var target: CommentTarget? {
        guard case let .comment(target) = CommentDropCarveOut.landing(for: hover) else { return nil }
        return target
    }

    private var acceptsFileDrops: Bool { comments.isEditable && target != nil }

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

    func dropUpdated(info: DropInfo) -> DropProposal? {
        DropProposal(operation: validateDrop(info: info) ? .copy : .cancel)
    }

    func performDrop(info: DropInfo) -> Bool {
        guard acceptsFileDrops, let target else { return false }
        let providers = info.itemProviders(for: [.fileURL])
        guard !providers.isEmpty else { return false }

        let comments = comments
        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 }
            comments.importFiles(urls, to: target)
        }
        return true
    }
}
