local ADDON_NAME, ns = ...

-- =========================================================
-- pom_heights.lua
-- All "heights" logic + UI lives here ONLY.
-- Other files should NOT read/write PlateOMaticDB.heights directly.
-- =========================================================

ns.heights = ns.heights or {}
local H = ns.heights

local DEFAULTS = {
    normal      = 16,
    plusOne     = 18,
    boss        = 20,
    eliteCaster = 18,
    eliteMelee  = 18,

    -- legacy compatibility during transition
    highlevel   = 20,
    caster      = 18,
    melee       = 18,
}

local ORDER = {
    { key = "normal",      label = "Normal Height" },
    { key = "boss",        label = "Boss / Highlevel Height" },
    { key = "plusOne",     label = "Mini boss Height" },
    { key = "eliteCaster", label = "Elite Caster Height" },
    { key = "eliteMelee",  label = "Elite Melee Height" },
}

local LEGACY_TO_MODERN = {
    highlevel = "boss",
    caster    = "eliteCaster",
    melee     = "eliteMelee",
}

local MODERN_TO_LEGACY = {
    boss        = "highlevel",
    eliteCaster = "caster",
    eliteMelee  = "melee",
}

-- READ-ONLY getter
local function GetDB()
    if not PlateOMaticDB then return nil end
    if type(PlateOMaticDB.heights) ~= "table" then return nil end
    return PlateOMaticDB.heights
end

-- write helper: only when we intentionally want to create/write
local function EnsureWritableDB()
    if not PlateOMaticDB then return nil end
    PlateOMaticDB.heights = PlateOMaticDB.heights or {}
    return PlateOMaticDB.heights
end

local function Clamp(v, lo, hi)
    v = tonumber(v)
    if not v then return nil end
    if v < lo then v = lo end
    if v > hi then v = hi end
    return v
end

local function SyncLegacyModernHeights(tbl)
    if type(tbl) ~= "table" then return end

    for oldKey, newKey in pairs(LEGACY_TO_MODERN) do
        if tbl[newKey] == nil and tbl[oldKey] ~= nil then
            tbl[newKey] = tbl[oldKey]
        elseif tbl[oldKey] == nil and tbl[newKey] ~= nil then
            tbl[oldKey] = tbl[newKey]
        end
    end
end

local function GetHeightValue(tbl, key)
    if type(tbl) ~= "table" then return nil end

    local legacyKey = MODERN_TO_LEGACY[key]
    if legacyKey then
        if tbl[key] ~= nil then
            return tbl[key]
        end
        return tbl[legacyKey]
    end

    return tbl[key]
end

local function SetHeightValue(tbl, key, value)
    if type(tbl) ~= "table" then return end

    tbl[key] = value

    local legacyKey = MODERN_TO_LEGACY[key]
    if legacyKey then
        tbl[legacyKey] = value
    end
end

-- ---------------------------------------------------------
-- Shared style resolver helpers
-- Keep these aligned with pom_mixins.lua
-- ---------------------------------------------------------
-- type() alone is not enough: a secret number still answers "number", and it
-- is `lvl == -1` below that raises. U.PlainNumber reduces first.
local function GetSafeUnitLevel(unit)
    if not unit or not UnitExists(unit) then return nil end
    return ns.util.PlainNumber(UnitLevel(unit))
end

local function GetSafePlayerLevel()
    return ns.util.PlainNumber(UnitLevel("player"))
end

local function IsBossLikeUnit(unit)
    if not unit or not UnitExists(unit) then return false end

    local lvl = GetSafeUnitLevel(unit)
    if lvl == -1 then
        return true
    end

    if UnitIsBossMob then
        -- The pcall protects the call; `ok and r` is the part that raises
        -- when r comes back secret.
        local r = ns.util.PlainBool(pcall(UnitIsBossMob, unit))
        if r then
            return true
        end
    end

    return false
end

local function ResolveEliteStyle(unit)
    if not unit or not UnitExists(unit) then
        return "eliteMelee"
    end

    -- Same conservative split as pom_mixins.lua. nil = restricted unit, and
    -- an unknown class falls to melee like every other non-paladin.
    local class = ns.util and ns.util.PlainClass(unit)
    if class == "PALADIN" then
        return "eliteCaster"
    end

    return "eliteMelee"
end

-- ---------------------------------------------------------
-- Ensure defaults + new global override keys (owned here)
-- NOTE: This writes, but it's intentional. Ideally called from InitDB / ToggleFrame.
-- ---------------------------------------------------------
function H:Ensure()
    if not PlateOMaticDB then return end

    local db = EnsureWritableDB()
    if not db then return end

    for k, v in pairs(DEFAULTS) do
        if db[k] == nil then
            db[k] = v
        end
    end

    SyncLegacyModernHeights(db)

    -- global override (stored on PlateOMaticDB root, but still OWNED by this file)
    if PlateOMaticDB.heightsGlobalEnabled == nil then
        PlateOMaticDB.heightsGlobalEnabled = false
    end
    if PlateOMaticDB.heightsGlobalValue == nil then
        PlateOMaticDB.heightsGlobalValue = 16
    end
end

-- ---------------------------------------------------------
-- StyleKey helper (height should follow style, not color)
-- ---------------------------------------------------------
function H:GetStyleKey(unit)
    if not unit or not UnitExists(unit) then
        return "normal"
    end

    local lvl = GetSafeUnitLevel(unit)
    local pl  = GetSafePlayerLevel()
    local cls = ns.util.PlainClassification(unit)

    if IsBossLikeUnit(unit) then
        return "boss"
    end

    if lvl and pl and lvl >= pl + 2 then
        return "boss"
    end

    if lvl and pl and lvl == pl + 1 then
        return "plusOne"
    end

    if cls == "elite" then
        return ResolveEliteStyle(unit)
    end

    return "normal"
end

-- Backward-compat wrapper for any old callers
function H:GetTypeKey(unit)
    return self:GetStyleKey(unit)
end

function H:Get(typeKey)
    local db = GetDB()
    local def = DEFAULTS[typeKey] or DEFAULTS.normal

    if not db then
        return def
    end

    local raw = GetHeightValue(db, typeKey)
    local v = Clamp(raw, 6, 64)
    if not v then
        return def
    end
    return v
end

function H:Set(typeKey, value)
    local db = EnsureWritableDB()
    if not db then return end

    local v = Clamp(value, 6, 64)
    if not v then return end

    SetHeightValue(db, typeKey, v)
end

function H:GetGlobalEnabled()
    return PlateOMaticDB and PlateOMaticDB.heightsGlobalEnabled and true or false
end

function H:GetGlobalValue()
    if not PlateOMaticDB then return 16 end
    local v = Clamp(PlateOMaticDB.heightsGlobalValue, 6, 64)
    if not v then v = 16 end
    return v
end

function H:SetGlobalEnabled(v)
    if not PlateOMaticDB then return end
    PlateOMaticDB.heightsGlobalEnabled = v and true or false
end

function H:SetGlobalValue(v)
    if not PlateOMaticDB then return end
    local n = Clamp(v, 6, 64)
    if not n then return end
    PlateOMaticDB.heightsGlobalValue = n
end

-- ---------------------------------------------------------
-- Apply height to a statusbar (MED FIX)
-- IMPORTANT:
--   Apply MUST NOT write to DB. InitDB/ToggleFrame owns Ensure().
-- ---------------------------------------------------------
function H:Apply(healthBar, styleKey)
    if not (healthBar and healthBar.SetHeight) then return end

    local value
    if self:GetGlobalEnabled() then
        value = self:GetGlobalValue()
    else
        value = self:Get(styleKey)
    end

    -- Blizzard locks TOP+BOTTOM; must re-anchor to allow SetHeight
    healthBar:ClearAllPoints()

    local parent = healthBar:GetParent()
    if parent then
        healthBar:SetPoint("TOPLEFT", parent, "TOPLEFT", 1, -1)
        healthBar:SetPoint("TOPRIGHT", parent, "TOPRIGHT", -1, -1)
    end

    healthBar:SetHeight(value)
end

-- Apply directly from unit (safe even when you don't run UpdateState)
function H:ApplyForUnit(healthBar, unit)
    if not unit then return end
    local styleKey = self:GetStyleKey(unit)
    self:Apply(healthBar, styleKey)
end

-- =========================================================
-- UI helpers
-- =========================================================
local function MakeRow(parent, label, key, x, y, onChange)
    local row = CreateFrame("Frame", nil, parent)
    row:SetSize(420, 20)
    row:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)

    local txt = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    txt:SetPoint("LEFT", row, "LEFT", 0, 0)
    txt:SetText(label)

    local eb = CreateFrame("EditBox", nil, row, "InputBoxTemplate")
    eb:SetSize(60, 18)
    eb:SetPoint("RIGHT", row, "RIGHT", 0, 0)
    eb:SetAutoFocus(false)
    eb:SetNumeric(true)

    local function refresh()
        eb:SetNumber(H:Get(key))
    end

    eb:SetScript("OnEnterPressed", function(self)
        local v = tonumber(self:GetText())
        if v then
            H:Set(key, v)
            refresh()
            if onChange then onChange() end
        else
            refresh()
        end
        self:ClearFocus()
    end)

    eb:SetScript("OnEscapePressed", function(self)
        refresh()
        self:ClearFocus()
    end)

    refresh()
    row._refresh = refresh
    return row
end

local function MakeGlobalRow(parent, x, y, onChange)
    local row = CreateFrame("Frame", nil, parent)
    row:SetSize(480, 22)
    row:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)

    local cb = CreateFrame("CheckButton", nil, row, "ChatConfigCheckButtonTemplate")
    cb:SetPoint("LEFT", row, "LEFT", 0, 0)

    local lbl = cb.Text or cb.text
    if lbl and lbl.SetText then
        lbl:SetText("Use Global Height Override")
        lbl:SetTextColor(1, 0.9, 0.6)
    end

    local eb = CreateFrame("EditBox", nil, row, "InputBoxTemplate")
    eb:SetSize(60, 18)
    eb:SetPoint("RIGHT", row, "RIGHT", 0, 0)
    eb:SetAutoFocus(false)
    eb:SetNumeric(true)

    local hint = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    hint:SetPoint("RIGHT", eb, "LEFT", -8, 0)
    hint:SetText("Height")

    local function refresh()
        cb:SetChecked(H:GetGlobalEnabled())
        eb:SetNumber(H:GetGlobalValue())
        eb:SetEnabled(H:GetGlobalEnabled())
        if H:GetGlobalEnabled() then
            eb:SetAlpha(1); hint:SetAlpha(1)
        else
            eb:SetAlpha(0.4); hint:SetAlpha(0.4)
        end
    end

    cb:SetScript("OnClick", function(self)
        H:SetGlobalEnabled(self:GetChecked() and true or false)
        refresh()
        if onChange then onChange() end
    end)

    eb:SetScript("OnEnterPressed", function(self)
        local v = tonumber(self:GetText())
        if v then
            H:SetGlobalValue(v)
            refresh()
            if onChange then onChange() end
        else
            refresh()
        end
        self:ClearFocus()
    end)

    eb:SetScript("OnEscapePressed", function(self)
        refresh()
        self:ClearFocus()
    end)

    refresh()
    row._refresh = refresh
    return row
end

function H:BuildUI(parent, onChange)
    self:Ensure()

    local p = CreateFrame("Frame", nil, parent)
    p:SetAllPoints(parent)

    local title = p:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", p, "TOPLEFT", 16, -16)
    title:SetText("Heights")

    local sub = p:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    sub:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -6)
    sub:SetText("Style-based heights with backward-compatible storage. Enter number and press Enter.")

    local y = -58
    p._rows = {}

    local gr = MakeGlobalRow(p, 16, y, onChange)
    table.insert(p._rows, gr)
    y = y - 30

    for i = 1, #ORDER do
        local it = ORDER[i]
        local r = MakeRow(p, it.label, it.key, 16, y, onChange)
        p._rows[i + 1] = r
        y = y - 24
    end

    local resetBtn = CreateFrame("Button", nil, p, "UIPanelButtonTemplate")
    resetBtn:SetSize(220, 22)
    resetBtn:SetPoint("TOPLEFT", p, "TOPLEFT", 16, y - 8)
    resetBtn:SetText("Reset Heights to Default")
    resetBtn:SetScript("OnClick", function()
        local db = EnsureWritableDB()
        if not db then return end

        for k, v in pairs(DEFAULTS) do
            db[k] = v
        end

        SyncLegacyModernHeights(db)

        PlateOMaticDB.heightsGlobalEnabled = false
        PlateOMaticDB.heightsGlobalValue = 16

        if p._rows then
            for _, r in ipairs(p._rows) do
                if r and r._refresh then r._refresh() end
            end
        end

        if onChange then onChange() end
    end)

    return p
end

function H:ToggleFrame(onChange)
    self:Ensure()

    if self._frame and self._frame:IsShown() then
        self._frame:Hide()
        return
    end

    if not self._frame then
        local f = CreateFrame("Frame", "POM_HeightsFrame", UIParent, "BasicFrameTemplateWithInset")
        f:SetSize(560, 300)
        f:SetPoint("CENTER")
        f:SetFrameStrata("FULLSCREEN_DIALOG")
        f:SetMovable(true)
        f:EnableMouse(true)
        f:RegisterForDrag("LeftButton")
        f:SetScript("OnDragStart", f.StartMoving)
        f:SetScript("OnDragStop", f.StopMovingOrSizing)

        if f.TitleText then
            f.TitleText:SetText("Plate-o-Matic - Heights")
        end

        self._frame = f
        self._content = self:BuildUI(f, onChange)
    end

    if self._content and self._content._rows then
        for _, r in ipairs(self._content._rows) do
            if r and r._refresh then r._refresh() end
        end
    end

    self._frame:Show()
end