import AppKit
import Combine
import SwiftUI

struct ConnectedScreenSummary: Identifiable, Equatable {
    let id: ScreenID
    let displayName: String
    let isActive: Bool
    let assignedWorkspaceID: WorkspaceID
}

@MainActor
final class ScreenRegistry: ObservableObject {
    static let shared = ScreenRegistry(assignmentStore: UserDefaultsScreenAssignmentStore())

    @Published private(set) var screenContexts: [ScreenContext] = []

    private let workspaceRegistry: WorkspaceRegistry
    private let settingsController: AppSettingsController
    private let assignmentStore: any ScreenAssignmentStoreType
    private let connectedScreenIDsProvider: @MainActor () -> [ScreenID]
    private let activeScreenIDProvider: @MainActor () -> ScreenID?
    private let screenLookup: @MainActor (ScreenID) -> NSScreen?

    private var contextsByID: [ScreenID: ScreenContext] = [:]
    private var preferredAssignments: [ScreenID: WorkspaceID]
    private var workspacePresenters: [WorkspaceID: ScreenID] = [:]
    private var cancellables = Set<AnyCancellable>()

    init(
        workspaceRegistry: WorkspaceRegistry? = nil,
        settingsController: AppSettingsController? = nil,
        assignmentStore: (any ScreenAssignmentStoreType)? = nil,
        initialAssignments: [ScreenID: WorkspaceID]? = nil,
        connectedScreenIDsProvider: @escaping @MainActor () -> [ScreenID] = {
            NSScreen.screens.map(\.displayUUID)
        },
        activeScreenIDProvider: @escaping @MainActor () -> ScreenID? = {
            let mouseLocation = NSEvent.mouseLocation
            return NSScreen.screens.first { NSMouseInRect(mouseLocation, $0.frame, false) }?.displayUUID
                ?? NSScreen.main?.displayUUID
        },
        screenLookup: @escaping @MainActor (ScreenID) -> NSScreen? = { screenID in
            NSScreen.screens.first { $0.displayUUID == screenID }
        }
    ) {
        let resolvedWorkspaceRegistry = workspaceRegistry ?? WorkspaceRegistry.shared
        let resolvedSettingsController = settingsController ?? AppSettingsController.shared
        let resolvedAssignmentStore = assignmentStore ?? UserDefaultsScreenAssignmentStore()

        self.workspaceRegistry = resolvedWorkspaceRegistry
        self.settingsController = resolvedSettingsController
        self.assignmentStore = resolvedAssignmentStore
        self.preferredAssignments = initialAssignments ?? resolvedAssignmentStore.loadScreenAssignments()
        self.connectedScreenIDsProvider = connectedScreenIDsProvider
        self.activeScreenIDProvider = activeScreenIDProvider
        self.screenLookup = screenLookup

        observeWorkspaceChanges()
        refreshConnectedScreens()
    }

    func allScreens() -> [ScreenContext] {
        screenContexts
    }

    func screenContext(for id: ScreenID) -> ScreenContext? {
        contextsByID[id]
    }

    func workspaceController(for screenID: ScreenID) -> WorkspaceController {
        let workspaceID = contextsByID[screenID]?.workspaceID ?? workspaceRegistry.defaultWorkspaceID
        return workspaceRegistry.controller(for: workspaceID) ?? workspaceRegistry.defaultWorkspaceController
    }

    func assignedScreenIDs(to workspaceID: WorkspaceID) -> [ScreenID] {
        preferredAssignments
            .filter { $0.value == workspaceID }
            .map(\.key)
            .sorted()
    }

    func assignedScreenCount(to workspaceID: WorkspaceID) -> Int {
        assignedScreenIDs(to: workspaceID).count
    }

    func connectedScreenSummaries() -> [ConnectedScreenSummary] {
        let activeScreenID = activeScreenID()

        return screenContexts.enumerated().map { index, context in
            ConnectedScreenSummary(
                id: context.id,
                displayName: resolvedDisplayName(for: context.id, fallbackIndex: index),
                isActive: context.id == activeScreenID,
                assignedWorkspaceID: context.workspaceID
            )
        }
    }

    func assignWorkspace(_ workspaceID: WorkspaceID, to screenID: ScreenID) {
        guard workspaceRegistry.controller(for: workspaceID) != nil else { return }

        let previousWorkspaceID = contextsByID[screenID]?.workspaceID ?? preferredAssignments[screenID]
        preferredAssignments[screenID] = workspaceID
        contextsByID[screenID]?.updateWorkspace(id: workspaceID)

        if let previousWorkspaceID,
           previousWorkspaceID != workspaceID,
           workspacePresenters[previousWorkspaceID] == screenID {
            workspacePresenters.removeValue(forKey: previousWorkspaceID)
        }

        persistAssignments()
    }

    @discardableResult
    func assignActiveScreen(to workspaceID: WorkspaceID) -> ScreenID? {
        guard let screenID = activeScreenID() else { return nil }
        assignWorkspace(workspaceID, to: screenID)
        return screenID
    }

    func presentingScreenID(for workspaceID: WorkspaceID) -> ScreenID? {
        guard let screenID = workspacePresenters[workspaceID] else { return nil }
        guard preferredAssignments[screenID] == workspaceID else {
            workspacePresenters.removeValue(forKey: workspaceID)
            return nil
        }
        return screenID
    }

    @discardableResult
    func claimWorkspacePresentation(for screenID: ScreenID) -> ScreenID? {
        guard let workspaceID = contextsByID[screenID]?.workspaceID ?? preferredAssignments[screenID] else {
            return nil
        }

        let previousPresenter = workspacePresenters[workspaceID]
        workspacePresenters[workspaceID] = screenID
        return previousPresenter == screenID ? nil : previousPresenter
    }

    func releaseWorkspacePresentation(for screenID: ScreenID) {
        workspacePresenters = workspacePresenters.filter { $0.value != screenID }
    }

    @discardableResult
    func deleteWorkspace(
        _ workspaceID: WorkspaceID,
        preferredFallback preferredFallbackID: WorkspaceID? = nil
    ) -> WorkspaceID? {
        guard workspaceRegistry.canDeleteWorkspace(id: workspaceID) else {
            return nil
        }

        guard let fallbackWorkspaceID = workspaceRegistry.deletionFallbackWorkspaceID(
            forDeleting: workspaceID,
            preferredFallback: preferredFallbackID
        ) else {
            return nil
        }

        workspacePresenters.removeValue(forKey: workspaceID)

        for (screenID, assignedWorkspaceID) in preferredAssignments where assignedWorkspaceID == workspaceID {
            preferredAssignments[screenID] = fallbackWorkspaceID
        }

        for context in contextsByID.values where context.workspaceID == workspaceID {
            context.updateWorkspace(id: fallbackWorkspaceID)
        }

        guard workspaceRegistry.deleteWorkspace(id: workspaceID) else {
            return nil
        }

        persistAssignments()
        return fallbackWorkspaceID
    }

    func activeScreenID() -> ScreenID? {
        activeScreenIDProvider() ?? screenContexts.first?.id
    }

    func refreshConnectedScreens() {
        let connectedScreenIDs = connectedScreenIDsProvider()
        let validWorkspaceIDs = Set(workspaceRegistry.allWorkspaceSummaries().map(\.id))
        let defaultWorkspaceID = workspaceRegistry.defaultWorkspaceID
        var nextContextsByID: [ScreenID: ScreenContext] = [:]
        var nextContexts: [ScreenContext] = []

        for screenID in connectedScreenIDs {
            let workspaceID = resolvedWorkspaceID(
                for: screenID,
                validWorkspaceIDs: validWorkspaceIDs,
                defaultWorkspaceID: defaultWorkspaceID
            )

            let context = contextsByID[screenID] ?? ScreenContext(
                id: screenID,
                workspaceID: workspaceID,
                settingsController: settingsController,
                screenProvider: screenLookup
            )

            context.updateWorkspace(id: workspaceID)
            context.refreshClosedSize()

            nextContextsByID[screenID] = context
            nextContexts.append(context)
        }

        contextsByID = nextContextsByID
        screenContexts = nextContexts
        reconcileWorkspacePresenters()
        persistAssignments()
    }

    private func resolvedWorkspaceID(
        for screenID: ScreenID,
        validWorkspaceIDs: Set<WorkspaceID>,
        defaultWorkspaceID: WorkspaceID
    ) -> WorkspaceID {
        guard let preferredWorkspaceID = preferredAssignments[screenID],
              validWorkspaceIDs.contains(preferredWorkspaceID) else {
            preferredAssignments[screenID] = defaultWorkspaceID
            return defaultWorkspaceID
        }

        return preferredWorkspaceID
    }

    private func observeWorkspaceChanges() {
        workspaceRegistry.$workspaceSummaries
            .dropFirst()
            .sink { [weak self] _ in
                Task { @MainActor [weak self] in
                    self?.refreshConnectedScreens()
                }
            }
            .store(in: &cancellables)
    }

    private func persistAssignments() {
        assignmentStore.saveScreenAssignments(preferredAssignments)
    }

    private func reconcileWorkspacePresenters() {
        let validScreenIDs = Set(contextsByID.keys)
        let validAssignments = preferredAssignments

        workspacePresenters = workspacePresenters.filter { workspaceID, screenID in
            validScreenIDs.contains(screenID) && validAssignments[screenID] == workspaceID
        }
    }

    private func resolvedDisplayName(for screenID: ScreenID, fallbackIndex: Int) -> String {
        let fallbackName = "Screen \(fallbackIndex + 1)"
        guard let screen = screenLookup(screenID) else {
            return fallbackName
        }

        let localizedName = screen.localizedName.trimmingCharacters(in: .whitespacesAndNewlines)
        return localizedName.isEmpty ? fallbackName : localizedName
    }
}

extension ScreenRegistry: ScreenRegistryType {}
