import SwiftUI

// MARK: - One comment

/// One comment in the thread: **an author line, the rendered Markdown body, and attachment chips when
/// its `attachments/` is non-empty** (05-card-window.md ▸ The comments column).
///
/// ### No avatars
///
/// "There is no identity system, and initials faked from self-reported strings would be decoration."
/// So the row's whole identity surface is a line of secondary text, and a comment with no `author`
/// renders **without a name** rather than with a placeholder standing in for one
/// (`CommentAuthorLine`).
///
/// ### The row is two views, not one with a mode
///
/// While an inline edit session names this comment, the body and its read-only chips are replaced by
/// the authoring surface — the *same* authoring surface the composer uses
/// (`CommentAuthoringSurface`), aimed at this comment's folder. That is what makes "the same pair
/// applies within an inline comment edit session" true by construction rather than by two views
/// agreeing.
struct CommentRowView: View {

    let comment: Comment
    let comments: CardComments
    let cardFolder: URL?
    let thumbnails: AttachmentThumbnailCache

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

    /// The session open over *this* comment, or `nil` — one at a time, window-wide
    /// (`CardComments.editing`).
    private var session: CommentEditSession? {
        guard let editing = comments.editing, editing.commentID == comment.id else { return nil }
        return editing
    }

    var body: some View {
        VStack(alignment: .leading, spacing: padding) {
            if let session {
                authorText
                editor(session)
            } else {
                reading

                if !comment.attachments.isEmpty {
                    // **Read-only** — "a posted comment's chips are read-only, Quick Look only — Edit
                    // the comment to change its files" (05). `onRemove` left `nil` is that sentence,
                    // and the editing branch above draws its *own* removable chips inside the
                    // authoring surface, which is why these are the reading branch's alone.
                    //
                    // They sit **outside** the flattened element deliberately: 10 flattens "author,
                    // date, edited state, body", and the chips are the one part of a comment that is
                    // not text but a row of controls with a Quick Look behind each — reachable exactly
                    // as the sidebar's are (`CommentAttachmentChips`). Flattening them in would have
                    // made a comment's files announceable but not openable.
                    CommentAttachmentChips(
                        names: comment.attachments,
                        url: { comments.attachmentURL($0, in: .comment(comment.id)) },
                        thumbnails: thumbnails
                    )
                }
            }
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .contentShape(Rectangle())
        .contextMenu { menu }
        .accessibilityElement(children: .contain)
    }

    // MARK: - Reading

    /// The author line and the rendered body, as **one flattened element** with the three custom
    /// actions (10-accessibility.md ▸ Comments — see `CommentRowAccessibility`).
    private var reading: some View {
        VStack(alignment: .leading, spacing: padding) {
            authorText

            CommentBodyView(
                body: comment.body,
                cardFolder: cardFolder,
                // The find's hits in this comment, and whether the current one is here — the row draws
                // them, the session found them (`CommentThreadFind`).
                highlights: comments.find.matches(in: comment.id),
                currentHighlight: comments.find.currentMatch.flatMap {
                    $0.comment == comment.id ? $0.range : nil
                },
                focus: comments
            )
            .frame(maxWidth: .infinity, alignment: .leading)
        }
        .modifier(CommentRowAccessibility(comment: comment, comments: comments, authorLine: authorLine))
    }

    /// The author line, or nothing — "a comment with no `author` renders **without a name**"
    /// (`CommentAuthorLine`).
    @ViewBuilder
    private var authorText: some View {
        if let line = authorLine {
            Text(line)
                .font(.caption)
                .foregroundStyle(.secondary)
                .textSelection(.enabled)
        }
    }

    // MARK: - Editing

    /// The inline session's surface: the composer's chrome, aimed at this comment, with Save and
    /// Cancel where the composer has Comment.
    private func editor(_ session: CommentEditSession) -> some View {
        CommentAuthoringSurface(
            comments: comments,
            target: .comment(comment.id),
            text: session.text,
            height: CardWindowMetrics.inlineEditorHeight(bodyPointSize: pointSize),
            // A constant, and it is enough: this editor is mounted by the session opening and
            // unmounted by it ending, so its coordinator sees exactly one transition from the
            // never-requested 0 — Edit puts the caret in the text, once, without a counter of its
            // own (contrast the composer, which is always mounted and needs one).
            focusRequest: 1,
            attachments: comment.attachments,
            thumbnails: thumbnails,
            onEdit: { session.edited($0) },
            onCommandReturn: { comments.commitEdit() },
            // **Escape is Cancel's keyboard twin** (ruled 2026-07-29; 11-command-nexus.md's grammar
            // table) — unlike the composer's Escape, which never discards, because a session *has* a
            // start state to go back to and a draft has not.
            onEscape: { comments.cancelEdit() }
        ) {
            // ⌘↩ is Save's chord and Escape is Cancel's, both intercepted by the editor's own text
            // view — see the composer's Comment button for why neither is `.defaultAction`.
            Button("Cancel") { comments.cancelEdit() }
                .controlSize(.small)
            Button("Save") { comments.commitEdit() }
                .buttonStyle(.borderedProminent)
                .controlSize(.small)
                .disabled(!comments.isEditable)
        }
    }

    // MARK: - The context menu

    /// **Edit / Delete / Reveal in Finder** — the per-item action inventory 11-command-nexus.md
    /// inventories for a comment, and the surface VoiceOver reads.
    ///
    /// Delete is "immediate and undoable, no confirm" (05; 01's ruling — the comment moves into
    /// `comments/.trash/` and ⌘Z is the move back), so there is no confirmation sheet here and no
    /// destructive-role ceremony beyond the divider that separates it.
    @ViewBuilder
    private var menu: some View {
        Button("Edit") { comments.beginEdit(comment.id) }
            .disabled(!comments.isEditable)
        Button("Reveal in Finder") { comments.reveal(comment.id) }
        Divider()
        Button("Delete") { comments.delete(comment.id) }
            .disabled(!comments.isEditable)
    }

    // MARK: - The author line

    private var authorLine: String? {
        CommentAuthorLine.text(
            author: comment.author.value,
            timestamp: comment.created.value.map(Self.timestamp),
            isEdited: comment.isEdited
        )
    }

    /// The card's own created/modified line's format, shared so a comment's timestamp and its card's
    /// read the same way.
    private static func timestamp(_ date: Date) -> String {
        date.formatted(date: .abbreviated, time: .shortened)
    }
}

// MARK: - The row as one element

/// **One comment is one flattened element with three custom actions** (10-accessibility.md ▸
/// Comments):
///
/// > each comment is **one flattened element** — author, date, edited state, body — with its
/// > context-menu rows (Edit / Delete / Reveal in Finder) riding as custom actions per the cut.
///
/// ### It covers the reading surface only, and that is the whole of where it is applied
///
/// While an inline edit session is open the row is a text editor with two buttons in it, and
/// collapsing *that* into one opaque element would put a VoiceOver user in front of a comment they
/// can neither read into nor type into. So this hangs on the reading branch alone
/// (`CommentRowView.reading`), and the editing branch is an ordinary container — the same asymmetry
/// the body column keeps between Preview and Edit.
///
/// The actions duplicate the context menu deliberately: 10 asks for the pointer inventory to be
/// reachable without the pointer, and the strings are shared with the menu (`AccessibilityPhrases`)
/// so the two inventories cannot drift. They are also **not** disabled under the read-only lock —
/// they call the same handles the menu rows do, and those already refuse (`CardComments.beginEdit`,
/// `.delete`), which keeps one refusal rather than two.
private struct CommentRowAccessibility: ViewModifier {

    let comment: Comment
    let comments: CardComments
    let authorLine: String?

    func body(content: Content) -> some View {
        content
            .accessibilityElement(children: .ignore)
            .accessibilityLabel(AccessibilityPhrases.commentLabel(authorLine: authorLine))
            .accessibilityValue(AccessibilityPhrases.commentValue(
                body: comment.body,
                attachments: comment.attachments.count
            ))
            .accessibilityAction(named: AccessibilityPhrases.commentEditAction) {
                comments.beginEdit(comment.id)
            }
            .accessibilityAction(named: AccessibilityPhrases.commentDeleteAction) {
                comments.delete(comment.id)
            }
            .accessibilityAction(named: AccessibilityPhrases.commentRevealAction) {
                comments.reveal(comment.id)
            }
    }
}
