-- LayoutSelector.lua

local selector = {}
local json = require("hs.json")
local styledtext = require("hs.styledtext")

-- ==========================================
-- CONFIGURATION & CENTRAL JSON CONFIG LOADING
-- ==========================================
local homeDir = os.getenv("HOME") or "/Users/francop"
selector.storageDir = homeDir .. "/.hammerspoon/layouts/"
local configFile = homeDir .. "/.hammerspoon/config.json"
selector.layoutHotkeys = {} 
selector.staticHotkeys = {} 
selector.instanceChooser = nil 
selector.actionChooser = nil -- Secondary chooser cache for profile actions

if not hs.fs.attributes(selector.storageDir) then hs.fs.mkdir(selector.storageDir) end

-- Baseline fallbacks
local stubbornAppsList = {
    ["Gemini"] = true, ["AFFiNE"] = true, ["Terminal"] = true,
    ["System Settings"] = true, ["Hammerspoon"] = true
}

local ignoreListItems = {
    ["TheBoringNotch"] = true, ["Control Center"] = true, ["Notification Center"] = true,
    ["Dock"] = true, ["BetterDisplay"] = true, ["Stats"] = true, ["DockDoor"] = true,
    ["Bartender 6"] = true, ["Bartender"] = true, ["Arq"] = true, ["Arq Agent"] = true, ["TypeWhisper"] = true,
    ["com.typewhisper.mac"] = true
}

-- Load Central Config JSON if available
local cfgData = hs.json.read(configFile)
if cfgData then
    if cfgData.stubbornAppsList then stubbornAppsList = cfgData.stubbornAppsList end
    if cfgData.ignoreListItems then ignoreListItems = cfgData.ignoreListItems end
    print("LayoutSelector: Successfully loaded rules from central config.json")
else
    print("LayoutSelector: Central config.json not found, utilizing default fallbacks")
end

-- ==========================================
-- INTERNAL UTILITIES
-- ==========================================

local function wrapText(text, limit)
    local lines = {}
    local currentLine = ""
    for word in text:gmatch("%S+") do
        if #currentLine + #word >= limit then
            table.insert(lines, currentLine)
            currentLine = "            " .. word 
        else
            currentLine = currentLine == "" and "      ↳ " .. word or currentLine .. " " .. word
        end
    end
    table.insert(lines, currentLine)
    return lines
end

local function getLayoutFiles()
    local files = {}
    local iter, dir_obj = hs.fs.dir(selector.storageDir)
    if iter then for f in iter, dir_obj do if f:find("%.json$") then table.insert(files, f) end end end
    table.sort(files)
    return files
end

-- ==========================================
-- CORE LOGIC
-- ==========================================

local function captureCurrentLayout()
    local screens = hs.screen.allScreens()
    local layout = { 
        saveTime = os.date("%Y-%m-%d %H:%M:%S"), 
        screenCount = #screens,
        mode = (#screens > 1) and "Docked" or "Laptop",
        windows = {} 
    }
    for _, win in ipairs(hs.window.allWindows()) do
        local app = win:application()
        if app and win:isVisible() and win:frame().w > 0 then
            local appName = app:name() or ""
            local bundleID = app:bundleID() or ""
            local winTitle = win:title() or "Untitled Window"
            -- Hybrid check for Capture
            if not ignoreListItems[appName] and not ignoreListItems[bundleID] then
                table.insert(layout.windows, {
                    appName = appName, bundleID = bundleID, winTitle = winTitle,
                    x = math.floor(win:frame().x), y = math.floor(win:frame().y),
                    w = math.floor(win:frame().w), h = math.floor(win:frame().h)
                })
            end
        end
    end
    return layout
end

local function executeRestore(filePath, layoutName)
    local data = hs.json.read(filePath)
    if not data or not data.windows then return end
    
    local launchedAny = false
    for _, winData in ipairs(data.windows) do
        local app = hs.application.get(winData.bundleID) or hs.application.get(winData.appName)
        if not app then
            hs.application.launchOrFocusByBundleID(winData.bundleID)
            launchedAny = true
        end
    end

    local function moveWindows()
        local savedByApp = {}
        for _, winData in ipairs(data.windows) do
            if not ignoreListItems[winData.appName] and not ignoreListItems[winData.bundleID] then
                savedByApp[winData.appName] = savedByApp[winData.appName] or {}
                table.insert(savedByApp[winData.appName], winData)
            end
        end

        for appName, entries in pairs(savedByApp) do
            table.sort(entries, function(a, b)
                if a.y == b.y then return a.x < b.x end
                return a.y < b.y
            end)
        end

        for appName, savedEntries in pairs(savedByApp) do
            local app = hs.application.get(savedEntries[1].bundleID) or hs.application.get(appName)
            if app then
                local physicalWins = {}
                for _, w in ipairs(app:allWindows()) do
                    if w:isVisible() and w:frame().w > 0 then table.insert(physicalWins, w) end
                end

                table.sort(physicalWins, function(a, b)
                    local af, bf = a:frame(), b:frame()
                    if af.y == bf.y then return af.x < bf.x end
                    return af.y < bf.y
                end)

                for i, winData in ipairs(savedEntries) do
                    local win = physicalWins[i]
                    if win then
                        if win:isMinimized() then win:unminimize() end
                        local x, y, w, h = winData.x, winData.y, winData.w, winData.h
                        local isStubborn = stubbornAppsList[appName] or (app:path() or ""):find("Electron")

                        local function moveAction()
                            if isStubborn then
                                local safeTitle = win:title():gsub('"', '\\"')
                                local winTarget = (appName == "Gemini") and "window 1" or string.format('first window whose name is "%s"', safeTitle)
                                local script = string.format([[
                                    tell application "System Events" to tell (first process whose unix id is %d)
                                        try
                                            set targetWin to %s
                                            set position of targetWin to {%d, %d}
                                            set size of targetWin to {%d, %d}
                                        end try
                                    end tell
                                ]], app:pid(), winTarget, x, y, w, h)
                                hs.applescript.applescript(script)
                            else
                                win:setFrame({x=x, y=y, w=w, h=h}, 0)
                            end
                        end
                        moveAction()
                        hs.timer.doAfter(0.5, moveAction)
                        hs.timer.doAfter(1.5, moveAction)
                    end
                end
            end
        end
        hs.alert.show("Restored: " .. layoutName, 1.5)
    end

    if launchedAny then
        hs.alert.show("Syncing Apps...", 3)
        hs.timer.doAfter(4.5, moveWindows)
    else
        moveWindows()
    end
end

-- ==========================================
-- RAYCAST EXTERNAL IPC HOOKS & MANAGEMENT
-- ==========================================

function selector.save(name)
    if not name or name == "" then 
        hs.alert.show("Error: Invalid Layout Name", 2)
        return false 
    end
    local path = selector.storageDir .. name .. ".json"
    local layoutData = captureCurrentLayout()
    hs.json.write(layoutData, path, true, true)
    selector.rebindLayoutKeys()
    hs.alert.show("Saved Layout: " .. name, 2)
    return true
end

function selector.load(name)
    if not name or name == "" then 
        hs.alert.show("Error: Specify Layout Name", 2)
        return false 
    end
    local path = selector.storageDir .. name .. ".json"
    if hs.fs.attributes(path) then
        executeRestore(path, name)
        return true
    else
        hs.alert.show("Layout Not Found: " .. name, 2)
        return false
    end
end

-- Secondary Selector Options Window (Handles single profile rules)
function selector.showLayoutActions(layoutName)
    local path = selector.storageDir .. layoutName .. ".json"
    
    local actionChoices = {
        {
            text = "🚀 Restore Layout Profile",
            subText = "Snap open apps back into position for: " .. layoutName,
            action = "restore"
        },
        {
            text = "🔄 Update / Overwrite Profile",
            subText = "Replace layout coordinates with your current window setup",
            action = "update"
        },
        {
            text = "🗑️ Delete Layout Profile",
            subText = "Permanently remove the profile config file from disk",
            action = "delete"
        }
    }

    if selector.actionChooser then selector.actionChooser:hide() end

    selector.actionChooser = hs.chooser.new(function(choice)
        if choice then
            if choice.action == "restore" then
                selector.load(layoutName)
            elseif choice.action == "update" then
                hs.json.write(captureCurrentLayout(), path, true, true)
                hs.alert.show("Updated: " .. layoutName, 2)
            elseif choice.action == "delete" then
                os.remove(path)
                selector.rebindLayoutKeys()
                hs.alert.show("Deleted Profile: " .. layoutName, 2)
            end
        end
    end)

    selector.actionChooser:placeholderText("Manage Layout Context [" .. layoutName .. "]...")
    selector.actionChooser:choices(actionChoices)
    selector.actionChooser:show()
end

-- Primary Selection Interface Window
function selector.showMenu()
    local choices = {
        {
            text = "📸 Create / Save New Layout...",
            subText = "Take a snapshot snapshot of your active desktop workspace configuration",
            action = "create"
        }
    }

    local files = getLayoutFiles()
    for _, file in ipairs(files) do
        local label = file:gsub("%.json$", "")
        local path = selector.storageDir .. file
        local data = hs.json.read(path)
        
        local subTextStr = "Layout Profile"
        if data then
            local modeStr = (data.mode == "Docked") and "🖥️ Docked" or "💻 Laptop"
            subTextStr = string.format("%s Mode | Screens: %d | Saved: %s", modeStr, data.screenCount or 1, data.saveTime or "Unknown")
        end

        table.insert(choices, {
            text = "📁 " .. label,
            subText = subTextStr,
            action = "manage",
            layoutName = label
        })
    end

    if selector.instanceChooser then selector.instanceChooser:hide() end

    selector.instanceChooser = hs.chooser.new(function(choice)
        if choice then
            if choice.action == "create" then
                -- Fallback input trigger to type profile key
                local _, name = hs.dialog.textPrompt("Create Window Layout", "Enter a unique profile label name:", "", "Save", "Cancel")
                if name and name ~= "" then
                    selector.save(name)
                end
            elseif choice.action == "manage" then
                -- Hand context frame mapping loop to the action menu selector block
                selector.showLayoutActions(choice.layoutName)
            end
        end
    end)

    selector.instanceChooser:placeholderText("Select a layout workspace profile or register a new one...")
    selector.instanceChooser:choices(choices)
    selector.instanceChooser:show()
end

-- ==========================================
-- HOTKEY MANAGEMENT
-- ==========================================

function selector.rebindLayoutKeys()
    for _, hk in pairs(selector.layoutHotkeys) do hk:delete() end
    selector.layoutHotkeys = {}

    local files = getLayoutFiles()
    for i, file in ipairs(files) do
        if i > 9 then break end
        local path = selector.storageDir .. file
        local label = file:gsub("%.json$", "")
        selector.layoutHotkeys[i] = hs.hotkey.bind({"ctrl", "alt", "cmd"}, tostring(i), function() executeRestore(path, label) end)
    end
    print("LayoutSelector: Dynamic Hotkeys Rebuilt")
end

-- ==========================================
-- INITIALIZATION & GLOBAL EXPORT
-- ==========================================

selector.staticHotkeys["save"] = hs.hotkey.bind({"shift", "ctrl", "alt", "cmd"}, "S", function()
    local _, name = hs.dialog.textPrompt("New Layout", "Enter name:", "", "Save", "Cancel")
    if name and name ~= "" then
        selector.save(name)
    end
end)

selector.rebindLayoutKeys()

LayoutSelector = selector

return selector