import AppKit
import SwiftUI

typealias ScreenID = String

/// Observable screen-local UI state for one physical display.
@MainActor
final class ScreenContext: ObservableObject, Identifiable {
    private static let minimumOpenWidth: CGFloat = 320
    private static let minimumOpenHeight: CGFloat = 140
    private static let windowHorizontalPadding: CGFloat = 40
    private static let windowVerticalPadding: CGFloat = 20

    let id: ScreenID

    @Published var workspaceID: WorkspaceID
    @Published var notchState: NotchState = .closed
    @Published var notchSize: CGSize
    @Published var closedNotchSize: CGSize
    @Published var isHovering = false
    @Published var isCloseTransitionActive = false
    @Published var suppressHoverOpenUntilHoverExit = false
    @Published var isUserResizing = false
    @Published var isPresetResizing = false
    @Published private(set) var suppressCloseOnFocusLoss = false

    var requestOpen: (() -> Void)?
    var requestClose: (() -> Void)?
    var requestWindowResize: (() -> Void)?
    var requestTerminalFocus: (() -> Void)?

    private let settingsController: AppSettingsController
    private let screenProvider: @MainActor (ScreenID) -> NSScreen?

    init(
        id: ScreenID,
        workspaceID: WorkspaceID,
        settingsController: AppSettingsController? = nil,
        screenProvider: @escaping @MainActor (ScreenID) -> NSScreen? = { screenID in
            NSScreen.screens.first { $0.displayUUID == screenID }
        }
    ) {
        self.id = id
        self.workspaceID = workspaceID
        self.settingsController = settingsController ?? AppSettingsController.shared
        self.screenProvider = screenProvider

        let closed = Self.resolveClosedNotchSize(
            for: id,
            using: self.settingsController.settings.display,
            screenProvider: screenProvider
        )
        self.closedNotchSize = closed
        self.notchSize = closed
    }

    var openAnimation: Animation {
        let animation = settingsController.settings.animation
        return .spring(
            response: animation.openSpringResponse,
            dampingFraction: animation.openSpringDamping
        )
    }

    var closeAnimation: Animation {
        let animation = settingsController.settings.animation
        return .spring(
            response: animation.closeSpringResponse,
            dampingFraction: animation.closeSpringDamping
        )
    }

    var openAnimationDuration: TimeInterval {
        max(0.05, settingsController.settings.animation.resizeAnimationDuration)
    }

    func open() {
        notchSize = openNotchSize
        notchState = .open
    }

    func close() {
        refreshClosedSize()
        notchSize = closedNotchSize
        notchState = .closed
    }

    func updateWorkspace(id: WorkspaceID) {
        guard workspaceID != id else { return }
        workspaceID = id
    }

    func refreshClosedSize() {
        closedNotchSize = Self.resolveClosedNotchSize(
            for: id,
            using: settingsController.settings.display,
            screenProvider: screenProvider
        )
    }

    var openNotchSize: CGSize {
        let display = settingsController.settings.display
        return clampedOpenSize(
            CGSize(width: display.openWidth, height: display.openHeight)
        )
    }

    func beginInteractiveResize() {
        isUserResizing = true
    }

    func resizeOpenNotch(to proposedSize: CGSize) {
        let clampedSize = clampedOpenSize(proposedSize)
        if notchState == .open {
            notchSize = clampedSize
        }
        requestWindowResize?()
    }

    func endInteractiveResize() {
        if notchState == .open {
            settingsController.update {
                $0.display.openWidth = notchSize.width
                $0.display.openHeight = notchSize.height
            }
        }
        isUserResizing = false
    }

    func applySizePreset(_ preset: TerminalSizePreset, notifyWindowResize: Bool = true) {
        setOpenSize(preset.size, notifyWindowResize: notifyWindowResize)
    }

    @discardableResult
    func setStoredOpenSize(_ proposedSize: CGSize) -> CGSize {
        let clampedSize = clampedOpenSize(proposedSize)
        settingsController.update {
            $0.display.openWidth = clampedSize.width
            $0.display.openHeight = clampedSize.height
        }
        return clampedSize
    }

    @discardableResult
    func setOpenSize(_ proposedSize: CGSize, notifyWindowResize: Bool) -> CGSize {
        let clampedSize = setStoredOpenSize(proposedSize)
        if notchState == .open {
            notchSize = clampedSize
        }
        if notifyWindowResize {
            requestWindowResize?()
        }
        return clampedSize
    }

    private func clampedOpenSize(_ size: CGSize) -> CGSize {
        CGSize(
            width: size.width.clamped(to: Self.minimumOpenWidth...maximumAllowedWidth),
            height: size.height.clamped(to: Self.minimumOpenHeight...maximumAllowedHeight)
        )
    }

    private var maximumAllowedWidth: CGFloat {
        guard let screen = resolvedScreen() ?? NSScreen.main else {
            return Self.minimumOpenWidth
        }
        return max(Self.minimumOpenWidth, screen.frame.width - Self.windowHorizontalPadding)
    }

    private var maximumAllowedHeight: CGFloat {
        guard let screen = resolvedScreen() ?? NSScreen.main else {
            return Self.minimumOpenHeight
        }
        return max(Self.minimumOpenHeight, screen.frame.height - Self.windowVerticalPadding)
    }

    var closeInteractionLockDuration: TimeInterval {
        max(settingsController.settings.animation.closeSpringResponse + 0.2, 0.35)
    }

    func beginCloseTransition() {
        isCloseTransitionActive = true
        if isHovering {
            suppressHoverOpenUntilHoverExit = true
        }
    }

    func cancelCloseTransition() {
        isCloseTransitionActive = false
    }

    func endCloseTransition() {
        isCloseTransitionActive = false
    }

    func clearHoverOpenSuppression() {
        suppressHoverOpenUntilHoverExit = false
    }

    func setCloseOnFocusLossSuppressed(_ suppressed: Bool) {
        suppressCloseOnFocusLoss = suppressed
    }

    private func resolvedScreen() -> NSScreen? {
        screenProvider(id)
    }

    private static func resolveClosedNotchSize(
        for screenID: ScreenID,
        using settings: AppSettings.DisplaySettings,
        screenProvider: @escaping @MainActor (ScreenID) -> NSScreen?
    ) -> CGSize {
        let screen = screenProvider(screenID) ?? NSScreen.main
        return screen?.closedNotchSize(using: settings) ?? CGSize(width: 220, height: 32)
    }
}

private extension CGFloat {
    func clamped(to range: ClosedRange<CGFloat>) -> CGFloat {
        Swift.min(Swift.max(self, range.lowerBound), range.upperBound)
    }
}
