import AppKit
import Combine
import SwiftUI

final class DesktopWidgetWindowManager: ObservableObject {
    static let shared = DesktopWidgetWindowManager()

    private let storageKey = "widgetInstancesData"
    private var windowsByInstanceID: [UUID: NSPanel] = [:]

    /// Bumps when persisted widget instances are added or removed so SwiftUI can refresh pickers.
    @Published private(set) var instancesChangeToken: Int = 0

    private init() {}

    private func notifyInstancesChanged() {
        instancesChangeToken += 1
    }

    func show(instance: WidgetInstance, appProvider: (UUID) -> LauncherApp?) {
        guard let app = appProvider(instance.appID) else { return }
        let resolvedVariant = WidgetTemplates.variant(for: app, variantID: instance.variantID)
        var resolvedInstance = instance
        if resolvedInstance.origin == nil {
            resolvedInstance.origin = nextAutoPlacement(for: resolvedVariant, appProvider: appProvider)
        }

        let panel = makePanel(for: resolvedInstance, app: app, variant: resolvedVariant)
        windowsByInstanceID[instance.id] = panel

        persistUpsert(instance: resolvedInstance)

        if let origin = resolvedInstance.origin {
            panel.setFrameOrigin(NSPoint(x: origin.x, y: origin.y))
            // Persist immediately so future auto-placement has accurate collision data.
            persistOrigin(instanceID: instance.id, origin: panel.frame.origin)
        } else {
            panel.center()
            // Persist immediately so future auto-placement has accurate collision data.
            persistOrigin(instanceID: instance.id, origin: panel.frame.origin)
        }

        // Ensure the newly added widget is visually on top.
        panel.orderFront(nil)
        panel.orderFrontRegardless()

        notifyInstancesChanged()
    }

    /// Returns `true` if there is already a persisted widget instance for the given `appID + variantID`.
    /// This is used by the widget picker UI to toggle `+`/`-` for each widget card.
    func hasInstance(appID: UUID, variantID: String) -> Bool {
        return loadInstances().contains { $0.appID == appID && $0.variantID == variantID }
    }

    /// Removes any widget instances matching the given `appID + variantID`.
    /// - Returns: number of instances removed.
    func removeInstances(appID: UUID, variantID: String) -> Int {
        let instances = loadInstances()
        let removed = instances.filter { $0.appID == appID && $0.variantID == variantID }
        let remaining = instances.filter { !($0.appID == appID && $0.variantID == variantID) }

        for instance in removed {
            if let win = windowsByInstanceID.removeValue(forKey: instance.id) {
                win.close()
            }
        }

        saveInstances(remaining)
        notifyInstancesChanged()
        return removed.count
    }

    func restore(appProvider: (UUID) -> LauncherApp?) {
        let instances = loadInstances()
        for instance in instances {
            guard windowsByInstanceID[instance.id] == nil else { continue }
            show(instance: instance, appProvider: appProvider)
        }
    }

    func removeAll(forAppID appID: UUID) {
        let instances = loadInstances()
        let remaining = instances.filter { $0.appID != appID }
        let removed = instances.filter { $0.appID == appID }

        for instance in removed {
            if let win = windowsByInstanceID.removeValue(forKey: instance.id) {
                win.close()
            }
        }

        saveInstances(remaining)
        notifyInstancesChanged()
    }

    // MARK: - Panel construction

    private func makePanel(for instance: WidgetInstance, app: LauncherApp, variant: WidgetVariant) -> NSPanel {
        let size = panelSize(for: variant)
        let panel: NSPanel
        if variant.layoutMode.needsKeyCapablePanel {
            // Key-capable so embedded web views and search fields receive clicks and typing reliably.
            panel = InteractiveMapsWidgetPanel(
                contentRect: NSRect(x: 0, y: 0, width: size.width, height: size.height),
                styleMask: [.nonactivatingPanel, .borderless],
                backing: .buffered,
                defer: false
            )
            // SwiftUI TextFields need the panel to become key reliably; `true` often blocks typing on desktop widgets.
            panel.becomesKeyOnlyIfNeeded = false
        } else {
            panel = DesktopWidgetPanel(
                contentRect: NSRect(x: 0, y: 0, width: size.width, height: size.height),
                styleMask: [.nonactivatingPanel, .borderless],
                backing: .buffered,
                defer: false
            )
            panel.becomesKeyOnlyIfNeeded = false
        }

        panel.isReleasedWhenClosed = false
        panel.isFloatingPanel = false
        panel.isMovable = true
        // One level above desktop icons so Finder desktop clicks do not leave our widget behind the desktop layer.
        let desktopIconLevel = Int(CGWindowLevelForKey(.desktopIconWindow))
        panel.level = NSWindow.Level(rawValue: desktopIconLevel + 1)
        // System window shadow is drawn as a rectangular halo; with transparent, rounded content it reads as ugly black lines.
        panel.hasShadow = false
        panel.isOpaque = false
        panel.backgroundColor = .clear
        panel.isMovableByWindowBackground = true
        panel.hidesOnDeactivate = false
        panel.ignoresMouseEvents = false
        panel.collectionBehavior = [
            .canJoinAllSpaces,
            .stationary,
            .ignoresCycle,
        ]

        let root = DesktopWidgetHostView(
            app: app,
            variant: variant,
            onRemove: { self.close(instanceID: instance.id) }
        )

        let hosting = NSHostingView(rootView: root)
        hosting.translatesAutoresizingMaskIntoConstraints = false

        let container = NSView()
        container.translatesAutoresizingMaskIntoConstraints = false
        container.wantsLayer = true
        container.layer?.backgroundColor = NSColor.clear.cgColor
        container.layer?.borderWidth = 0
        container.layer?.cornerRadius = 22
        container.layer?.masksToBounds = true
        container.addSubview(hosting)

        hosting.wantsLayer = true
        hosting.layer?.borderWidth = 0
        hosting.layer?.backgroundColor = NSColor.clear.cgColor

        NSLayoutConstraint.activate([
            hosting.leadingAnchor.constraint(equalTo: container.leadingAnchor),
            hosting.trailingAnchor.constraint(equalTo: container.trailingAnchor),
            hosting.topAnchor.constraint(equalTo: container.topAnchor),
            hosting.bottomAnchor.constraint(equalTo: container.bottomAnchor),
        ])

        panel.contentView = container

        NotificationCenter.default.addObserver(
            forName: NSWindow.didMoveNotification,
            object: panel,
            queue: .main
        ) { [weak self, weak panel] _ in
            guard let self, let panel else { return }
            self.persistOrigin(instanceID: instance.id, origin: panel.frame.origin)
        }

        return panel
    }

    private func panelSize(for variant: WidgetVariant) -> CGSize {
        if variant.layoutMode == .interactiveMap {
            switch variant.size {
            case .small:
                return CGSize(width: 340, height: 420)
            case .medium:
                return CGSize(width: 460, height: 400)
            case .large:
                return CGSize(width: 520, height: 560)
            }
        }
        if let override = variant.layoutMode.desktopPanelSizeOverride() {
            return override
        }
        switch variant.size {
        case .small:
            return CGSize(width: 220, height: 210)
        case .medium:
            return CGSize(width: 390, height: 210)
        case .large:
            return CGSize(width: 390, height: 400)
        }
    }

    /// Region where macOS system desktop widgets (Calendar, Weather, Photos, etc.) usually sit.
    /// Those windows are composited above normal third-party desktop windows, so our panels cannot
    /// appear “on top” of them—auto-placement must avoid this area so the user sees our widget beside them.
    private func reservedSystemDesktopWidgetZone(in visibleFrame: NSRect) -> NSRect {
        let margin: CGFloat = 18
        let w = min(560, visibleFrame.width * 0.48)
        let h = min(420, visibleFrame.height * 0.55)
        return NSRect(
            x: visibleFrame.minX + margin,
            y: visibleFrame.maxY - margin - h,
            width: w,
            height: h
        )
    }

    private func nextAutoPlacement(for variant: WidgetVariant, appProvider: (UUID) -> LauncherApp?) -> WidgetInstance.Origin {
        let panelSizeForNew = panelSize(for: variant)
        let frame = NSScreen.main?.visibleFrame ?? NSRect(x: 100, y: 100, width: 1200, height: 800)
        let systemWidgetZone = reservedSystemDesktopWidgetZone(in: frame)

        let margin: CGFloat = 18
        let gapX: CGFloat = 26
        let gapY: CGFloat = 20
        let cellW = panelSizeForNew.width + gapX
        let cellH = panelSizeForNew.height + gapY

        // Existing widgets:
        // 1) Live panels already on screen (most reliable; avoids missing persisted origins).
        // 2) Persisted widget origins (fallback for already-closed widgets).
        let liveRects: [NSRect] = windowsByInstanceID.values.map { $0.frame }
        let persistedRects: [NSRect] = loadInstances().compactMap { instance in
            guard let origin = instance.origin else { return nil }
            guard let app = appProvider(instance.appID) else { return nil }
            let v = WidgetTemplates.variant(for: app, variantID: instance.variantID)
            let s = panelSize(for: v)
            return NSRect(x: origin.x, y: origin.y, width: s.width, height: s.height)
        }
        let existingRects = liveRects + persistedRects

        let keepOut: CGFloat = 8
        func overlapsExisting(_ candidate: NSRect) -> Bool {
            let inflated = candidate.insetBy(dx: -keepOut, dy: -keepOut)
            return existingRects.contains(where: { $0.intersects(inflated) })
        }

        func overlapsSystemWidgetZone(_ candidate: NSRect) -> Bool {
            candidate.intersects(systemWidgetZone)
        }

        let cols = max(1, Int((frame.width - margin * 2) / max(cellW, 1)))
        let maxSlots = 200

        // Scan row-major for the first slot that avoids our widgets and Apple's desktop widget zone.
        for slot in 0..<maxSlots {
            let row = slot / cols
            let col = slot % cols

            let x = frame.minX + margin + CGFloat(col) * cellW
            let y = frame.maxY - margin - panelSizeForNew.height - CGFloat(row) * cellH

            let candidate = NSRect(x: x, y: y, width: panelSizeForNew.width, height: panelSizeForNew.height)

            // Keep inside visible bounds with margin.
            guard candidate.minX >= frame.minX + margin,
                  candidate.maxX <= frame.maxX - margin,
                  candidate.minY >= frame.minY + margin,
                  candidate.maxY <= frame.maxY - margin else {
                continue
            }

            if !overlapsExisting(candidate), !overlapsSystemWidgetZone(candidate) {
                return WidgetInstance.Origin(x: candidate.minX, y: candidate.minY)
            }
        }

        // Fallback: first column to the right of the system widget zone, then continue the grid.
        let safeMinX = max(systemWidgetZone.maxX + gapX, frame.minX + margin)
        for slot in 0..<maxSlots {
            let row = slot / cols
            let col = slot % cols

            let x = safeMinX + CGFloat(col) * cellW
            let y = frame.maxY - margin - panelSizeForNew.height - CGFloat(row) * cellH

            let candidate = NSRect(x: x, y: y, width: panelSizeForNew.width, height: panelSizeForNew.height)

            guard candidate.minX >= frame.minX + margin,
                  candidate.maxX <= frame.maxX - margin,
                  candidate.minY >= frame.minY + margin,
                  candidate.maxY <= frame.maxY - margin else {
                continue
            }

            if !overlapsExisting(candidate) {
                return WidgetInstance.Origin(x: candidate.minX, y: candidate.minY)
            }
        }

        // Last resort: center of screen (usually clear of the top-left widget stack).
        let cx = frame.midX - panelSizeForNew.width / 2
        let cy = frame.midY - panelSizeForNew.height / 2
        let x = min(max(cx, frame.minX + margin), frame.maxX - margin - panelSizeForNew.width)
        let y = min(max(cy, frame.minY + margin), frame.maxY - margin - panelSizeForNew.height)
        return WidgetInstance.Origin(x: x, y: y)
    }

    private func close(instanceID: UUID) {
        if let win = windowsByInstanceID.removeValue(forKey: instanceID) {
            win.close()
        }
        let instances = loadInstances().filter { $0.id != instanceID }
        saveInstances(instances)
        notifyInstancesChanged()
    }

    // MARK: - Persistence

    private func loadInstances() -> [WidgetInstance] {
        guard let data = UserDefaults.standard.data(forKey: storageKey) else { return [] }
        do {
            return try JSONDecoder().decode([WidgetInstance].self, from: data)
        } catch {
            return []
        }
    }

    private func saveInstances(_ instances: [WidgetInstance]) {
        do {
            let data = try JSONEncoder().encode(instances)
            UserDefaults.standard.set(data, forKey: storageKey)
        } catch {
            // Ignore and keep UI responsive.
        }
    }

    private func persistUpsert(instance: WidgetInstance) {
        var instances = loadInstances()
        if let idx = instances.firstIndex(where: { $0.id == instance.id }) {
            instances[idx] = instance
        } else {
            instances.append(instance)
        }
        saveInstances(instances)
    }

    private func persistOrigin(instanceID: UUID, origin: NSPoint) {
        var instances = loadInstances()
        guard let idx = instances.firstIndex(where: { $0.id == instanceID }) else { return }
        instances[idx].origin = WidgetInstance.Origin(x: origin.x, y: origin.y)
        saveInstances(instances)
    }
}

private final class DesktopWidgetPanel: NSPanel {
    override var canBecomeKey: Bool { false }
    override var canBecomeMain: Bool { false }

    override func mouseDown(with event: NSEvent) {
        orderFront(nil)
        super.mouseDown(with: event)
    }
}

/// Used for the interactive Google Maps widget so `TextField` and toolbar buttons can become first responder.
private final class InteractiveMapsWidgetPanel: NSPanel {
    override var canBecomeKey: Bool { true }
    override var canBecomeMain: Bool { false }

    override func mouseDown(with event: NSEvent) {
        NSApp.activate(ignoringOtherApps: true)
        makeKeyAndOrderFront(nil)
        super.mouseDown(with: event)
    }
}

private struct DesktopWidgetHostView: View {
    let app: LauncherApp
    let variant: WidgetVariant
    let onRemove: () -> Void

    var body: some View {
        DesktopWidgetView(app: app, variant: variant, isPreview: false)
            .contextMenu {
                Button("Remove Widget", role: .destructive) {
                    onRemove()
                }
            }
    }
}

