//
//  ConversationListView.swift
//  Confab
//
//  Saved conversations list
//
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
// Copyright (C) 2026 Rune Olsen
//
// This file is part of Confab.
//
// Confab is licensed under the PolyForm Noncommercial License 1.0.0.
// You may use, study, modify, and share it for any noncommercial
// purpose. Commercial use — including selling Confab or any part of
// it, standalone or bundled into another product or service —
// requires a separate commercial license from the copyright holder.
//
// See the LICENSE file or
// <https://polyformproject.org/licenses/noncommercial/1.0.0> for
// the full license text. For commercial licensing, contact Rune
// Olsen via <https://confab.no>.


import os
import SwiftUI

struct ConversationListView: View {
    @Environment(\.dismiss) var dismiss
    @State private var searchText = ""
    @State private var conversations: [Conversation] = []
    @State private var folders: [Folder] = []
    @State private var collapsedFolders: Set<UUID> = []
    @State private var selectedConversations: Set<UUID> = []
    @State private var isSelecting = false
    @State private var lastClickedId: UUID? = nil
    @State private var useSemanticSearch = false
    @State private var semanticResults: [Conversation] = []
    @State private var isSearching = false
    @State private var selectedIndex: Int = 0
    @State private var showCombineSheet = false
    @FocusState private var searchFocused: Bool
    private let settings = SettingsService.shared
    var onLoad: ((Conversation) -> Void)?
    var onRename: ((UUID, String) -> Void)?

    private var filteredConversations: [Conversation] {
        if searchText.isEmpty {
            return conversations
        }

        if useSemanticSearch && settings.embeddingsEnabled {
            return semanticResults
        } else {
            return conversations.filter {
                $0.name.lowercased().contains(searchText.lowercased())
            }
        }
    }

    private var conversationsByFolder: [UUID?: [Conversation]] {
        Dictionary(grouping: filteredConversations, by: { $0.folderId })
    }

    private var orderedFolderTree: [(folder: Folder, depth: Int)] { Folder.orderedTree(from: folders) }
    private var visibleFolderIds: Set<UUID> { Folder.visibleFolderIds(tree: orderedFolderTree, collapsed: collapsedFolders) }

    /// Flattened conversation order matching what's actually rendered in the List — folders in
    /// depth-first tree order (skipping collapsed ones' contents, since they're not
    /// visible/selectable), then Unfiled last. Used as the anchor sequence for Shift-click range
    /// selection.
    private var visibleOrderedConversations: [Conversation] {
        guard !folders.isEmpty else { return filteredConversations }
        var result: [Conversation] = []
        for (folder, _) in orderedFolderTree where visibleFolderIds.contains(folder.id) && !collapsedFolders.contains(folder.id) {
            result.append(contentsOf: conversationsByFolder[folder.id] ?? [])
        }
        result.append(contentsOf: conversationsByFolder[nil] ?? [])
        return result
    }

    var body: some View {
        VStack(spacing: 0) {
            // Header
            HStack {
                Text("Conversations")
                    .font(.system(size: 18, weight: .bold))
                Spacer()

                if isSelecting {
                    Button("Cancel") {
                        isSelecting = false
                        selectedConversations.removeAll()
                        lastClickedId = nil
                    }
                    .buttonStyle(.plain)

                    if selectedConversations.count >= 2 {
                        Button {
                            showCombineSheet = true
                        } label: {
                            HStack(spacing: 4) {
                                Image(systemName: "arrow.triangle.merge")
                                Text("Combine (\(selectedConversations.count))")
                            }
                        }
                        .buttonStyle(.plain)
                    }

                    if !selectedConversations.isEmpty {
                        Menu {
                            if !folders.isEmpty {
                                ForEach(orderedFolderTree, id: \.folder.id) { entry in
                                    Button(String(repeating: "    ", count: entry.depth) + entry.folder.name) {
                                        moveSelectedToFolder(entry.folder.id)
                                    }
                                }
                                Divider()
                            }
                            Button("Remove from Folder") {
                                moveSelectedToFolder(nil)
                            }
                        } label: {
                            HStack(spacing: 4) {
                                Image(systemName: "folder")
                                Text("Move to Folder (\(selectedConversations.count))")
                            }
                        }
                        .buttonStyle(.plain)
                    }

                    if !selectedConversations.isEmpty {
                        Button(role: .destructive) {
                            deleteSelected()
                        } label: {
                            HStack(spacing: 4) {
                                Image(systemName: "trash")
                                Text("Delete (\(selectedConversations.count))")
                            }
                        }
                        .buttonStyle(.plain)
                        .foregroundStyle(.red)
                    }
                } else {
                    Button {
                        createFolderPrompt()
                    } label: {
                        Label("New Folder", systemImage: "folder.badge.plus")
                    }
                    .buttonStyle(.plain)

                    if !conversations.isEmpty {
                        Button("Select") {
                            isSelecting = true
                        }
                        .buttonStyle(.plain)
                    }

                    Button { dismiss() } label: {
                        Image(systemName: "xmark.circle.fill")
                            .font(.title2)
                            .foregroundStyle(.secondary)
                    }
                    .buttonStyle(.plain)
                    .keyboardShortcut(.escape, modifiers: [])
                }
            }
            .padding(.horizontal, 24)
            .padding(.top, 20)
            .padding(.bottom, 12)

            // Search bar
            HStack(spacing: 8) {
                Image(systemName: "magnifyingglass")
                    .foregroundStyle(.secondary)
                TextField("Search conversations...", text: $searchText)
                    .textFieldStyle(.plain)
                    .focused($searchFocused)
                    .onChange(of: searchText) {
                        selectedIndex = 0
                        if useSemanticSearch && settings.embeddingsEnabled && !searchText.isEmpty {
                            performSemanticSearch()
                        }
                    }
                    #if os(macOS)
                    .onKeyPress(.upArrow) {
                        if selectedIndex > 0 {
                            selectedIndex -= 1
                        }
                        return .handled
                    }
                    .onKeyPress(.downArrow) {
                        if selectedIndex < filteredConversations.count - 1 {
                            selectedIndex += 1
                        }
                        return .handled
                    }
                    .onKeyPress(.return, phases: .down) { _ in
                        guard !isSelecting, !filteredConversations.isEmpty else { return .ignored }
                        let conv = filteredConversations[min(selectedIndex, filteredConversations.count - 1)]
                        onLoad?(conv)
                        dismiss()
                        return .handled
                    }
                    #endif
                if !searchText.isEmpty {
                    Button { searchText = "" } label: {
                        Image(systemName: "xmark.circle.fill")
                            .foregroundStyle(.tertiary)
                    }
                    .buttonStyle(.plain)
                }

                if settings.embeddingsEnabled {
                    Divider()
                        .frame(height: 16)
                    Toggle("Semantic", isOn: $useSemanticSearch)
                        .toggleStyle(.switch)
                        .controlSize(.small)
                        .onChange(of: useSemanticSearch) {
                            if useSemanticSearch && !searchText.isEmpty {
                                performSemanticSearch()
                            }
                        }
                        .help("Use AI-powered semantic search instead of keyword matching")
                }

                if isSearching {
                    ProgressView()
                        .controlSize(.small)
                }
            }
            .padding(10)
            .background(.quaternary.opacity(0.5), in: RoundedRectangle(cornerRadius: 8))
            .padding(.horizontal, 24)
            .padding(.bottom, 12)

            Divider()

            // Content
            if filteredConversations.isEmpty {
                Spacer()
                VStack(spacing: 8) {
                    Image(systemName: searchText.isEmpty ? "tray" : "magnifyingglass")
                        .font(.largeTitle)
                        .foregroundStyle(.tertiary)
                    Text(searchText.isEmpty ? "No Saved Conversations" : "No Matches")
                        .font(.headline)
                        .foregroundStyle(.secondary)
                    Text(searchText.isEmpty ? "Conversations you save will appear here" : "Try a different search term")
                        .font(.caption)
                        .foregroundStyle(.tertiary)
                }
                Spacer()
            } else {
                ScrollViewReader { proxy in
                    List {
                        if folders.isEmpty {
                            ForEach(filteredConversations) { conversation in
                                conversationRow(conversation)
                            }
                        } else {
                            ForEach(orderedFolderTree, id: \.folder.id) { entry in
                                if visibleFolderIds.contains(entry.folder.id) {
                                    let folderConversations = conversationsByFolder[entry.folder.id] ?? []
                                    if !folderConversations.isEmpty || searchText.isEmpty {
                                        Section {
                                            if !collapsedFolders.contains(entry.folder.id) {
                                                ForEach(folderConversations) { conversation in
                                                    conversationRow(conversation, depth: entry.depth + 1)
                                                }
                                            }
                                        } header: {
                                            folderHeader(entry.folder, depth: entry.depth)
                                        }
                                    }
                                }
                            }

                            let unfiled = conversationsByFolder[nil] ?? []
                            if !unfiled.isEmpty {
                                Section {
                                    ForEach(unfiled) { conversation in
                                        conversationRow(conversation)
                                    }
                                } header: {
                                    Text("Unfiled")
                                        .dropDestination(for: String.self) { items, _ in
                                            _ = handleDrop(items, toFolder: nil)
                                        }
                                }
                            }
                        }
                    }
                    .listStyle(.plain)
                    .onChange(of: selectedIndex) {
                        guard !filteredConversations.isEmpty else { return }
                        let clamped = min(selectedIndex, filteredConversations.count - 1)
                        withAnimation(.easeInOut(duration: 0.1)) {
                            proxy.scrollTo(filteredConversations[clamped].id, anchor: .center)
                        }
                    }
                }
            }

            Divider()

            // Bottom bar
            HStack {
                Text("↑↓ navigate  ↩ open")
                    .font(.system(size: 11))
                    .foregroundStyle(.tertiary)
                Spacer()
                Button("Done") { dismiss() }
                    .buttonStyle(.borderedProminent)
                    .controlSize(.regular)
            }
            .padding(.horizontal, 24)
            .padding(.vertical, 12)
        }
        .onAppear {
            loadConversations()
            searchFocused = true
            collapsedFolders = SettingsService.shared.collapsedFolderIds
        }
        .frame(minWidth: 700, idealWidth: 800, minHeight: 500, idealHeight: 600)
        .sheet(isPresented: $showCombineSheet) {
            CombineConversationsSheet(
                conversations: conversations.filter { selectedConversations.contains($0.id) },
                onCompleted: { _ in
                    loadConversations()
                    selectedConversations.removeAll()
                    isSelecting = false
                    lastClickedId = nil
                }
            )
        }
    }

    @ViewBuilder
    private func conversationRow(_ conversation: Conversation, depth: Int = 0) -> some View {
        let index = filteredConversations.firstIndex(where: { $0.id == conversation.id }) ?? 0
        HStack(spacing: 12) {
            if isSelecting {
                Button {
                    toggleSelection(conversation.id)
                    lastClickedId = conversation.id
                } label: {
                    Image(systemName: selectedConversations.contains(conversation.id) ? "checkmark.circle.fill" : "circle")
                        .foregroundStyle(selectedConversations.contains(conversation.id) ? .blue : .secondary)
                        .font(.title2)
                }
                .buttonStyle(.plain)
            }

            ConversationRow(conversation: conversation)
                .contentShape(Rectangle())
                .onTapGesture {
                    handleRowTap(conversation, index: index)
                }

            Spacer()

            if !isSelecting {
                Button {
                    renameConversation(conversation)
                } label: {
                    Image(systemName: "pencil")
                        .foregroundStyle(.secondary)
                        .font(.system(size: 15))
                }
                .buttonStyle(.plain)
                .help("Rename conversation")

                Button {
                    deleteConversation(conversation)
                } label: {
                    Image(systemName: "trash")
                        .foregroundStyle(.red)
                        .font(.system(size: 16))
                }
                .buttonStyle(.plain)
                .help("Delete conversation")
            }
        }
        .padding(.leading, CGFloat(depth) * 14)
        .listRowBackground(
            !isSelecting && index == selectedIndex
                ? Color.confabAccent.opacity(0.15)
                : Color.clear
        )
        .id(conversation.id)
        .draggable(DraggedItem.conversations(
            isSelecting && selectedConversations.contains(conversation.id) && selectedConversations.count > 1
                ? Array(selectedConversations) : [conversation.id]
        ).rawValue) {
            if isSelecting && selectedConversations.contains(conversation.id) && selectedConversations.count > 1 {
                Text("\(selectedConversations.count) conversations")
                    .font(.system(size: 13, weight: .medium))
                    .foregroundStyle(.white)
                    .padding(.horizontal, 10)
                    .padding(.vertical, 6)
                    .background(Color.confabAccent, in: RoundedRectangle(cornerRadius: 6))
            } else {
                Text(conversation.name)
                    .font(.system(size: 13, weight: .medium))
                    .foregroundStyle(.white)
                    .padding(.horizontal, 10)
                    .padding(.vertical, 6)
                    .background(Color.confabAccent, in: RoundedRectangle(cornerRadius: 6))
            }
        }
        .swipeActions(edge: .trailing, allowsFullSwipe: false) {
            Button(role: .destructive) {
                deleteConversation(conversation)
            } label: {
                Label("Delete", systemImage: "trash")
            }

            Button {
                renameConversation(conversation)
            } label: {
                Label("Rename", systemImage: "pencil")
            }
            .tint(.orange)

            Button {
                exportConversation(conversation)
            } label: {
                Label("Export", systemImage: "square.and.arrow.up")
            }
            .tint(.blue)
        }
        .contextMenu {
            Menu {
                if conversation.folderId != nil {
                    Button {
                        moveConversationOrSelection(conversation, toFolder: nil)
                    } label: {
                        Label("Remove from Folder", systemImage: "folder.badge.minus")
                    }
                    Divider()
                }
                ForEach(orderedFolderTree, id: \.folder.id) { entry in
                    if entry.folder.id != conversation.folderId {
                        Button {
                            moveConversationOrSelection(conversation, toFolder: entry.folder.id)
                        } label: {
                            Text(String(repeating: "    ", count: entry.depth) + entry.folder.name)
                        }
                    }
                }
            } label: {
                Label(isSelecting && selectedConversations.contains(conversation.id) && selectedConversations.count > 1
                      ? "Move \(selectedConversations.count) to Folder" : "Move to Folder", systemImage: "folder")
            }
            Menu {
                Button {
                    exportConversation(conversation, format: "md")
                } label: {
                    Label("Markdown", systemImage: "doc.text")
                }
                Button {
                    exportConversation(conversation, format: "html")
                } label: {
                    Label("HTML", systemImage: "chevron.left.forwardslash.chevron.right")
                }
                Button {
                    exportConversation(conversation, format: "pdf")
                } label: {
                    Label("PDF", systemImage: "doc.richtext")
                }
            } label: {
                Label("Export", systemImage: "square.and.arrow.up")
            }
            Button {
                renameConversation(conversation)
            } label: {
                Label("Rename", systemImage: "pencil")
            }
            Button(role: .destructive) {
                deleteConversation(conversation)
            } label: {
                Label("Delete", systemImage: "trash")
            }
        }
    }

    @ViewBuilder
    private func folderHeader(_ folder: Folder, depth: Int) -> some View {
        HStack(spacing: 4) {
            Image(systemName: "chevron.right")
                .font(.system(size: 9, weight: .bold))
                .rotationEffect(.degrees(collapsedFolders.contains(folder.id) ? 0 : 90))
            Text(folder.name)
                .font(.system(size: 12, weight: .bold))
        }
        .padding(.leading, CGFloat(depth) * 14)
        .contentShape(Rectangle())
        .onTapGesture {
            withAnimation(.easeInOut(duration: 0.15)) {
                toggleCollapsed(folder.id)
            }
        }
        .contextMenu {
            Button {
                createFolderPrompt(parentId: folder.id)
            } label: {
                Label("New Subfolder…", systemImage: "folder.badge.plus")
            }
            Button {
                renameFolderPrompt(folder)
            } label: {
                Label("Rename Folder", systemImage: "pencil")
            }
            Button(role: .destructive) {
                deleteFolder(folder)
            } label: {
                Label("Delete Folder", systemImage: "trash")
            }
        }
        .draggable(DraggedItem.folder(folder.id).rawValue) {
            Text(folder.name)
                .font(.system(size: 13, weight: .medium))
                .foregroundStyle(.white)
                .padding(.horizontal, 10)
                .padding(.vertical, 6)
                .background(Color.confabAccent, in: RoundedRectangle(cornerRadius: 6))
        }
        .dropDestination(for: String.self) { items, _ in
            _ = handleDrop(items, toFolder: folder.id)
        }
    }

    private func toggleCollapsed(_ folderId: UUID) {
        if collapsedFolders.contains(folderId) {
            collapsedFolders.remove(folderId)
        } else {
            collapsedFolders.insert(folderId)
        }
        SettingsService.shared.collapsedFolderIds = collapsedFolders
    }

    private func handleDrop(_ items: [String], toFolder targetFolderId: UUID?) -> Bool {
        var moved = false
        for raw in items {
            guard let item = DraggedItem(rawValue: raw) else { continue }
            switch item {
            case .conversations(let ids):
                for id in ids {
                    guard let conversation = conversations.first(where: { $0.id == id }) else { continue }
                    moveConversation(conversation, toFolder: targetFolderId)
                    moved = true
                }
            case .folder(let sourceId):
                guard sourceId != targetFolderId else { continue }
                if let targetFolderId, Folder.isDescendant(targetFolderId, of: sourceId, in: folders) { continue }
                do {
                    try DatabaseService.shared.moveFolder(id: sourceId, toParent: targetFolderId)
                    if let i = folders.firstIndex(where: { $0.id == sourceId }) { folders[i].parentId = targetFolderId }
                    moved = true
                } catch {
                    Log.db.error("Failed to move folder: \(error.localizedDescription)")
                }
            }
        }
        return moved
    }

    private func createFolderPrompt(parentId: UUID? = nil) {
        #if os(macOS)
        let alert = NSAlert()
        alert.messageText = parentId == nil ? "New Folder" : "New Subfolder"
        alert.addButton(withTitle: "Create")
        alert.addButton(withTitle: "Cancel")
        let input = NSTextField(frame: NSRect(x: 0, y: 0, width: 260, height: 24))
        alert.accessoryView = input
        alert.window.initialFirstResponder = input
        guard alert.runModal() == .alertFirstButtonReturn else { return }
        let name = input.stringValue.trimmingCharacters(in: .whitespaces)
        guard !name.isEmpty else { return }
        do {
            let folder = try DatabaseService.shared.createFolder(name: name, parentId: parentId)
            folders.append(folder)
            sortFolders()
        } catch {
            Log.db.error("Failed to create folder: \(error.localizedDescription)")
        }
        #endif
    }

    private func sortFolders() {
        folders.sort { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
    }

    private func loadConversations() {
        do {
            conversations = try DatabaseService.shared.listConversations()
            folders = try DatabaseService.shared.listFolders()
        } catch {
            Log.db.error("Failed to load conversations: \(error.localizedDescription)")
            conversations = []
        }
    }

    private func moveConversation(_ conversation: Conversation, toFolder folderId: UUID?) {
        do {
            try DatabaseService.shared.moveConversation(id: conversation.id, toFolder: folderId)
            if let i = conversations.firstIndex(where: { $0.id == conversation.id }) {
                conversations[i].folderId = folderId
            }
        } catch {
            Log.db.error("Failed to move conversation: \(error.localizedDescription)")
        }
    }

    /// Moves every currently-selected conversation to a folder (or removes them all from their
    /// folders if `folderId` is nil).
    private func moveSelectedToFolder(_ folderId: UUID?) {
        for id in selectedConversations {
            guard let conversation = conversations.first(where: { $0.id == id }) else { continue }
            moveConversation(conversation, toFolder: folderId)
        }
    }

    /// Right-clicking a conversation that's part of a multi-item selection moves the whole
    /// selection; right-clicking a single (non-selected, or lone-selected) row moves just that one.
    private func moveConversationOrSelection(_ conversation: Conversation, toFolder folderId: UUID?) {
        if isSelecting && selectedConversations.contains(conversation.id) && selectedConversations.count > 1 {
            moveSelectedToFolder(folderId)
        } else {
            moveConversation(conversation, toFolder: folderId)
        }
    }

    private func renameFolderPrompt(_ folder: Folder) {
        #if os(macOS)
        let alert = NSAlert()
        alert.messageText = "Rename Folder"
        alert.addButton(withTitle: "Rename")
        alert.addButton(withTitle: "Cancel")
        let input = NSTextField(frame: NSRect(x: 0, y: 0, width: 260, height: 24))
        input.stringValue = folder.name
        input.selectText(nil)
        alert.accessoryView = input
        alert.window.initialFirstResponder = input
        guard alert.runModal() == .alertFirstButtonReturn else { return }
        let newName = input.stringValue.trimmingCharacters(in: .whitespaces)
        guard !newName.isEmpty, newName != folder.name else { return }
        do {
            try DatabaseService.shared.renameFolder(id: folder.id, name: newName)
            if let i = folders.firstIndex(where: { $0.id == folder.id }) {
                folders[i].name = newName
            }
            sortFolders()
        } catch {
            Log.db.error("Failed to rename folder: \(error.localizedDescription)")
        }
        #endif
    }

    private func deleteFolder(_ folder: Folder) {
        do {
            try DatabaseService.shared.deleteFolder(id: folder.id)
            // Matches the DB's reparent-up-one-level semantics: children and conversations
            // filed directly in this folder move to its own parent (nil if it was top-level),
            // not blanket-unfiled.
            let parentId = folder.parentId
            folders.removeAll { $0.id == folder.id }
            for i in folders.indices where folders[i].parentId == folder.id {
                folders[i].parentId = parentId
            }
            for i in conversations.indices where conversations[i].folderId == folder.id {
                conversations[i].folderId = parentId
            }
        } catch {
            Log.db.error("Failed to delete folder: \(error.localizedDescription)")
        }
    }

    private func toggleSelection(_ id: UUID) {
        if selectedConversations.contains(id) {
            selectedConversations.remove(id)
        } else {
            selectedConversations.insert(id)
        }
    }

    /// Standard macOS row-click handling: ⌘-click toggles the individual row (entering selection
    /// mode if needed), Shift-click extends/creates a contiguous range from the last-clicked row,
    /// and a plain click either toggles (while already selecting) or opens the conversation.
    private func handleRowTap(_ conversation: Conversation, index: Int) {
        #if os(macOS)
        let modifiers = NSEvent.modifierFlags
        if modifiers.contains(.command) {
            isSelecting = true
            toggleSelection(conversation.id)
            lastClickedId = conversation.id
            return
        }
        if modifiers.contains(.shift) {
            isSelecting = true
            selectRange(to: conversation.id)
            lastClickedId = conversation.id
            return
        }
        #endif
        if isSelecting {
            toggleSelection(conversation.id)
            lastClickedId = conversation.id
        } else {
            selectedIndex = index
            onLoad?(conversation)
            dismiss()
        }
    }

    /// Pure range-selection logic, pulled out so it's testable without a live View: given the
    /// on-screen id order, an anchor, and a target, returns the ids that should end up selected.
    /// Falls back to just `targetId` if the anchor is nil or no longer present in `orderedIds`
    /// (e.g. the very first Shift-click, or the anchor row was deleted/filtered out since).
    nonisolated static func idsInRange(orderedIds: [UUID], anchorId: UUID?, targetId: UUID) -> Set<UUID> {
        guard let anchorId,
              let anchorIndex = orderedIds.firstIndex(of: anchorId),
              let targetIndex = orderedIds.firstIndex(of: targetId)
        else {
            return [targetId]
        }
        let range = anchorIndex <= targetIndex ? anchorIndex...targetIndex : targetIndex...anchorIndex
        return Set(orderedIds[range])
    }

    /// Selects every conversation between `lastClickedId` and `targetId` in on-screen order.
    private func selectRange(to targetId: UUID) {
        let orderedIds = visibleOrderedConversations.map { $0.id }
        selectedConversations.formUnion(Self.idsInRange(orderedIds: orderedIds, anchorId: lastClickedId, targetId: targetId))
    }

    private func deleteSelected() {
        for id in selectedConversations {
            do {
                let _ = try DatabaseService.shared.deleteConversation(id: id)
            } catch {
                Log.db.error("Failed to delete conversation: \(error.localizedDescription)")
            }
        }
        withAnimation {
            conversations.removeAll { selectedConversations.contains($0.id) }
            selectedConversations.removeAll()
            isSelecting = false
        }
        lastClickedId = nil
        selectedIndex = 0
        GitSyncService.shared.syncAfterDeletion()
    }

    private func renameConversation(_ conversation: Conversation) {
        #if os(macOS)
        let alert = NSAlert()
        alert.messageText = "Rename Conversation"
        alert.addButton(withTitle: "Rename")
        alert.addButton(withTitle: "Cancel")
        let input = NSTextField(frame: NSRect(x: 0, y: 0, width: 260, height: 24))
        input.stringValue = conversation.name
        input.selectText(nil)
        alert.accessoryView = input
        alert.window.initialFirstResponder = input
        guard alert.runModal() == .alertFirstButtonReturn else { return }
        let newName = input.stringValue.trimmingCharacters(in: .whitespaces)
        guard !newName.isEmpty, newName != conversation.name else { return }
        do {
            _ = try DatabaseService.shared.updateConversation(id: conversation.id, name: newName, messages: nil)
            withAnimation {
                if let i = conversations.firstIndex(where: { $0.id == conversation.id }) {
                    conversations[i].name = newName
                    conversations[i].updatedAt = Date()
                }
            }
            onRename?(conversation.id, newName)
        } catch {
            Log.db.error("Failed to rename conversation: \(error.localizedDescription)")
        }
        #endif
    }

    private func deleteConversation(_ conversation: Conversation) {
        do {
            let _ = try DatabaseService.shared.deleteConversation(id: conversation.id)
            withAnimation {
                conversations.removeAll { $0.id == conversation.id }
            }
            selectedIndex = min(selectedIndex, max(0, filteredConversations.count - 1))
            GitSyncService.shared.syncAfterDeletion()
        } catch {
            Log.db.error("Failed to delete conversation: \(error.localizedDescription)")
        }
    }

    private func performSemanticSearch() {
        guard !searchText.isEmpty else {
            semanticResults = []
            return
        }

        isSearching = true

        Task {
            do {
                guard let provider = EmbeddingService.shared.getSelectedProvider() else {
                    Log.api.warning("No embedding providers available - skipping semantic search")
                    await MainActor.run {
                        isSearching = false
                    }
                    return
                }

                let embedding = try await EmbeddingService.shared.generateEmbedding(
                    text: searchText,
                    provider: provider
                )

                let results = try DatabaseService.shared.searchConversationsBySemantic(
                    queryEmbedding: embedding,
                    limit: 20
                )

                await MainActor.run {
                    semanticResults = results.map { $0.0 }
                    selectedIndex = 0
                    isSearching = false
                    Log.ui.info("Semantic search found \(results.count) results using \(provider.displayName)")
                }
            } catch {
                await MainActor.run {
                    semanticResults = []
                    isSearching = false
                    Log.ui.error("Semantic search failed: \(error)")
                }
            }
        }
    }

    private func exportConversation(_ conversation: Conversation, format: String = "md") {
        guard let (_, loadedMessages) = try? DatabaseService.shared.loadConversation(id: conversation.id),
              !loadedMessages.isEmpty else {
            return
        }
        let baseName = conversation.name.replacingOccurrences(of: " ", with: "_")

        if format == "pdf" {
            Task { @MainActor in
                guard let data = try? await ConversationExportService.pdfData(name: conversation.name, messages: loadedMessages) else {
                    return
                }
                _ = ConversationExportService.writeToDownloads(data, filename: baseName + ".pdf")
            }
            return
        }

        let content: String
        let filename: String
        switch format {
        case "html":
            content = ConversationExportService.html(name: conversation.name, messages: loadedMessages)
            filename = baseName + ".html"
        default:
            content = ConversationExportService.markdown(messages: loadedMessages)
            filename = baseName + ".md"
        }
        _ = ConversationExportService.writeToDownloads(content, filename: filename)
    }
}

struct ConversationRow: View {
    let conversation: Conversation

    private var formattedDate: String {
        let formatter = DateFormatter()
        formatter.dateFormat = "dd.MM.yyyy HH:mm"
        return formatter.string(from: conversation.updatedAt)
    }

    /// Strips the provider prefix from OpenRouter-style IDs (e.g. "anthropic/claude-3" → "claude-3")
    private var modelDisplayName: String? {
        guard let model = conversation.primaryModel, !model.isEmpty else { return nil }
        if let slash = model.lastIndex(of: "/") {
            return String(model[model.index(after: slash)...])
        }
        return model
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(conversation.name)
                .font(.system(size: 15, weight: .semibold))
                .foregroundStyle(.primary)
                .lineLimit(1)

            HStack(spacing: 6) {
                Label("\(conversation.messageCount)", systemImage: "message")
                    .font(.system(size: 12))

                Text("•")
                    .font(.system(size: 12))

                Text(formattedDate)
                    .font(.system(size: 12))

                if let model = modelDisplayName {
                    Text("•")
                        .font(.system(size: 12))
                    Text(model)
                        .font(.system(size: 12))
                        .lineLimit(1)
                        .truncationMode(.middle)
                }
            }
            .foregroundColor(.secondary)
        }
        .padding(.vertical, 5)
    }
}

#Preview {
    ConversationListView()
}
