import AppKit
import IHatePDFsCore
import SwiftUI

struct LeftSidebarView: View {
    @EnvironmentObject private var appState: AppState

    var body: some View {
        VStack(spacing: 0) {
            LeftSidebarModeSwitcher(selection: $appState.leftSidebarMode)
                .padding(.horizontal, 10)
                .padding(.vertical, 8)

            Divider()

            switch appState.leftSidebarMode {
            case .pages:
                PDFThumbnailRepresentedView()
                    .padding(.vertical, 6)
            case .annotations:
                AnnotationNavigationListView()
            }
        }
        .background(.bar)
    }
}

private struct LeftSidebarModeSwitcher: View {
    @Binding var selection: LeftSidebarMode

    var body: some View {
        Picker("Left Sidebar", selection: $selection) {
            Text("Pages").tag(LeftSidebarMode.pages)
            Text("Marks").tag(LeftSidebarMode.annotations)
        }
        .pickerStyle(.segmented)
        .controlSize(.small)
        .padding(.vertical, 7)
        .padding(.horizontal, 6)
        .labelsHidden()
    }
}

private struct AnnotationNavigationListView: View {
    @EnvironmentObject private var appState: AppState

    var body: some View {
        VStack(spacing: 0) {
            SidebarSectionHeader(
                title: "Annotations",
                count: appState.annotations.count,
                systemImage: "list.bullet.rectangle"
            )

            Divider()

            if appState.annotations.isEmpty {
                SidebarEmptyState(
                    systemImage: "list.bullet.rectangle",
                    title: "No annotations",
                    message: "Highlights, underlines, and comments will appear here."
                )
            } else {
                ScrollView {
                    LazyVStack(alignment: .leading, spacing: 0) {
                        ForEach(appState.annotations) { item in
                            AnnotationNavigationRow(item: item)
                        }
                    }
                    .padding(.vertical, 4)
                }
            }
        }
    }
}

private struct AnnotationNavigationRow: View {
    @EnvironmentObject private var appState: AppState
    @Environment(\.colorScheme) private var colorScheme
    let item: AnnotationSnapshot

    private var previewText: String {
        if item.kind == .highlight, !item.hasComment {
            return item.highlightExcerpt
        }

        if item.kind == .reply {
            return item.contents.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
                ? "Reply"
                : item.contents
        }

        return item.firstLine
    }

    private var submetadataText: String {
        var parts = ["Page \(item.pageLabel)"]
        if !item.author.isEmpty {
            parts.append(item.author)
        }
        return parts.joined(separator: " - ")
    }

    private var dateText: String? {
        let date = item.modifiedAt ?? item.createdAt
        return date?.formatted(date: .abbreviated, time: .omitted)
    }

    var body: some View {
        Button {
            appState.select(item)
        } label: {
            HStack(alignment: .top, spacing: 8) {
                Image(systemName: item.kind.symbolName)
                    .font(.system(size: 13, weight: .semibold))
                    .foregroundStyle(item.id == appState.selectedAnnotationID ? Color.accentColor : InterfacePalette.secondaryText(for: colorScheme))
                    .frame(width: 16, height: 18)

                VStack(alignment: .leading, spacing: 4) {
                    HStack(alignment: .firstTextBaseline, spacing: 5) {
                        Text(item.kind.displayName)
                            .font(.caption.weight(.semibold))
                            .foregroundStyle(InterfacePalette.primaryText(for: colorScheme))
                            .lineLimit(1)

                        Spacer(minLength: 4)

                        if let dateText {
                            Text(dateText)
                                .font(.caption2)
                                .foregroundStyle(InterfacePalette.quietText(for: colorScheme))
                                .lineLimit(1)
                        }
                    }

                    Text(submetadataText)
                        .font(.caption2)
                        .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
                        .lineLimit(1)

                    Text(previewText)
                        .font(.caption)
                        .foregroundStyle(InterfacePalette.primaryText(for: colorScheme))
                        .lineLimit(2)
                        .fixedSize(horizontal: false, vertical: true)
                }
            }
            .padding(.horizontal, 10)
            .padding(.vertical, 7)
            .frame(maxWidth: .infinity, alignment: .leading)
            .contentShape(Rectangle())
            .background(item.id == appState.selectedAnnotationID ? InterfacePalette.selectedRowFill(for: colorScheme) : Color.clear)
        }
        .buttonStyle(.plain)
        .help("Go to \(item.kind.displayName.lowercased()) on page \(item.pageLabel)")
        .onHover { isHovered in
            appState.setCommentHover(item, isHovered: isHovered)
        }
    }
}

struct RightSidebarView: View {
    @EnvironmentObject private var appState: AppState

    var body: some View {
        VStack(spacing: 0) {
            SidebarModeSwitcher(selection: $appState.sidebarMode)
                .padding(.horizontal, 12)
                .padding(.vertical, 10)

            Divider()

            switch appState.sidebarMode {
            case .annotations, .pages:
                CommentsReviewSidebar()
            case .highlights:
                HighlightedTextListView()
            }
        }
        .background(.bar)
    }
}

private struct SidebarModeSwitcher: View {
    @Binding var selection: SidebarMode

    var body: some View {
        Picker("Right Sidebar", selection: $selection) {
            Text("Comments").tag(SidebarMode.annotations)
            Text("Highlights").tag(SidebarMode.highlights)
        }
        .pickerStyle(.segmented)
        .controlSize(.small)
        .padding(.vertical, 7)
        .padding(.horizontal, 6)
        .labelsHidden()
    }
}

private struct HighlightedTextListView: View {
    @EnvironmentObject private var appState: AppState
    @Environment(\.colorScheme) private var colorScheme

    private var totalHighlightCount: Int {
        appState.highlightedTextItems.count
    }

    var body: some View {
        VStack(spacing: 0) {
            highlightHeader

            Divider()

            if appState.highlightedTextGroups.isEmpty {
                SidebarEmptyState(
                    systemImage: "highlighter",
                    title: "No highlighted text",
                    message: "Highlighted passages will appear here."
                )
            } else {
                ScrollView {
                    LazyVStack(alignment: .leading, spacing: 0) {
                        switch appState.highlightSortMode {
                        case .color:
                            ForEach(appState.highlightedTextGroups, id: \.id) { group in
                                HighlightGroupView(group: group)
                            }
                        case .page:
                            ForEach(appState.highlightedTextItems) { item in
                                HighlightRow(item: item, showsColorSwatch: true)
                            }
                        }
                    }
                    .padding(.vertical, 4)
                }
            }
        }
    }

    private var highlightHeader: some View {
        HStack(spacing: 8) {
            Image(systemName: "highlighter")
                .font(.system(size: 15, weight: .semibold))
                .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))

            Text("Highlights")
                .font(.headline)
                .lineLimit(1)

            Text("\(totalHighlightCount)")
                .font(.headline.monospacedDigit())
                .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))

            Spacer()

            Button {
                appState.highlightSortMode = appState.highlightSortMode == .color ? .page : .color
            } label: {
                Image(systemName: appState.highlightSortMode.systemImage)
                    .font(.system(size: 11, weight: .semibold))
                    .frame(width: 22, height: 22)
                    .contentShape(Rectangle())
            }
            .buttonStyle(.plain)
            .controlSize(.mini)
            .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
            .help("Sort highlights by \(appState.highlightSortMode == .color ? "page" : "color")")
            .accessibilityLabel("Toggle Highlight Sort")
        }
        .padding(.horizontal, 10)
        .padding(.vertical, 9)
    }
}

private struct HighlightGroupView: View {
    let group: HighlightedTextGroup
    @EnvironmentObject private var appState: AppState
    @Environment(\.colorScheme) private var colorScheme

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            HStack(spacing: 8) {
                Capsule()
                    .fill(group.color)
                    .frame(width: 34, height: 7)
                    .overlay {
                        Capsule()
                            .stroke(InterfacePalette.hairline(for: colorScheme), lineWidth: 0.6)
                    }

                Text(group.title)
                    .font(.caption.weight(.semibold))
                    .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
                    .lineLimit(1)

                Spacer()

                Text("\(group.items.count)")
                    .font(.caption.monospacedDigit())
                    .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
            }
            .padding(.horizontal, 10)
            .padding(.top, 8)
            .padding(.bottom, 5)
            .help(group.title)

            ForEach(group.items) { item in
                HighlightRow(item: item, showsColorSwatch: false)
            }
        }
    }
}

private struct HighlightRow: View {
    @EnvironmentObject private var appState: AppState
    @Environment(\.colorScheme) private var colorScheme
    @State private var isRowHovered = false
    let item: AnnotationSnapshot
    let showsColorSwatch: Bool

    private var swatchColor: Color {
        Color(nsColor: AppSettings.displayColor(forHighlightColor: item.annotation.color))
    }

    var body: some View {
        HStack(alignment: .top, spacing: 6) {
            Button {
                appState.selectHighlightedText(item)
            } label: {
                highlightSummary
            }
            .buttonStyle(.plain)
            .help("Go to highlight on page \(item.pageLabel)")

            Button(role: .destructive) {
                appState.delete(item)
            } label: {
                Image(systemName: "trash")
                    .font(.caption2.weight(.semibold))
                    .frame(width: 20, height: 20)
                    .contentShape(Rectangle())
            }
            .buttonStyle(.plain)
            .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
            .help("Delete Highlight")
            .accessibilityLabel("Delete Highlight")
            .opacity(isRowHovered || item.id == appState.selectedAnnotationID ? 1 : 0.68)
        }
        .padding(.horizontal, 10)
        .padding(.vertical, 7)
        .frame(maxWidth: .infinity, alignment: .leading)
        .contentShape(Rectangle())
        .background(item.id == appState.selectedAnnotationID ? InterfacePalette.selectedRowFill(for: colorScheme) : Color.clear)
        .onHover { isHovered in
            isRowHovered = isHovered
            appState.setCommentHover(item, isHovered: isHovered)
        }
        .contextMenu {
            Button(role: .destructive) {
                appState.delete(item)
            } label: {
                Label("Delete Highlight", systemImage: "trash")
            }
        }
    }

    private var highlightSummary: some View {
        HStack(alignment: .top, spacing: 8) {
            Image(systemName: item.id == appState.selectedAnnotationID ? "largecircle.fill.circle" : "circle")
                .font(.caption2.weight(.semibold))
                .foregroundStyle(item.id == appState.selectedAnnotationID ? Color.accentColor : InterfacePalette.quietText(for: colorScheme))
                .frame(width: 14, height: 18)

            if showsColorSwatch {
                Capsule()
                    .fill(swatchColor)
                    .frame(width: 20, height: 6)
                    .overlay {
                        Capsule()
                            .stroke(InterfacePalette.hairline(for: colorScheme), lineWidth: 0.6)
                    }
                    .padding(.top, 6)
            }

            VStack(alignment: .leading, spacing: 4) {
                Text("p. \(item.pageLabel)")
                    .font(.caption2.weight(.medium))
                    .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))

                Text(item.highlightExcerpt)
                    .font(.caption)
                    .foregroundStyle(InterfacePalette.primaryText(for: colorScheme))
                    .lineLimit(3)
                    .fixedSize(horizontal: false, vertical: true)
            }
            .frame(maxWidth: .infinity, alignment: .leading)
        }
        .frame(maxWidth: .infinity, alignment: .leading)
        .contentShape(Rectangle())
    }
}

private struct SidebarSectionHeader: View {
    @Environment(\.colorScheme) private var colorScheme
    let title: String
    let count: Int
    let systemImage: String

    var body: some View {
        HStack(spacing: 8) {
            Image(systemName: systemImage)
                .font(.system(size: 15, weight: .semibold))
                .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
            Text(title)
                .font(.headline)
                .lineLimit(1)
            Text("\(count)")
                .font(.headline.monospacedDigit())
                .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
            Spacer()
        }
        .padding(.horizontal, 10)
        .padding(.vertical, 9)
    }
}

private struct SidebarEmptyState: View {
    @Environment(\.colorScheme) private var colorScheme
    let systemImage: String
    let title: String
    let message: String

    var body: some View {
        VStack(spacing: 9) {
            Image(systemName: systemImage)
                .font(.system(size: 28, weight: .regular))
                .foregroundStyle(InterfacePalette.quietText(for: colorScheme))
            Text(title)
                .font(.callout.weight(.semibold))
                .foregroundStyle(InterfacePalette.primaryText(for: colorScheme))
            Text(message)
                .font(.caption)
                .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
                .multilineTextAlignment(.center)
                .fixedSize(horizontal: false, vertical: true)
        }
        .padding(18)
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct CommentsReviewSidebar: View {
    @EnvironmentObject private var appState: AppState
    @Environment(\.colorScheme) private var colorScheme
    @State private var showsSearch = false
    @State private var showsFilters = false
    @State private var showsAdvancedFilters = false
    @FocusState private var isCommentSearchFocused: Bool

    private var groupedComments: [(pageIndex: Int, items: [AnnotationSnapshot])] {
        let grouped = Dictionary(grouping: appState.topLevelComments, by: \.pageIndex)
        return grouped
            .map { (pageIndex: $0.key, items: $0.value) }
            .sorted { $0.pageIndex < $1.pageIndex }
    }

    private var visibleCommentCount: Int {
        appState.topLevelComments.reduce(0) { partial, item in
            partial + 1 + (appState.repliesByParent[item.id]?.count ?? 0)
        }
    }

    private var totalCommentCount: Int {
        appState.annotations.reduce(0) { partial, item in
            let isVisibleReviewItem = item.isReply ? item.hasComment : item.kind != .highlight || item.hasComment
            return partial + (isVisibleReviewItem ? 1 : 0)
        }
    }

    private var isFilteringComments: Bool {
        hasActiveCommentSearch || hasActiveCommentFilters
    }

    private var hasActiveCommentSearch: Bool {
        !appState.commentSearchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
    }

    private var hasActiveCommentFilters: Bool {
        appState.commentFilter != .all
            || appState.selectedKindFilter != nil
            || appState.selectedAuthorFilter != "All Authors"
            || appState.selectedStatusFilter != ReviewState.allStatuses
    }

    var body: some View {
        VStack(spacing: 0) {
            header
            Divider()
            if isFilteringComments { filterSummary }
            if showsSearch || showsFilters {
                Divider()
                filters
            }
            Divider()
            commentList
        }
        .background(.bar)
    }

    private var header: some View {
        HStack(spacing: 7) {
            Image(systemName: "text.bubble.fill")
                .font(.system(size: 15, weight: .semibold))
                .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
                .help("Comments")

            Text("Comments")
                .font(.headline)
                .lineLimit(1)
                .minimumScaleFactor(0.85)
                .layoutPriority(1)

            Text(commentCountText)
                .font(.headline.monospacedDigit())
                .foregroundStyle(.secondary)
                .lineLimit(1)
                .fixedSize()

            Spacer()

            Button {
                showsSearch.toggle()
                if showsSearch {
                    focusCommentSearch()
                } else {
                    isCommentSearchFocused = false
                }
            } label: {
                Label(
                    "Search Comments",
                    systemImage: (showsSearch || hasActiveCommentSearch) ? "magnifyingglass.circle.fill" : "magnifyingglass"
                )
            }
            .labelStyle(.iconOnly)
            .buttonStyle(.plain)
            .frame(width: 24, height: 24)
            .contentShape(Rectangle())
            .foregroundStyle(hasActiveCommentSearch ? InterfacePalette.actionText(for: colorScheme) : InterfacePalette.secondaryText(for: colorScheme))
            .help("Search Comments")

            Button {
                showsFilters.toggle()
            } label: {
                Label(
                    "Filter Comments",
                    systemImage: (showsFilters || hasActiveCommentFilters) ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle"
                )
            }
            .labelStyle(.iconOnly)
            .buttonStyle(.plain)
            .frame(width: 24, height: 24)
            .contentShape(Rectangle())
            .foregroundStyle(hasActiveCommentFilters ? InterfacePalette.actionText(for: colorScheme) : InterfacePalette.secondaryText(for: colorScheme))
            .help("Filter Comments")

            if isFilteringComments {
                Button {
                    clearVisibleFilters()
                } label: {
                    Label("Clear Comment Filters", systemImage: "xmark.circle.fill")
                }
                .labelStyle(.iconOnly)
                .buttonStyle(.plain)
                .frame(width: 24, height: 24)
                .contentShape(Rectangle())
                .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
                .help("Clear Comment Filters")
                .accessibilityLabel("Clear Comment Filters")
            }
        }
        .padding(.horizontal, 10)
        .padding(.vertical, 9)
    }

    private var commentCountText: String {
        guard isFilteringComments, totalCommentCount > 0 else { return "\(visibleCommentCount)" }
        return "\(visibleCommentCount)/\(totalCommentCount)"
    }

    private var filters: some View {
        VStack(spacing: 8) {
            if showsSearch {
                commentSearchField
            }

            if showsFilters {
                Picker("Comment filter", selection: $appState.commentFilter) {
                    ForEach(CommentFilter.allCases, id: \.self) { filter in
                        Text(filter.title).tag(filter)
                    }
                }
                .pickerStyle(.segmented)
                .labelsHidden()

                DisclosureGroup("More Filters", isExpanded: $showsAdvancedFilters) {
                    VStack(spacing: 8) {
                        Picker("Type", selection: Binding(
                            get: { appState.selectedKindFilter },
                            set: { appState.selectedKindFilter = $0 }
                        )) {
                            Text("All Types").tag(Optional<AcademicAnnotationKind>.none)
                            ForEach(AcademicAnnotationKind.allCases.filter { $0 != .other }, id: \.self) { kind in
                                Text(kind.displayName).tag(Optional(kind))
                            }
                        }

                        Picker("Author", selection: $appState.selectedAuthorFilter) {
                            ForEach(appState.authors, id: \.self) { author in
                                Text(author).tag(author)
                            }
                        }

                        Picker("Status", selection: $appState.selectedStatusFilter) {
                            ForEach(appState.statuses, id: \.self) { status in
                                Text(status).tag(status)
                            }
                        }
                    }
                    .labelsHidden()
                    .padding(.top, 4)
                }
                .font(.caption)
            }
        }
        .padding(10)
    }

    private var filterSummary: some View {
        HStack(spacing: 8) {
            Label(commentCountText, systemImage: "line.3.horizontal.decrease.circle")
                .labelStyle(.titleAndIcon)
                .font(.caption.weight(.medium))
                .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
                .lineLimit(1)

            Spacer(minLength: 6)

            Button {
                clearVisibleFilters()
            } label: {
                Label("Clear", systemImage: "xmark.circle")
            }
            .labelStyle(.titleAndIcon)
            .font(.caption.weight(.medium))
            .buttonStyle(.plain)
            .foregroundStyle(InterfacePalette.actionText(for: colorScheme))
            .help("Clear Comment Filters")
        }
        .padding(.horizontal, 10)
        .frame(height: 30)
        .background(InterfacePalette.subtleFill(for: colorScheme))
    }

    private var commentSearchField: some View {
        ZStack(alignment: .trailing) {
            TextField("Search comments", text: $appState.commentSearchText)
                .textFieldStyle(.plain)
                .padding(.leading, 8)
                .padding(.trailing, hasActiveCommentSearch ? 28 : 8)
                .frame(height: 28)
                .background(InterfacePalette.fieldFill(for: colorScheme))
                .clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
                .overlay {
                    RoundedRectangle(cornerRadius: 6, style: .continuous)
                        .stroke(
                            isCommentSearchFocused ? Color.accentColor : InterfacePalette.hairline(for: colorScheme),
                            lineWidth: isCommentSearchFocused ? 1.4 : 1
                        )
                }
                .focused($isCommentSearchFocused)
                .onAppear {
                    isCommentSearchFocused = true
                }

            if hasActiveCommentSearch {
                Button {
                    appState.commentSearchText = ""
                    focusCommentSearch()
                } label: {
                    Image(systemName: "xmark.circle.fill")
                        .font(.system(size: 12, weight: .semibold))
                        .symbolRenderingMode(.hierarchical)
                }
                .buttonStyle(.plain)
                .foregroundStyle(.secondary)
                .padding(.trailing, 8)
                .help("Clear Search")
                .accessibilityLabel("Clear Search")
            }
        }
    }

    private func clearVisibleFilters() {
        appState.clearCommentFilters()
        if !showsSearch {
            isCommentSearchFocused = false
        }
    }

    private func focusCommentSearch() {
        isCommentSearchFocused = true
    }

    private var commentList: some View {
        Group {
            if groupedComments.isEmpty {
                CommentsEmptyState(isFiltering: isFilteringComments)
            } else {
                ScrollView {
                    LazyVStack(alignment: .leading, spacing: 0) {
                        ForEach(groupedComments, id: \.pageIndex) { group in
                            PageCommentGroup(
                                pageIndex: group.pageIndex,
                                items: group.items,
                                repliesByParent: appState.repliesByParent,
                                showsPageHeader: appState.pageCount > 1,
                                isFiltering: isFilteringComments
                            )
                        }
                    }
                    .padding(.vertical, 4)
                }
            }
        }
    }
}

private struct CommentsEmptyState: View {
    @EnvironmentObject private var appState: AppState
    @Environment(\.colorScheme) private var colorScheme
    let isFiltering: Bool

    var body: some View {
        VStack(spacing: 9) {
            Image(systemName: isFiltering ? "line.3.horizontal.decrease.circle" : "text.bubble")
                .font(.system(size: 28, weight: .regular))
                .foregroundStyle(InterfacePalette.quietText(for: colorScheme))

            Text(isFiltering ? "No matching comments" : "No comments yet")
                .font(.callout.weight(.semibold))
                .foregroundStyle(InterfacePalette.primaryText(for: colorScheme))

            Text(isFiltering ? "Adjust search or filters." : "Comments will appear here.")
                .font(.caption)
                .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
                .multilineTextAlignment(.center)
                .fixedSize(horizontal: false, vertical: true)

            if isFiltering {
                Button {
                    appState.clearCommentFilters()
                } label: {
                    Label("Clear Filters", systemImage: "xmark.circle")
                }
                .buttonStyle(.bordered)
                .controlSize(.small)
            }
        }
        .padding(18)
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

private struct PageCommentGroup: View {
    @EnvironmentObject private var appState: AppState
    @Environment(\.colorScheme) private var colorScheme
    let pageIndex: Int
    let items: [AnnotationSnapshot]
    let repliesByParent: [String: [AnnotationSnapshot]]
    let showsPageHeader: Bool
    let isFiltering: Bool

    private var isCollapsed: Bool {
        showsPageHeader && !isFiltering && appState.collapsedPageIndexes.contains(pageIndex)
    }

    private var visibleItemCount: Int {
        items.reduce(0) { partial, item in
            partial + 1 + (repliesByParent[item.id]?.count ?? 0)
        }
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            if showsPageHeader {
                if isFiltering {
                    pageHeader
                        .help("Filtered results are expanded")
                } else {
                    Button {
                        if isCollapsed {
                            appState.collapsedPageIndexes.remove(pageIndex)
                        } else {
                            appState.collapsedPageIndexes.insert(pageIndex)
                        }
                    } label: {
                        pageHeader
                    }
                    .buttonStyle(.plain)
                    .help(isCollapsed ? "Expand Page Comments" : "Collapse Page Comments")
                }
            }

            if !isCollapsed {
                ForEach(items) { item in
                    let replies = repliesByParent[item.id] ?? []
                    CommentRow(item: item, replies: replies)
                        .id(([item.sidebarRenderID] + replies.map(\.sidebarRenderID)).joined(separator: "|"))
                }
            }
        }
    }

    private var pageHeader: some View {
        HStack {
            Image(systemName: isCollapsed ? "chevron.right" : "chevron.down")
                .font(.caption2.weight(.semibold))
                .frame(width: 12)
                .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
            Text("Page \(pageIndex + 1)")
                .font(.caption.weight(.semibold))
                .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
            Spacer()
            Text("\(visibleItemCount)")
                .font(.caption.monospacedDigit())
                .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
        }
        .padding(.horizontal, 10)
        .padding(.top, 7)
        .padding(.bottom, 5)
    }
}

private struct CommentRow: View {
    @EnvironmentObject private var appState: AppState
    @Environment(\.colorScheme) private var colorScheme
    @State private var isRowHovered = false
    let item: AnnotationSnapshot
    let replies: [AnnotationSnapshot]

    var body: some View {
        ZStack(alignment: .topLeading) {
            if !replies.isEmpty {
                Rectangle()
                    .fill(InterfacePalette.connector(for: colorScheme))
                    .frame(width: 1)
                    .padding(.leading, 22)
                    .padding(.top, 30)
                    .padding(.bottom, 20)
            }

            VStack(alignment: .leading, spacing: 0) {
                parentComment

                ForEach(replies) { reply in
                    ReplyRow(item: reply, threadRoot: item)
                        .id(reply.sidebarRenderID)
                }

                if appState.sidebarReplyParentID == item.id {
                    SidebarReplyComposer(threadRoot: item)
                        .id("reply-composer-\(item.id)")
                }
            }
        }
        .padding(.horizontal, 8)
        .padding(.vertical, 6)
        .overlay(alignment: .bottom) {
            Rectangle()
                .fill(InterfacePalette.hairline(for: colorScheme))
                .frame(height: 1)
        }
    }

    private var parentComment: some View {
        HStack(alignment: .top, spacing: 8) {
            CommentMarker(symbolName: item.kind.symbolName, size: 28, font: .caption)
                .frame(width: 30, alignment: .center)
                .padding(.top, 2)
                .help(item.kind == .reply ? "Reply" : "Comment Thread")

            VStack(alignment: .leading, spacing: 5) {
                Button {
                    appState.select(item)
                } label: {
                    commentSummary
                        .frame(maxWidth: .infinity, alignment: .leading)
                }
                .buttonStyle(.plain)
                .contentShape(Rectangle())
                .accessibilityAddTraits(.isButton)
                .accessibilityAction {
                        appState.select(item)
                }

                HStack(spacing: 6) {
                    Text(item.kind.displayName)
                        .font(.caption2)
                        .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
                    ReviewStatusChip(item: item)
                    Spacer()
                    CommentReviewRowActions(
                        isVisible: isRowHovered || appState.sidebarReplyParentID == item.id,
                        onEdit: {
                            appState.edit(item)
                        },
                        onReply: {
                            appState.beginSidebarReply(to: item, inThread: item)
                        },
                        onDelete: {
                            appState.delete(item)
                        }
                    )
                }
            }
        }
        .padding(.horizontal, 8)
        .padding(.vertical, 8)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(rowFill(for: item))
        .clipShape(RoundedRectangle(cornerRadius: 7, style: .continuous))
        .contentShape(Rectangle())
        .onHover { isHovered in
            isRowHovered = isHovered
            appState.setCommentHover(item, isHovered: isHovered)
        }
        .contextMenu {
            Button {
                appState.edit(item)
            } label: {
                Label("Edit", systemImage: "pencil")
            }

            Button {
                appState.beginSidebarReply(to: item, inThread: item)
            } label: {
                Label("Reply", systemImage: "arrowshape.turn.up.left")
            }

            Button(role: .destructive) {
                appState.delete(item)
            } label: {
                Label("Delete", systemImage: "trash")
            }
        }
    }

    private var commentSummary: some View {
        VStack(alignment: .leading, spacing: 4) {
            HStack(alignment: .firstTextBaseline, spacing: 8) {
                Text(item.author)
                    .font(.caption.weight(.semibold))
                    .foregroundStyle(InterfacePalette.primaryText(for: colorScheme))
                    .lineLimit(1)

                Spacer()

                Text(dateString(item.modifiedAt ?? item.createdAt))
                    .font(.caption2)
                    .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
                    .lineLimit(1)
            }

            if item.hasComment {
                Text(item.contents)
                    .font(.callout)
                    .foregroundStyle(InterfacePalette.primaryText(for: colorScheme))
                    .fixedSize(horizontal: false, vertical: true)
            } else if replies.isEmpty {
                Text("No comment text")
                    .font(.callout)
                    .foregroundStyle(InterfacePalette.quietText(for: colorScheme))
                    .fixedSize(horizontal: false, vertical: true)
            }
        }
    }

    private func rowFill(for item: AnnotationSnapshot) -> Color {
        if item.id == appState.selectedAnnotationID {
            return InterfacePalette.selectedRowFill(for: colorScheme)
        }
        if isRowHovered {
            return InterfacePalette.subtleFill(for: colorScheme)
        }
        return Color.clear
    }

    private func dateString(_ date: Date?) -> String {
        guard let date else { return "No date" }
        return date.formatted(date: .abbreviated, time: .shortened)
    }
}

private extension AnnotationSnapshot {
    var sidebarRenderID: String {
        [
            id,
            author,
            contents,
            status,
            String(modifiedAt?.timeIntervalSinceReferenceDate ?? 0),
            String(describing: bounds.minX),
            String(describing: bounds.minY)
        ].joined(separator: "|")
    }
}

private struct CommentMarker: View {
    @Environment(\.colorScheme) private var colorScheme
    let symbolName: String
    let size: CGFloat
    let font: Font

    var body: some View {
        ZStack {
            Circle()
                .fill(InterfacePalette.markerFill(for: colorScheme))
            Circle()
                .stroke(InterfacePalette.markerStroke(for: colorScheme), lineWidth: 0.75)
            Image(systemName: symbolName)
                .font(font)
                .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
        }
        .frame(width: size, height: size)
        .clipShape(Circle())
    }
}

private struct SidebarReplyComposer: View {
    @EnvironmentObject private var appState: AppState
    @Environment(\.colorScheme) private var colorScheme
    let threadRoot: AnnotationSnapshot

    private let editorHorizontalInset: CGFloat = 7
    private let editorVerticalInset: CGFloat = 6

    var body: some View {
        HStack(alignment: .top, spacing: 8) {
            CommentMarker(symbolName: "arrowshape.turn.up.left", size: 22, font: .caption2)
                .frame(width: 28, alignment: .center)
                .padding(.top, 9)
                .help("Reply")

            VStack(alignment: .leading, spacing: 7) {
                HStack(alignment: .firstTextBaseline, spacing: 6) {
                    Text("Reply")
                        .font(.caption.weight(.semibold))
                        .foregroundStyle(InterfacePalette.primaryText(for: colorScheme))

                    if let target = appState.sidebarReplyTarget {
                        Text("to \(target.author)")
                            .font(.caption2)
                            .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
                            .lineLimit(1)
                    } else {
                        Text("to \(threadRoot.author)")
                            .font(.caption2)
                            .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
                            .lineLimit(1)
                    }

                    Spacer()
                }

                ZStack(alignment: .topLeading) {
                    CommitTextView(
                        text: $appState.sidebarReplyDraft,
                        font: NSFont.systemFont(ofSize: NSFont.systemFontSize),
                        onCommit: {
                            if !appState.sidebarReplyDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
                                appState.commitSidebarReply()
                            }
                        }
                    )
                        .padding(.horizontal, editorHorizontalInset)
                        .padding(.vertical, editorVerticalInset)

                    if appState.sidebarReplyDraft.isEmpty {
                        Text("Write a reply")
                            .font(.callout)
                            .foregroundStyle(InterfacePalette.quietText(for: colorScheme))
                            .padding(.leading, editorHorizontalInset + 6)
                            .padding(.top, editorVerticalInset)
                            .allowsHitTesting(false)
                    }
                }
                .frame(minHeight: 76)
                .background(InterfacePalette.fieldFill(for: colorScheme))
                .clipShape(RoundedRectangle(cornerRadius: 6))
                .overlay {
                    RoundedRectangle(cornerRadius: 6)
                        .stroke(InterfacePalette.hairline(for: colorScheme), lineWidth: 1)
                }

                HStack(spacing: 8) {
                    TextField("Author", text: $appState.sidebarReplyAuthor)
                        .textFieldStyle(.plain)
                        .foregroundStyle(InterfacePalette.primaryText(for: colorScheme))
                        .onSubmit {
                            if !appState.sidebarReplyDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
                                appState.commitSidebarReply()
                            }
                        }
                        .padding(.horizontal, 7)
                        .frame(height: 26)
                        .background(InterfacePalette.fieldFill(for: colorScheme))
                        .clipShape(RoundedRectangle(cornerRadius: 6))
                        .overlay {
                            RoundedRectangle(cornerRadius: 6)
                                .stroke(InterfacePalette.hairline(for: colorScheme), lineWidth: 1)
                        }

                    Spacer()

                    Button("Cancel") {
                        appState.cancelSidebarReply()
                    }
                    .buttonStyle(.plain)
                    .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))

                    Button {
                        appState.commitSidebarReply()
                    } label: {
                        Label("Reply", systemImage: "arrowshape.turn.up.left")
                    }
                    .disabled(appState.sidebarReplyDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
                    .keyboardShortcut(.return, modifiers: [.command])
                }
                .font(.caption.weight(.medium))
            }
        }
        .padding(.top, 9)
        .padding(.bottom, 2)
    }
}

private struct ReviewStatusChip: View {
    @EnvironmentObject private var appState: AppState
    @Environment(\.colorScheme) private var colorScheme
    let item: AnnotationSnapshot

    var body: some View {
        Button {
            appState.toggleReviewed(item)
        } label: {
            HStack(spacing: 4) {
                if isReviewed {
                    Image(systemName: "checkmark")
                        .font(.caption2.weight(.bold))
                }

                Text(label)
                    .font(.caption2)
            }
            .foregroundStyle(foreground)
            .padding(.horizontal, 7)
            .padding(.vertical, 2)
            .background(background)
            .clipShape(Capsule())
        }
        .buttonStyle(.plain)
        .help(isReviewed ? "Mark as not reviewed" : "Mark as reviewed")
    }

    private var isReviewed: Bool {
        ReviewState.isReviewed(item.status)
    }

    private var label: String {
        ReviewState.label(for: item.status)
    }

    private var foreground: Color {
        isReviewed
            ? InterfacePalette.actionText(for: colorScheme)
            : InterfacePalette.quietText(for: colorScheme)
    }

    private var background: Color {
        if isReviewed {
            return Color(nsColor: .controlAccentColor).opacity(colorScheme == .dark ? 0.16 : 0.11)
        }
        return InterfacePalette.subtleFill(for: colorScheme)
    }
}

private struct ReplyRow: View {
    @EnvironmentObject private var appState: AppState
    @Environment(\.colorScheme) private var colorScheme
    @State private var isRowHovered = false
    let item: AnnotationSnapshot
    let threadRoot: AnnotationSnapshot

    var body: some View {
        HStack(alignment: .top, spacing: 8) {
            CommentMarker(symbolName: "text.bubble", size: 22, font: .caption2)
                .frame(width: 28, alignment: .center)
                .padding(.top, 2)
                .help("Reply")

            VStack(alignment: .leading, spacing: 4) {
                Button {
                    appState.select(item)
                } label: {
                    replySummary
                        .frame(maxWidth: .infinity, alignment: .leading)
                }
                .buttonStyle(.plain)
                .contentShape(Rectangle())
                .accessibilityAddTraits(.isButton)
                .accessibilityAction {
                        appState.select(item)
                }

                HStack(spacing: 6) {
                    Text("Reply")
                        .font(.caption2)
                        .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
                    ReviewStatusChip(item: item)
                    Spacer()
                    CommentReviewRowActions(
                        isVisible: isRowHovered || appState.sidebarReplyParentID == threadRoot.id,
                        onEdit: {
                            appState.edit(item)
                        },
                        onReply: {
                            appState.beginSidebarReply(to: item, inThread: threadRoot)
                        },
                        onDelete: {
                            appState.delete(item)
                        }
                    )
                }
            }
        }
        .padding(.horizontal, 8)
        .padding(.vertical, 7)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(rowFill)
        .clipShape(RoundedRectangle(cornerRadius: 7, style: .continuous))
        .contentShape(Rectangle())
        .padding(.top, 4)
        .onHover { isHovered in
            isRowHovered = isHovered
            appState.setCommentHover(item, isHovered: isHovered)
        }
        .contextMenu {
            Button {
                appState.edit(item)
            } label: {
                Label("Edit", systemImage: "pencil")
            }

            Button {
                appState.beginSidebarReply(to: item, inThread: threadRoot)
            } label: {
                Label("Reply", systemImage: "arrowshape.turn.up.left")
            }

            Button(role: .destructive) {
                appState.delete(item)
            } label: {
                Label("Delete", systemImage: "trash")
            }
        }
    }

    private var rowFill: Color {
        if item.id == appState.selectedAnnotationID {
            return InterfacePalette.selectedRowFill(for: colorScheme)
        }
        if isRowHovered {
            return InterfacePalette.subtleFill(for: colorScheme)
        }
        return Color.clear
    }

    private var replySummary: some View {
        VStack(alignment: .leading, spacing: 4) {
            HStack(alignment: .firstTextBaseline) {
                Text(item.author)
                    .font(.caption.weight(.semibold))
                    .foregroundStyle(InterfacePalette.primaryText(for: colorScheme))
                    .lineLimit(1)
                Spacer()
                Text(dateString(item.modifiedAt ?? item.createdAt))
                    .font(.caption2)
                    .foregroundStyle(InterfacePalette.secondaryText(for: colorScheme))
                    .lineLimit(1)
            }

            Text(item.contents.isEmpty ? "No reply text" : item.contents)
                .font(.caption)
                .foregroundStyle(item.contents.isEmpty ? InterfacePalette.quietText(for: colorScheme) : InterfacePalette.primaryText(for: colorScheme))
                .fixedSize(horizontal: false, vertical: true)
        }
    }

    private func dateString(_ date: Date?) -> String {
        guard let date else { return "No date" }
        return date.formatted(date: .abbreviated, time: .shortened)
    }
}

private struct CommentReviewRowActions: View {
    let isVisible: Bool
    let onEdit: () -> Void
    let onReply: () -> Void
    let onDelete: () -> Void

    var body: some View {
        HStack(spacing: 5) {
            Button(action: onReply) {
                Label("Reply", systemImage: "arrowshape.turn.up.left")
                    .font(.caption2.weight(.medium))
                    .padding(.horizontal, 6)
                    .padding(.vertical, 2)
                    .contentShape(Capsule())
            }
            .buttonStyle(.plain)
            .help("Reply")
            .accessibilityLabel("Reply")

            Menu {
                Button(role: .none, action: onEdit) {
                    Label("Edit", systemImage: "pencil")
                }
                Button(role: .none, action: onReply) {
                    Label("Reply", systemImage: "arrowshape.turn.up.left")
                }
                Button(role: .destructive, action: onDelete) {
                    Label("Delete", systemImage: "trash")
                }
            } label: {
                Image(systemName: "ellipsis")
                    .font(.system(size: 11, weight: .semibold))
                    .frame(width: 18, height: 18)
                    .contentShape(Rectangle())
            }
            .menuStyle(.borderlessButton)
            .buttonStyle(.plain)
            .menuIndicator(.hidden)
            .opacity(isVisible ? 1 : 0)
            .animation(.easeInOut(duration: 0.12), value: isVisible)
            .help("More Actions")
            .accessibilityLabel("More Actions")
        }
    }
}
