//
//  MainView.swift
//  iKeyMon
//
//  Created by tracer on 30.03.25.
//

import SwiftUI
import Combine
import UserNotifications
import SwiftData
import AppKit
import UniformTypeIdentifiers

struct MainView: View {
    
    private static let serverOrderKeyStatic = "serverOrder"
    private static let storedServersKeyStatic = "storedServers"
    private static let storedGroupsKeyStatic = "storedGroups"

    @State var showAddServerSheet: Bool = false
    @State private var showAddGroupSheet: Bool = false
    @State private var serverBeingEdited: Server?
    @State private var groupBeingEdited: ServerGroup?
    @State private var serverToDelete: Server?
    @State private var groupToDelete: ServerGroup?
    @State private var showDeleteConfirmation = false
    @State private var showDeleteGroupConfirmation = false
    @State private var isFetchingInfo: Bool = false
    @AppStorage("pingInterval") private var pingInterval: Int = 10
    @AppStorage("refreshInterval") private var refreshInterval: Int = 60
    @AppStorage("enableStatusNotifications") private var enableStatusNotifications: Bool = true
    @AppStorage("enableAlertNotifications") private var enableAlertNotifications: Bool = true
    @AppStorage("alertGracePeriod") private var alertGracePeriod: Int = 30
    @State private var refreshTimer: Timer.TimerPublisher?
    @State private var refreshSubscription: AnyCancellable?
    @State private var pingTimer: Timer?
    @State private var restartingServerID: UUID?
    @State private var draggedGroupID: UUID?
    @State private var groupDropIndicator: GroupDropIndicator?
    @State private var lastRefreshInterval: Int?
    @State private var lastMetricPrune: Date?
    @State private var previousServiceStates: [String: String] = [:]
    @State private var serviceFailureStartedAt: [String: Date] = [:]
    @State private var serviceOfflineAlerted: [String: Bool] = [:]
    private let serverOrderKey = MainView.serverOrderKeyStatic
    private let storedGroupsKey = MainView.storedGroupsKeyStatic
    @Environment(\.modelContext) private var modelContext

    @State private var servers: [Server] = MainView.loadStoredServers()
    @State private var groups: [ServerGroup] = MainView.loadStoredGroups()

//    @State private var selectedServer: Server?
    @State private var selectedServerID: UUID?

    var body: some View {
        var mainContent: some View {
            NavigationSplitView {
                ZStack {
                    SidebarMaterialView()

                    List(selection: $selectedServerID) {
                        sidebarContent
                    }
                    .listStyle(.sidebar)
                    .scrollContentBackground(.hidden)
                    .background(Color.clear)
                }
                .background(
                    RoundedRectangle(cornerRadius: 0, style: .continuous)
                        .fill(.ultraThinMaterial)
                )
                .overlay(alignment: .trailing) {
                    Rectangle()
                        .fill(Color.white.opacity(0.08))
                        .frame(width: 1)
                }
                .toolbar {
                    ToolbarItem(placement: .primaryAction) {
                        Menu {
                            Button("Add Host") {
                                showAddServerSheet = true
                            }
                            Button("Add Group") {
                                showAddGroupSheet = true
                            }
                        } label: {
                            Image(systemName: "plus")
                        }
                        .help("Add Host or Group")
                    }
                }
                .navigationTitle("Servers")
                .onChange(of: selectedServerID) {
                    if let selectedServerID {
                        UserDefaults.standard.set(selectedServerID.uuidString, forKey: "selectedServerID")
                        fetchServerInfo(for: selectedServerID)
                    }
                }
            } detail: {
                if let selectedServerID,
                   let index = servers.firstIndex(where: { selectedServerID == $0.id }) {
                    let serverID = servers[index].id
                    ServerDetailView(
                        server: $servers[index],
                        isFetching: isFetchingInfo,
                        canRestart: servers[index].info?.supportsRestartCommand == true,
                        isRestarting: restartingServerID == serverID
                    ) {
                        await restartServer(for: serverID)
                    }
                } else {
                    ContentUnavailableView("No Server Selected", systemImage: "server.rack")
                }
            }
        }
        return mainContent
        .sheet(isPresented: $showAddServerSheet) {
            ServerFormView(
                mode: .add,
                servers: $servers,
                groups: $groups,
                dismiss: { showAddServerSheet = false }
            )
        }
        .sheet(isPresented: $showAddGroupSheet) {
            GroupFormView(mode: .add, groups: $groups) {
                saveGroups()
            }
        }
        .sheet(item: $serverBeingEdited) { server in
            ServerFormView(
                mode: .edit(server),
                servers: $servers,
                groups: $groups,
                dismiss: { serverBeingEdited = nil }
            )
        }
        .sheet(item: $groupBeingEdited) { group in
            GroupFormView(mode: .edit(group), groups: $groups) {
                saveGroups()
            }
        }
        .alert("Are you sure you want to delete this server?", isPresented: $showDeleteConfirmation, presenting: serverToDelete) { server in
            Button("Delete", role: .destructive) {
                ServerFormView.delete(server: server, from: &servers)
                saveServers()
            }
            Button("Cancel", role: .cancel) {}
        }
        .alert("Are you sure you want to delete this group?", isPresented: $showDeleteGroupConfirmation, presenting: groupToDelete) { group in
            Button("Delete", role: .destructive) {
                deleteGroup(group)
            }
            Button("Cancel", role: .cancel) {}
        } message: { group in
            Text("Servers in \(group.name) will remain available and become ungrouped.")
        }
        .onAppear {
            requestNotificationPermissions()

            let initialID: UUID?
            if let storedID = UserDefaults.standard.string(forKey: "selectedServerID"),
               let uuid = UUID(uuidString: storedID),
               servers.contains(where: { $0.id == uuid }) {
                print("✅ [MainView] Restored selected server \(uuid)")
                initialID = uuid
            } else if let first = servers.first {
                print("✅ [MainView] Selecting first server \(first.hostname)")
                initialID = first.id
            } else {
                print("ℹ️ [MainView] No stored selection")
                initialID = nil
            }

            selectedServerID = initialID

            if let initialID {
                Task {
                    await prefetchOtherServers(activeID: initialID)
                }
            }
            setupTimers()
        }
        .onChange(of: pingInterval) { _, _ in
            setupPingTimer()
        }
        .onChange(of: refreshInterval) { oldValue, newValue in
            if oldValue != newValue {
                setupRefreshTimer()
            }
        }
        .onChange(of: groups) { _, _ in
            saveGroups()
        }
        .frame(minWidth: 800, minHeight: 450)
    }

    @ViewBuilder
    private var sidebarContent: some View {
        if groups.isEmpty {
            ForEach(servers) { server in
                sidebarRow(for: server)
            }
            .onMove(perform: moveServer)
        } else {
            ForEach(groups) { group in
                Section {
                    ForEach(servers(in: group)) { server in
                        sidebarRow(for: server)
                    }
                    .onMove { source, destination in
                        moveServers(in: group.id, from: source, to: destination)
                    }
                } header: {
                    groupHeader(for: group)
                }
            }

            if !ungroupedServers.isEmpty {
                Section {
                    ForEach(ungroupedServers) { server in
                        sidebarRow(for: server)
                    }
                    .onMove { source, destination in
                        moveServers(in: nil, from: source, to: destination)
                    }
                } header: {
                    sidebarSectionHeader("Ungrouped")
                }
            }
        }
    }

    private func sidebarSectionHeader(_ title: String) -> some View {
        HStack {
            Text(title)
                .font(.system(size: NSFont.systemFontSize + 1, weight: .bold))
                .foregroundStyle(Color.accentColor)
            Spacer(minLength: 0)
        }
        .padding(.vertical, 4)
    }

    private func sidebarRow(for server: Server) -> some View {
        HStack {
            Image(systemName: "dot.circle.fill")
                .foregroundColor(sidebarStatusColor(for: server))
            Text(server.hostname)
        }
        .help(sidebarStatusTooltip(for: server))
        .tag(server.id)
        .contextMenu {
            Button("Edit") {
                print("Editing:", server.hostname)
                serverBeingEdited = server
            }
            Divider()
            Button("Delete", role: .destructive) {
                serverToDelete = server
                showDeleteConfirmation = true
            }
        }
    }

    private func sidebarStatusColor(for server: Server) -> Color {
        if !server.pingable {
            return .red
        }
        if server.info?.rebootRequired == true {
            return .yellow
        }
        return .green
    }

    private func sidebarStatusTooltip(for server: Server) -> String {
        if !server.pingable {
            return "This host is offline."
        }
        return server.info?.statusTooltip ?? "This host is online."
    }

    private func groupHeader(for group: ServerGroup) -> some View {
        let activePlacement = groupDropIndicator?.groupID == group.id ? groupDropIndicator?.placement : nil

        return VStack(spacing: 0) {
            if activePlacement == .before {
                dropIndicator
            }

            sidebarSectionHeader(group.name)
            .contentShape(Rectangle())
            .background {
                if activePlacement != nil {
                    RoundedRectangle(cornerRadius: 6, style: .continuous)
                        .fill(Color.accentColor.opacity(0.12))
                }
            }

            if activePlacement == .after {
                dropIndicator
            }
        }
        .onDrag {
            draggedGroupID = group.id
            return NSItemProvider(object: group.id.uuidString as NSString)
        }
        .onDrop(
            of: [UTType.text],
            delegate: GroupDropDelegate(
                targetGroup: group,
                groups: $groups,
                draggedGroupID: $draggedGroupID,
                indicator: $groupDropIndicator
            )
        )
        .contextMenu {
            Button("Edit Group") {
                groupBeingEdited = group
            }
            Button("Delete Group", role: .destructive) {
                groupToDelete = group
                showDeleteGroupConfirmation = true
            }
        }
    }

    private var dropIndicator: some View {
        VStack(spacing: 4) {
            Capsule()
                .fill(Color.accentColor)
                .frame(height: 3)
                .shadow(color: Color.accentColor.opacity(0.25), radius: 1, y: 0)
            Color.clear
                .frame(height: 4)
        }
        .padding(.vertical, 2)
    }

    private var ungroupedServers: [Server] {
        servers.filter { server in
            guard let groupID = server.groupID else { return true }
            return groups.contains(where: { $0.id == groupID }) == false
        }
    }

    private func servers(in group: ServerGroup) -> [Server] {
        servers.filter { $0.groupID == group.id }
    }
    
    private func fetchServerInfo(for id: UUID) {
        guard let server = servers.first(where: { $0.id == id }) else {
            print("❌ [MainView] fetchServerInfo: server not found for id \(id)")
            return
        }
        guard let apiKey = KeychainHelper.loadApiKey(for: server.hostname)?.trimmingCharacters(in: .whitespacesAndNewlines),
              !apiKey.isEmpty else {
            print("❌ [MainView] fetchServerInfo: missing API key for \(server.hostname)")
            return
        }
        guard let baseURL = URL(string: "https://\(server.hostname)") else {
            print("❌ [MainView] Invalid base URL for \(server.hostname)")
            return
        }

        isFetchingInfo = true

        Task {
            defer { isFetchingInfo = false }
            do {
                let api = try await APIFactory.detectAndCreateAPI(baseURL: baseURL, apiKey: apiKey)
                let info = try await api.fetchServerSummary(apiKey: apiKey)
                await MainActor.run {
                    if let index = servers.firstIndex(where: { $0.id == id }) {
                        var updated = servers[index]
                        updated.info = info
                        servers[index] = updated
                        recordMetricSample(for: id, info: info)
                        checkServiceStatusChanges(for: server.hostname, newInfo: info)
                    }
                }
            } catch {
                print("❌ Failed to fetch server data: \(error)")
            }
        }
    }

    private func prefetchOtherServers(activeID: UUID) async {
        let others = servers.filter { $0.id != activeID }
        await withTaskGroup(of: Void.self) { group in
            for server in others {
                group.addTask {
                    await fetchServerInfoAsync(for: server.id)
                }
            }
        }
    }

    private func fetchServerInfoAsync(for id: UUID) async {
        guard let server = servers.first(where: { $0.id == id }) else { return }
        guard let apiKey = KeychainHelper.loadApiKey(for: server.hostname)?.trimmingCharacters(in: .whitespacesAndNewlines),
              !apiKey.isEmpty,
              let baseURL = URL(string: "https://\(server.hostname)")
        else { return }

        do {
            let api = try await APIFactory.detectAndCreateAPI(baseURL: baseURL, apiKey: apiKey)
            let info = try await api.fetchServerSummary(apiKey: apiKey)
            await MainActor.run {
                if let index = servers.firstIndex(where: { $0.id == id }) {
                    var updated = servers[index]
                    updated.info = info
                    servers[index] = updated
                    recordMetricSample(for: id, info: info)
                }
            }
        } catch {
            print("❌ Prefetch failed for \(server.hostname): \(error)")
        }
    }
    
    private func moveServer(from source: IndexSet, to destination: Int) {
        servers.move(fromOffsets: source, toOffset: destination)
        saveServers()
        saveServerOrder()
    }

    private func moveServers(in groupID: UUID?, from source: IndexSet, to destination: Int) {
        let matchingServers = servers.filter { server in
            if let groupID {
                return server.groupID == groupID
            }
            return server.groupID == nil || groups.contains(where: { $0.id == server.groupID }) == false
        }

        var reorderedServers = matchingServers
        reorderedServers.move(fromOffsets: source, toOffset: destination)

        let replacements = Dictionary(uniqueKeysWithValues: reorderedServers.map { ($0.id, $0) })
        var reorderedIDs = reorderedServers.map(\.id)

        servers = servers.map { server in
            let belongsInSection: Bool
            if let groupID {
                belongsInSection = server.groupID == groupID
            } else {
                belongsInSection = server.groupID == nil || groups.contains(where: { $0.id == server.groupID }) == false
            }

            guard belongsInSection, let nextID = reorderedIDs.first else {
                return server
            }

            reorderedIDs.removeFirst()
            return replacements[nextID] ?? server
        }

        saveServers()
        saveServerOrder()
    }
    
    private func saveServerOrder() {
        let ids = servers.map { $0.id.uuidString }
        UserDefaults.standard.set(ids, forKey: serverOrderKey)
        print("💾 [MainView] Saved server order with \(ids.count) entries")
    }

    private func saveServers() {
        if let data = try? JSONEncoder().encode(servers) {
            UserDefaults.standard.set(data, forKey: MainView.storedServersKeyStatic)
        }
    }

    private func saveGroups() {
        if let data = try? JSONEncoder().encode(groups) {
            UserDefaults.standard.set(data, forKey: storedGroupsKey)
        }
    }

    private func recordMetricSample(for serverID: UUID, info: ServerInfo) {
        let sample = MetricSample(
            serverID: serverID,
            cpuPercent: info.load.percent,
            memoryPercent: info.memory.percent,
            swapPercent: info.swap.percent,
            diskPercent: info.diskSpace.percent
        )
        modelContext.insert(sample)

        do {
            try modelContext.save()
        } catch {
            print("❌ [MainView] Failed to save metric sample: \(error)")
        }

        pruneOldMetricSamplesIfNeeded()
    }

    private func pruneOldMetricSamplesIfNeeded() {
        let now = Date()

        if let lastMetricPrune, now.timeIntervalSince(lastMetricPrune) < 3600 {
            return
        }

        let cutoff = now.addingTimeInterval(-30 * 24 * 60 * 60)
        let descriptor = FetchDescriptor<MetricSample>(
            predicate: #Predicate { sample in
                sample.timestamp < cutoff
            }
        )

        do {
            let expiredSamples = try modelContext.fetch(descriptor)
            for sample in expiredSamples {
                modelContext.delete(sample)
            }
            if !expiredSamples.isEmpty {
                try modelContext.save()
            }
            lastMetricPrune = now
        } catch {
            print("❌ [MainView] Failed to prune metric samples: \(error)")
        }
    }

    private func deleteGroup(_ group: ServerGroup) {
        groups.removeAll { $0.id == group.id }
        for index in servers.indices {
            if servers[index].groupID == group.id {
                servers[index].groupID = nil
            }
        }
        saveGroups()
        saveServers()
    }
    
    private struct PingResponse: Codable {
        let response: String
    }
        
    func pingAllServers() {
        let pingTargets = servers.map { ($0.id, $0.hostname) }

        for (serverID, hostname) in pingTargets {
            Task {
                let apiKey = KeychainHelper.loadApiKey(for: hostname)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
                let pingable = await PingService.ping(hostname: hostname, apiKey: apiKey, notificationsEnabled: enableStatusNotifications)
                await MainActor.run {
                    guard let index = servers.firstIndex(where: { $0.id == serverID }) else {
                        return
                    }
                    servers[index].pingable = pingable
                }
            }
        }
    }

    private func setupTimers() {
        setupPingTimer()
        setupRefreshTimer()
    }

    private func setupPingTimer() {
        pingTimer?.invalidate()
        pingAllServers()
        pingTimer = Timer.scheduledTimer(withTimeInterval: Double(pingInterval), repeats: true) { _ in
            pingAllServers()
        }
    }

    private func setupRefreshTimer() {
        refreshSubscription?.cancel()
        refreshSubscription = nil
        refreshTimer = Timer.publish(every: Double(refreshInterval), on: .main, in: .common)
        refreshSubscription = refreshTimer?.autoconnect().sink { _ in
            for server in servers {
                fetchServerInfo(for: server.id)
            }
        }
    }

    private static func loadStoredServers() -> [Server] {
        let defaults = UserDefaults.standard
        guard let data = defaults.data(forKey: storedServersKeyStatic) else {
            print("ℹ️ [MainView] No storedServers data found")
            return []
        }
        do {
            let saved = try JSONDecoder().decode([Server].self, from: data)
            print("📦 [MainView] Loaded \(saved.count) servers from UserDefaults")
            if let order = defaults.stringArray(forKey: serverOrderKeyStatic) {
                let idMap = order.compactMap(UUID.init)
                let sorted = saved.sorted { a, b in
                    guard
                        let i1 = idMap.firstIndex(of: a.id),
                        let i2 = idMap.firstIndex(of: b.id)
                    else { return false }
                    return i1 < i2
                }
                return sorted
            }
            return saved
        } catch {
            print("❌ [MainView] Failed to decode stored servers: \(error)")
            return []
        }
    }

    private static func loadStoredGroups() -> [ServerGroup] {
        let defaults = UserDefaults.standard
        guard let data = defaults.data(forKey: storedGroupsKeyStatic) else {
            return []
        }

        do {
            return try JSONDecoder().decode([ServerGroup].self, from: data)
        } catch {
            print("❌ [MainView] Failed to decode stored groups: \(error)")
            return []
        }
    }

    private func requestNotificationPermissions() {
        Task {
            do {
                try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound])
            } catch {
                print("❌ [MainView] Failed to request notification permissions: \(error)")
            }
        }
    }

    private func checkServiceStatusChanges(for hostname: String, newInfo: ServerInfo) {
        guard let ports = newInfo.ports else { return }

        for port in ports {
            let key = "\(hostname)-\(port.id)"
            let previousStatus = previousServiceStates[key]
            let currentStatus = port.status.lowercased()
            let now = Date()

            if let previousStatus, previousStatus != currentStatus {
                if currentStatus == "offline" {
                    serviceFailureStartedAt[key] = now
                    serviceOfflineAlerted[key] = false
                } else if currentStatus == "online" {
                    let hadOfflineAlert = serviceOfflineAlerted[key] == true
                    serviceFailureStartedAt.removeValue(forKey: key)
                    serviceOfflineAlerted.removeValue(forKey: key)
                    if hadOfflineAlert && enableStatusNotifications {
                        sendServiceNotification(service: port.service, hostname: hostname, status: "online")
                    }
                }
            } else if previousStatus == nil {
                if currentStatus == "offline" {
                    serviceFailureStartedAt[key] = now
                    serviceOfflineAlerted[key] = false
                } else if currentStatus == "online" {
                    serviceFailureStartedAt.removeValue(forKey: key)
                }
            }

            if currentStatus == "offline" {
                let startedAt = serviceFailureStartedAt[key] ?? now
                if serviceFailureStartedAt[key] == nil {
                    serviceFailureStartedAt[key] = startedAt
                }

                if serviceOfflineAlerted[key] != true,
                   enableAlertNotifications,
                   now.timeIntervalSince(startedAt) >= TimeInterval(alertGracePeriod) {
                    sendServiceNotification(service: port.service, hostname: hostname, status: "offline")
                    serviceOfflineAlerted[key] = true
                }
            }

            previousServiceStates[key] = currentStatus
        }
    }

    private func sendServiceNotification(service: String, hostname: String, status: String) {
        let content = UNMutableNotificationContent()
        content.title = "\(service) \(status.uppercased())"
        content.body = "\(service) on \(hostname) is \(status)"
        content.sound = .default

        let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
        UNUserNotificationCenter.current().add(request)
    }

    private func restartServer(for id: UUID) async -> ServerActionFeedback {
        guard let server = servers.first(where: { $0.id == id }) else {
            return ServerActionFeedback(
                title: "Reboot Failed",
                message: "The selected server could not be found."
            )
        }

        guard server.info?.supportsRestartCommand == true else {
            return ServerActionFeedback(
                title: "Reboot Unavailable",
                message: "\(server.hostname) does not support remote reboot via the API."
            )
        }

        guard let apiKey = KeychainHelper.loadApiKey(for: server.hostname)?.trimmingCharacters(in: .whitespacesAndNewlines),
              !apiKey.isEmpty else {
            return ServerActionFeedback(
                title: "Reboot Failed",
                message: "No API key is configured for \(server.hostname)."
            )
        }

        guard let baseURL = URL(string: "https://\(server.hostname)") else {
            return ServerActionFeedback(
                title: "Reboot Failed",
                message: "The server URL for \(server.hostname) is invalid."
            )
        }

        restartingServerID = id
        defer { restartingServerID = nil }

        do {
            let api: AnyServerAPI
            if let versionString = server.info?.apiVersion,
               let versionedAPI = APIFactory.createAPI(baseURL: baseURL, versionString: versionString) {
                api = versionedAPI
            } else {
                api = try await APIFactory.detectAndCreateAPI(baseURL: baseURL, apiKey: apiKey)
            }
            try await api.restartServer(apiKey: apiKey)
            await PingService.suppressChecks(for: server.hostname, duration: 90)

            return ServerActionFeedback(
                title: "Reboot Requested",
                message: "The reboot command was sent to \(server.hostname). The host may become unavailable briefly while it restarts."
            )
        } catch let error as URLError where Self.isExpectedRestartDisconnect(error) {
            await PingService.suppressChecks(for: server.hostname, duration: 90)
            return ServerActionFeedback(
                title: "Reboot Requested",
                message: "The reboot command appears to have been accepted by \(server.hostname). The connection dropped while the host was going away, which is expected during a reboot."
            )
        } catch APIError.httpError(404, let message) {
            return ServerActionFeedback(
                title: "Reboot Unavailable",
                message: message ?? "\(server.hostname) returned 404 for /api/v2/server/reboot."
            )
        } catch {
            return ServerActionFeedback(
                title: "Reboot Failed",
                message: error.localizedDescription
            )
        }
    }

    private static func isExpectedRestartDisconnect(_ error: URLError) -> Bool {
        switch error.code {
        case .timedOut,
             .cannotConnectToHost,
             .networkConnectionLost,
             .notConnectedToInternet,
             .cannotFindHost,
             .dnsLookupFailed:
            return true
        default:
            return false
        }
    }
}

private struct SidebarMaterialView: NSViewRepresentable {
    func makeNSView(context: Context) -> NSVisualEffectView {
        let view = NSVisualEffectView()
        view.blendingMode = .behindWindow
        view.material = .sidebar
        view.state = .active
        return view
    }

    func updateNSView(_ nsView: NSVisualEffectView, context: Context) {
        nsView.state = .active
    }
}

private struct GroupDropDelegate: DropDelegate {
    enum Placement {
        case before
        case after
    }

    let targetGroup: ServerGroup
    @Binding var groups: [ServerGroup]
    @Binding var draggedGroupID: UUID?
    @Binding var indicator: GroupDropIndicator?

    func dropEntered(info: DropInfo) {
        updateIndicator(with: info)
    }

    func dropUpdated(info: DropInfo) -> DropProposal? {
        updateIndicator(with: info)
        return DropProposal(operation: .move)
    }

    func dropExited(info: DropInfo) {
        indicator = nil
    }

    func performDrop(info: DropInfo) -> Bool {
        defer {
            draggedGroupID = nil
            indicator = nil
        }

        guard
            let draggedGroupID,
            draggedGroupID != targetGroup.id,
            let fromIndex = groups.firstIndex(where: { $0.id == draggedGroupID }),
            let toIndex = groups.firstIndex(where: { $0.id == targetGroup.id })
        else {
            return false
        }

        let placement = placement(for: info)
        let proposedIndex = placement == .after ? toIndex + 1 : toIndex
        groups.move(
            fromOffsets: IndexSet(integer: fromIndex),
            toOffset: proposedIndex > fromIndex ? proposedIndex + 1 : proposedIndex
        )
        return true
    }

    private func updateIndicator(with info: DropInfo) {
        guard let draggedGroupID, draggedGroupID != targetGroup.id else {
            indicator = nil
            return
        }

        indicator = GroupDropIndicator(
            groupID: targetGroup.id,
            placement: placement(for: info)
        )
    }

    private func placement(for info: DropInfo) -> Placement {
        info.location.y > 12 ? .after : .before
    }
}

private struct GroupDropIndicator: Equatable {
    let groupID: UUID
    let placement: GroupDropDelegate.Placement
}

#Preview {
    MainView()
        .environmentObject(SparkleUpdater())
}
