---@diagnostic disable: undefined-field, undefined-global, inject-field
local _, namespace = ...

local WTUI = namespace.WildThingsUI
local Theme = namespace.Theme

--[[
BindWidget + DirtyFlag + RefreshAllWidgets (TOOLKIT-F1, TOOLKIT-F3).

BindWidget is a factory that creates a widget bound to a WildThingsSavedVars leaf path:
  WTUI:BindWidget(parent, {type="toggle", path="update.party_mode", label="Party Mode"})

The factory resolves the dot-separated path, reads the initial value, creates the widget,
wires auto-save (changes write directly to SavedVars), applies a DirtyFlag decorator
(transient amber border on change), and registers the widget for RefreshAllWidgets.

DirtyFlag is a 1px DIRTY_AMBER rounded border that appears when the widget's setter fires
and auto-clears after ~0.75s via C_Timer.After. It uses a separate BuildRoundedStroke
instance (stored as ctrl._dirtyBorder) to never clobber a widget's own roundedBorder.

RefreshAllWidgets iterates the global registry and re-syncs every bound widget from
SavedVars without firing any setter -- using each widget type's suppress-callback path.
]]

--------------------------------------------------------------------------------
-- Dirty-Flag decorator (TOOLKIT-F3, D-01)
--------------------------------------------------------------------------------

local function BuildDirtyBorder(widget, radius, thickness)
    -- Use BuildRoundedStroke directly -- NOT WTUI:AddRoundedBorder, which hardcodes
    -- the field name "roundedBorder" (Functions.lua:309). The dirty border is a separate
    -- instance stored as widget._dirtyBorder so it never clobbers a widget's own border.
    local stroke = WTUI.BuildRoundedStroke(widget, radius, thickness)
    widget._dirtyBorder = stroke

    -- Install the four border methods on the dirty border instance, matching
    -- AddRoundedBorder's contract (Functions.lua:311-329).
    function widget:SetDirtyBorderColor(r, g, b, a)
        widget._dirtyBorder:SetStrokeColor(r, g, b, a)
    end

    function widget:ShowDirtyBorder()
        widget._dirtyBorder:Show()
    end

    function widget:HideDirtyBorder()
        widget._dirtyBorder:Hide()
    end

    function widget:SetDirtyBorderShown(shown)
        if shown then
            widget:ShowDirtyBorder()
        else
            widget:HideDirtyBorder()
        end
    end

    -- Initial state: amber color, hidden.
    widget:SetDirtyBorderColor(Theme.DIRTY_AMBER.r, Theme.DIRTY_AMBER.g, Theme.DIRTY_AMBER.b, Theme.DIRTY_AMBER.a)
    widget:HideDirtyBorder()
end

--- Shows the dirty-flag amber border on a widget and schedules auto-clear.
--- Idempotent: cancels any existing timer and restarts the countdown.
--- @param widget Frame -- the widget to show the dirty border on (must have _dirtyBorder)
function WTUI:ShowDirtyFlag(widget)
    if not widget._dirtyBorder then return end

    widget:SetDirtyBorderColor(Theme.DIRTY_AMBER.r, Theme.DIRTY_AMBER.g, Theme.DIRTY_AMBER.b, Theme.DIRTY_AMBER.a)
    widget:ShowDirtyBorder()

    -- Cancel any existing timer and restart the auto-clear countdown.
    if widget._dirtyTimer then
        widget._dirtyTimer:Cancel()
    end

    widget._dirtyTimer = C_Timer.After(0.75, function()
        if widget._dirtyBorder then
            widget:HideDirtyBorder()
        end
        widget._dirtyTimer = nil
    end)
end

--------------------------------------------------------------------------------
-- BindWidget factory (TOOLKIT-F1, D-02, D-04, D-05)
--------------------------------------------------------------------------------

-- Resolves a dot-separated path (e.g. "update.party_mode") to a WildThingsSavedVars leaf.
-- Walks all segments except the last to find the parent table, then reads/writes the leaf.
-- Returns the leaf value and a setter closure. If an INTERMEDIATE segment is nil
-- (corrupted SavedVars), returns nil, nil -- caller shows a graceful-degradation placeholder.
-- A nil leaf value (last segment missing) is fine -- the setter still works on the parent.
local function ResolvePath(path)
    -- Collect segments.
    local segments = {}
    for seg in string.gmatch(path, "[^.]+") do
        segments[#segments + 1] = seg
    end
    if #segments == 0 then return nil, nil end

    -- Walk to the parent table (all segments except the last).
    local parent = WildThingsSavedVars
    for i = 1, #segments - 1 do
        if parent == nil then return nil, nil end
        parent = parent[segments[i]]
    end
    if parent == nil then return nil, nil end

    local leafKey = segments[#segments]
    local leaf = parent[leafKey]

    -- Setter writes to the verified parent table.
    local function SetLeaf(value)
        parent[leafKey] = value
    end

    return leaf, SetLeaf
end

--- Creates a widget bound to a WildThingsSavedVars leaf path.
--- @param parent Frame -- parent frame for the widget row
--- @param def table -- {type, path, label, ...} descriptor
--- @return Frame row, number height
function WTUI:BindWidget(parent, def)
    local leaf, setLeaf = ResolvePath(def.path)

    -- Graceful degradation: corrupted SavedVars returns nil setter (intermediate path
    -- segment missing). A nil leaf value (last segment missing) is fine — the parent
    -- table exists and the setter still works.
    if not setLeaf then
        local spacer = CreateFrame("Frame", nil, parent)
        spacer:SetHeight(1)
        return spacer, 1
    end

    local row = CreateFrame("Frame", nil, parent)
    local ctrl

    if def.type == "toggle" then
        row:SetHeight(Theme.H_ROW)

        ctrl = namespace:CreateToggle(row, function(checked)
            setLeaf(checked)
            WTUI:ShowDirtyFlag(ctrl)
        end)

        ctrl:SetPoint("LEFT", row, "LEFT", 0, 0)
        ctrl:SetChecked(leaf, true) -- true = suppress initial OnValueChanged fire

        local text = row:CreateFontString(nil, "ARTWORK")
        text:SetFontObject(WildThingsFont13)
        text:SetText(def.label)
        text:SetTextColor(Theme.TEXT.r, Theme.TEXT.g, Theme.TEXT.b, Theme.TEXT.a)
        text:SetPoint("LEFT", ctrl, "RIGHT", Theme.GAP, 0)

        function row:Refresh()
            ctrl:SetChecked(ResolvePath(def.path), true) -- true = dontRun suppress
        end

        -- Forward Enable/Disable for BuildWidgets' def.disabled dispatch.
        function row:Enable() ctrl:Enable() end
        function row:Disable() ctrl:Disable() end

    elseif def.type == "slider" then
        local min = def.min or 0
        local max = def.max or 1
        if min >= max then min = 0; max = 1 end

        -- Slider is anchored LEFT only (not stretched) so the valueBox stays
        -- visible to the right of the track. The row wrapper stretches to full
        -- width; the slider keeps its native DEFAULT_WIDTH.
        ctrl = namespace:CreateSlider(row, def.label, min, max, function(v)
            setLeaf(v)
            WTUI:ShowDirtyFlag(ctrl)
        end, leaf or math.floor((max - min) / 2))

        ctrl:SetPoint("TOPLEFT", row, "TOPLEFT", 0, -8)
        row:SetHeight(48)

        function row:Refresh()
            local current = ResolvePath(def.path)
            if current ~= nil then
                ctrl:SetValueSilent(current)
            end
        end

        function row:Enable() ctrl:Enable() end
        function row:Disable() ctrl:Disable() end

    elseif def.type == "dropdown" then
        ctrl = namespace:CreateDropdown(row, def.label, def.infoTable, function(...)
            local values = {...}
            setLeaf(values)
            WTUI:ShowDirtyFlag(ctrl)
        end, leaf)

        ctrl:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
        row:SetHeight(Theme.H_DROPDOWN)

        function row:Refresh()
            local current = ResolvePath(def.path)
            if current then
                ctrl:SetValueSilent(current)
            end
        end

        function row:Enable() ctrl:Enable() end
        function row:Disable() ctrl:Disable() end

    elseif def.type == "editbox" then
        -- No-op OnValueChanged: EditBox fires on every keystroke, commit happens on focus-lost
        -- (matching BuildWidgets.lua editbox builder pattern).
        ctrl = namespace:CreateEditBox(row, def.label, function() end)
        if leaf ~= nil then
            ctrl:SetText(tostring(leaf))
        end

        ctrl:HookScript("OnEditFocusLost", function()
            setLeaf(ctrl.currentValue)
            WTUI:ShowDirtyFlag(ctrl)
        end)

        ctrl:SetPoint("TOPLEFT", row, "TOPLEFT", 0, 0)
        ctrl:SetPoint("TOPRIGHT", row, "TOPRIGHT", 0, 0)
        row:SetHeight(Theme.H_INPUT)

        function row:Refresh()
            local current = ResolvePath(def.path)
            if current ~= nil then
                ctrl:SetText(tostring(current))
            end
        end

        function row:Enable() ctrl:Enable() end
        function row:Disable() ctrl:Disable() end

    else
        error("BindWidget: unknown type '" .. tostring(def.type) .. "'")
    end

    -- Apply DirtyFlag decorator to the inner control (not the wrapper row).
    BuildDirtyBorder(ctrl, Theme.RADIUS_SM, 1)

    -- Register for global refresh.
    WTUI._boundWidgets = WTUI._boundWidgets or {}
    WTUI._boundWidgets[#WTUI._boundWidgets + 1] = {row = row, ctrl = ctrl}

    return row, Theme.H_ROW
end

--------------------------------------------------------------------------------
-- RefreshAllWidgets (TOOLKIT-F1, D-03)
--------------------------------------------------------------------------------

--- Re-syncs every bound widget from SavedVars without firing any setter.
--- Each widget's Refresh uses its type's suppress-callback path so the dirty
--- flag never triggers on a refresh cycle.
function WTUI:RefreshAllWidgets()
    WTUI._boundWidgets = WTUI._boundWidgets or {}
    for _, entry in ipairs(WTUI._boundWidgets) do
        if entry.row and entry.row.Refresh then
            entry.row:Refresh()
        end
    end
end

-- Initialize the global registry.
WTUI._boundWidgets = WTUI._boundWidgets or {}
