local obj = {}
local json = require("hs.json")

-- ==========================================
-- CONFIGURATION
-- ==========================================
obj.layoutFile = os.getenv("HOME") .. "/.hammerspoon/saved_layout.json"
obj.saveInterval = 300 

-- Apps that require AppleScript authority
local stubbornApps = {
    ["Gemini"] = true,
    ["AFFiNE"] = true,
    ["Hammerspoon"] = true,
    ["System Settings"] = true
}

-- Apps or Bundle IDs that the window manager should NEVER touch
local ignoreList = {
    ["TheBoringNotch"] = true,
    ["theboringteam.boringnotch"] = true,
    ["boring.notch"] = true,
    ["The Boring Notch"] = true,
    ["Control Center"] = true,
    ["Notification Center"] = true,
    ["Dock"] = true,
    ["com.surteesstudios.Bartender"] = true,
    ["pro.betterdisplay.BetterDisplay"] = true
}

-- ==========================================
-- LOGIC
-- ==========================================

-- Check if window actually needs to be moved
local function needsMoving(win, savedFrame)
    local curr = win:frame()
    local threshold = 10 
    return math.abs(curr.x - savedFrame.x) > threshold or 
           math.abs(curr.y - savedFrame.y) > threshold
end

function obj.saveLayout(silent)
    -- Safety: Don't save if the system is currently sleeping or screens are locked
    if hs.caffeinate.get("displayIdle") or hs.caffeinate.get("systemIdle") then return end

    local allWindows = hs.window.allWindows()
    local consoleWin = hs.console.hswindow()
    if consoleWin then table.insert(allWindows, consoleWin) end

    local currentLayout = {
        saveTime = os.date("%Y-%m-%d %H:%M:%S"),
        windows = {}
    }

    for _, win in ipairs(allWindows) do
        local app = win:application()
        local screen = win:screen()
        local frame = win:frame()
        
        -- GHOST WINDOW FIX: Only save windows that have actual size
        if app and screen and win:isVisible() and frame.w > 0 and frame.h > 0 then
            local name = app:name() or ""
            local bid = app:bundleID() or ""
            
            -- IGNORE LOGIC: Match by name, Bundle ID, or fuzzy notch match
            local isIgnored = ignoreList[name] or ignoreList[bid] or name:find("TheBoringNotch")
            
            if not isIgnored then
                table.insert(currentLayout.windows, {
                    appName = name,
                    bundleID = bid,
                    winTitle = win:title(),
                    screenName = screen:name(),
                    x = frame.x, y = frame.y, w = frame.w, h = frame.h
                })
            end
        end
    end

    hs.json.write(currentLayout, obj.layoutFile, true, true)
    if not silent then hs.alert.show("Layout Saved: " .. currentLayout.saveTime, 1.5) end
end

function obj.restoreLayout()
    local data = hs.json.read(obj.layoutFile)
    if not data then return end
    local windowList = data.windows or data

    local appleScriptParts = {"tell application \"System Events\""}
    local needsAppleScript = false

    for _, winData in ipairs(windowList) do
        -- Handle Hammerspoon Console specifically
        if winData.appName == "Hammerspoon" and winData.winTitle == "Hammerspoon Console" then
            local cWin = hs.console.hswindow()
            if cWin and needsMoving(cWin, winData) then
                cWin:setFrame({x=winData.x, y=winData.y, w=winData.w, h=winData.h}, 0)
            end
        else
            local app = hs.application.get(winData.bundleID) or hs.application.get(winData.appName)
            if app then
                for _, win in ipairs(app:allWindows()) do
                    local targetScreen = hs.screen.find(winData.screenName) or hs.screen.primaryScreen()
                    
                    local isFuzzy = (winData.bundleID == "com.apple.Terminal" or winData.appName == "Gemini")
                    local isStubborn = stubbornApps[winData.appName]
                    local shouldMove = (win:title() == winData.winTitle or #app:allWindows() == 1 or isFuzzy or isStubborn)

                    if targetScreen and shouldMove and needsMoving(win, winData) then
                        if isStubborn then
                            needsAppleScript = true
                            table.insert(appleScriptParts, string.format([[
                                try
                                    tell process "%s" to set position of window 1 to {%d, %d}
                                    tell process "%s" to set size of window 1 to {%d, %d}
                                end try
                            ]], winData.appName, winData.x, winData.y, winData.appName, winData.w, winData.h))
                        else
                            win:setFrame({x=winData.x, y=winData.y, w=winData.w, h=winData.h}, 0)
                        end
                    end
                end
            end
        end
    end

    if needsAppleScript then
        table.insert(appleScriptParts, "end tell")
        hs.applescript.applescript(table.concat(appleScriptParts, "\n"))
    end
end

-- Hotkeys
hs.hotkey.bind({"shift", "cmd"}, "W", function() obj.saveLayout(false) end)
hs.hotkey.bind({"shift", "cmd"}, "R", function() obj.restoreLayout() end)

-- Automation: Screen Watcher
obj.screenWatcher = hs.screen.watcher.new(function()
    hs.timer.doAfter(2, function() obj.restoreLayout() end)
end):start()

-- Automation: Sleep Watcher
obj.sleepWatcher = hs.caffeinate.watcher.new(function(event)
    if event == hs.caffeinate.watcher.systemWillSleep or event == hs.caffeinate.watcher.screensDidLock then
        if obj.autoSaveTimer then obj.autoSaveTimer:stop() end
    elseif event == hs.caffeinate.watcher.systemDidWake or event == hs.caffeinate.watcher.screensDidUnlock then
        if obj.autoSaveTimer then obj.autoSaveTimer:start() end
        hs.timer.doAfter(3, function() obj.restoreLayout() end)
    end
end):start()

obj.autoSaveTimer = hs.timer.doEvery(obj.saveInterval, function() obj.saveLayout(true) end)

hs.alert.show("Window Manager Loaded", 1.5)

return obj