-- HousingDecorGuide/Modules/HDG_TopologyCapture.lua
-- Pin-frame snapshot capture for the player's house topology.
-- Ported from ProfTools/ProfTools_LayoutProbe.lua, adapted to HDG conventions.

local _, privateDB = ...

local IDs      = privateDB.HDG_ProjectsIDs
local FP       = privateDB.HDG_FingerprintMatcher
local Store    = privateDB.HDG_ProjectsStore
local RD       = privateDB.HDG_RotationDeducer
-- ShapesDB consumed indirectly via RD; no direct reference here.
-- HDG_EventBus lives on the HDG global (HDG.EventBus) and uses Register/Trigger
local function _EventBus()
    local g = _G.HousingDecorGuide
    return g and g.EventBus
end
local Debug    = privateDB.HDG_Debug          -- existing HDG debug logger

-- Detect player's faction → houseID
local function _currentHouseID()
    local faction = UnitFactionGroup("player")
    if faction == "Alliance" then return IDs.makeHouseID("alliance") end
    if faction == "Horde"    then return IDs.makeHouseID("horde") end
    return nil
end

-- Debug flag: set to true in-game to print raw door facings per pin for convention verification.
local DEBUG_FACINGS = false

-- Layout mode = 3; Enum is preferred but provide a safety fallback
local function _layoutMode()
    return (Enum and Enum.HouseEditorMode and Enum.HouseEditorMode.Layout) or 3
end

local M = {}

-- Capture state (transient across one Layout-mode session)
local capture = nil

-- Sweep state (forward-declared so _onFloorChanged can reference it)
local activeSweep = nil

local function _newCapture()
    return {
        active = false,
        rooms  = {},   -- { capturedID → { shape, name, blizzardGUID, doors = {} } }
        floor  = nil,  -- floor index for this snapshot
    }
end

-- Capture the player's facing-room from a single pin frame
local function _ingestPin(pinFrame)
    if not capture then return end
    if not pinFrame or not pinFrame.GetPinType then return end
    local pinType = pinFrame:GetPinType()
    local roomGUID = pinFrame:GetRoomGUID()
    if not roomGUID then return end

    capture.rooms[roomGUID] = capture.rooms[roomGUID] or {
        shape        = nil,
        name         = nil,
        blizzardGUID = roomGUID,
        doors        = {},
        rawFacings   = {},          -- raw radian values from pin-frame (compass-bearing)
        capturedID   = roomGUID,    -- transient ID = roomGUID for this session
    }
    local room = capture.rooms[roomGUID]

    if pinType == 1 then
        -- Room pin — gives us the localized name
        room.name = pinFrame:GetRoomName()
        -- Shape is derived from name in P1 (DB2 shape catalog arrives in P5).
        -- Use a minimal name→shape map for now; unknown shapes get the literal name.
        room.shape = M.NameToShape(room.name) or room.name
    elseif pinType == 0 then
        -- Door pin
        local d = pinFrame:GetDoorConnectionInfo()
        if d then
            table.insert(room.doors, {
                doorID   = d.doorID,
                facing   = d.doorFacing,
                occupied = pinFrame:IsOccupiedDoor() and true or false,
            })
            table.insert(room.rawFacings, d.doorFacing)
            if DEBUG_FACINGS and Debug and Debug.Log then
                Debug.Log("HDG.TopologyCapture.FACING",
                    string.format("name=%s doorFacing=%.4f occupied=%s",
                        tostring(room.name), d.doorFacing,
                        tostring(pinFrame:IsOccupiedDoor() and true or false)))
            end
        end
    end
end

-- Minimal name→shape mapping (English fallback; populated more fully in P5 via DB2)
local NAME_TO_SHAPE = {
    ["Closet"]                = "closet_xs",
    ["Square Room (Small)"]   = "square_s",
    ["Square Room (Medium)"]  = "square_m",
    ["Square Room (Large)"]   = "square_l",
    ["Octagon Room (Small)"]  = "octagon_s",
    ["Octagon Room (Medium)"] = "octagon_m",
    ["Octagon Room (Large)"]  = "octagon_l",
    ["L-Shaped Room"]         = "l_shape",
    ["T-Shaped Room"]         = "t_shape",
    ["Cross-Shaped Room"]     = "cross_shape",
    ["Hallway"]               = "hallway",
    ["Entry"]                 = "entry",
    ["Evening Circle Room"]   = "circle_evening",
    ["Daylight Circle Room"]  = "circle_daylight",
}

function M.NameToShape(localizedName)
    return NAME_TO_SHAPE[localizedName]
end

-- Public API ------------------------------------------------------------------

function M.IsCapturing()
    return capture and capture.active or false
end

function M.BeginCapture(floor)
    capture = _newCapture()
    capture.active = true
    capture.floor = floor or 1
    if Debug and Debug.Log then Debug.Log("HDG.TopologyCapture", "begin floor=" .. tostring(floor)) end
end

function M.EndCapture()
    if not capture or not capture.active then return nil end
    capture.active = false
    local snapshot = capture
    capture = nil
    if Debug and Debug.Log then
        local n = 0
        if snapshot then for _ in pairs(snapshot.rooms) do n = n + 1 end end
        Debug.Log("HDG.TopologyCapture", "end rooms=" .. n)
    end
    return snapshot
end

function M.OnPinFrameAdded(pinFrame)
    if capture and capture.active then _ingestPin(pinFrame) end
end

-- Convert capture.rooms into the fingerprint matcher's "captured" list shape
local function _toCapturedList(rooms)
    local list = {}
    for _, r in pairs(rooms) do
        list[#list + 1] = {
            capturedID  = r.capturedID,
            shape       = r.shape,
            doors       = r.doors,
            rawFacings  = r.rawFacings,
            _name       = r.name,
            _guid       = r.blizzardGUID,
        }
    end
    return list
end

-- Convert Store rooms (for a floor) into the matcher's "existing" list shape
local function _toExistingList(houseID, floor)
    local floorID = IDs.makeFloorID(houseID, floor)
    if not floorID then return {} end
    local entries = Store.GetRoomsForFloor(floorID)
    local list = {}
    for _, e in ipairs(entries) do
        if e.room.fingerprint then
            list[#list + 1] = {
                existingID  = e.id,
                fingerprint = e.room.fingerprint,
            }
        end
    end
    return list
end

function M.Finalize(snapshot, houseID)
    if not snapshot or not houseID then return nil end
    local floor = snapshot.floor or 1
    local floorID = IDs.makeFloorID(houseID, floor)
    if not floorID then return nil end

    local capturedList = _toCapturedList(snapshot.rooms)
    local existingList = _toExistingList(houseID, floor)

    local match = FP.matchRooms(capturedList, existingList)

    local now = time and time() or 0
    local stats = { matched = 0, new = 0, orphaned = 0, ambiguous = 0 }

    -- Helper: deduce rotation for a captured room and return {rotation, worldCardinals}
    local function _deduceRotation(cap)
        if not RD or not cap.shape or not cap.rawFacings then return nil, nil end
        local rotation, err = RD.deduce(cap.shape, cap.rawFacings)
        if rotation ~= nil then
            local worldCardinals = RD.facingsToCardinals(cap.shape, rotation, "compass")
            return rotation, worldCardinals
        end
        if Debug and Debug.Log then
            Debug.Log("HDG.TopologyCapture", string.format(
                "rotation deduction failed shape=%s err=%s", tostring(cap.shape), tostring(err)))
        end
        return nil, nil
    end

    -- 1. Update matched rooms (refresh blizzardGUID, fingerprint, capturedAt)
    for _, m in ipairs(match.matched) do
        local cap = snapshot.rooms[m.capturedID]
        if cap then
            local fp = FP.computeFingerprint(cap)
            local rotation, worldCardinals = _deduceRotation(cap)
            Store.UpsertRoom(m.existingID, {
                shape          = cap.shape,
                fingerprint    = fp,
                blizzardGUID   = cap.blizzardGUID,
                capturedAt     = now,
                rotation       = rotation,
                worldCardinals = worldCardinals,
            })
            stats.matched = stats.matched + 1
        end
    end

    -- 2. Mint fresh internal IDs for new rooms
    for _, n in ipairs(match.newRooms) do
        local cap = snapshot.rooms[n.capturedID]
        if cap then
            local roomID = Store.MintRoomID(floorID)
            local fp = FP.computeFingerprint(cap)
            local rotation, worldCardinals = _deduceRotation(cap)
            Store.UpsertRoom(roomID, {
                shape          = cap.shape,
                fingerprint    = fp,
                blizzardGUID   = cap.blizzardGUID,
                capturedAt     = now,
                rotation       = rotation,
                worldCardinals = worldCardinals,
            })
            stats.new = stats.new + 1
        end
    end

    -- 3. Demote orphans (delete the room; in P3, also move attached crates to orphans namespace)
    for _, o in ipairs(match.orphans) do
        Store.DeleteRoom(o.existingID)
        stats.orphaned = stats.orphaned + 1
    end

    -- 4. Ambiguous matches: log for now; manual remap UI lands in a later phase
    stats.ambiguous = #match.ambiguous

    -- 5. Update house's lastCapturedAt
    Store.UpsertHouse(houseID, { lastCapturedAt = now })

    -- 6. Fire event for UI refresh
    local bus = _EventBus()
    if bus and bus.Trigger then
        bus:Trigger("HDG_PROJECTS_TOPOLOGY_UPDATED", { houseID = houseID, floor = floor, stats = stats })
    end

    return stats
end

-- Exposed for T11 (event wiring) and the active-sweep iterator
function M.GetCurrentHouseID()
    return _currentHouseID()
end

-- Event wiring ----------------------------------------------------------------

local eventFrame = nil

local function _onModeChanged()
    local active = C_HouseEditor and C_HouseEditor.IsHouseEditorActive() or false
    local mode = (C_HouseEditor and C_HouseEditor.GetActiveHouseEditorMode()) or 0

    if active and mode == _layoutMode() then
        -- Entering Layout mode: start capture for current floor IF not already active
        -- (CaptureAllFloors initiates capture explicitly; this is for passive entry)
        if not M.IsCapturing() then
            local floor = (C_HousingLayout and C_HousingLayout.GetViewedFloor()) or 1
            M.BeginCapture(floor)
        end
    else
        -- Leaving Layout mode (or any other mode change): finalize
        if M.IsCapturing() then
            local snap = M.EndCapture()
            local houseID = _currentHouseID()
            if snap and houseID then
                M.Finalize(snap, houseID)
                -- During an active sweep, accumulate the room count from this final floor
                if activeSweep and snap then
                    local n = 0; for _ in pairs(snap.rooms) do n = n + 1 end
                    activeSweep.totalRooms = (activeSweep.totalRooms or 0) + n
                end
            end
        end
    end
end

local function _onFloorChanged()
    -- Only used during PASSIVE capture (player manually switching floors in Layout mode).
    -- During an active sweep, _stepSweep drives finalize+begin directly without
    -- depending on this event.
    if activeSweep then return end
    if not M.IsCapturing() then return end

    local snap = M.EndCapture()
    local houseID = _currentHouseID()
    if snap and houseID then M.Finalize(snap, houseID) end

    local floor = (C_HousingLayout and C_HousingLayout.GetViewedFloor()) or 1
    M.BeginCapture(floor)
end

function M.RegisterEvents()
    if eventFrame then return end
    eventFrame = CreateFrame("Frame")
    eventFrame:RegisterEvent("HOUSE_EDITOR_MODE_CHANGED")
    eventFrame:RegisterEvent("HOUSING_LAYOUT_PIN_FRAME_ADDED")
    eventFrame:RegisterEvent("HOUSING_LAYOUT_VIEWED_FLOOR_CHANGED")
    eventFrame:SetScript("OnEvent", function(_, event, arg1)
        if event == "HOUSE_EDITOR_MODE_CHANGED" then
            _onModeChanged()
        elseif event == "HOUSING_LAYOUT_PIN_FRAME_ADDED" then
            M.OnPinFrameAdded(arg1)
        elseif event == "HOUSING_LAYOUT_VIEWED_FLOOR_CHANGED" then
            _onFloorChanged()
        end
    end)
end

-- Active "Capture all floors" iterator ----------------------------------------

local function _stepSweep()
    if not activeSweep or activeSweep.cancelled then return end

    local nextFloor = activeSweep.floor + 1

    if nextFloor > activeSweep.maxFloor then
        -- Done: finalize the LAST floor's snapshot before leaving the editor
        if M.IsCapturing() then
            local snap = M.EndCapture()
            local houseID = _currentHouseID()
            if snap and houseID then M.Finalize(snap, houseID) end
            if snap then
                local n = 0; for _ in pairs(snap.rooms) do n = n + 1 end
                activeSweep.totalRooms = (activeSweep.totalRooms or 0) + n
            end
        end

        if C_HouseEditor and C_HouseEditor.LeaveHouseEditor then
            C_HouseEditor.LeaveHouseEditor()
        end
        if UIErrorsFrame and UIErrorsFrame.AddMessage then
            UIErrorsFrame:AddMessage(string.format(
                "Captured %d floor%s, %d rooms",
                activeSweep.maxFloor,
                activeSweep.maxFloor == 1 and "" or "s",
                activeSweep.totalRooms or 0), 0.0, 1.0, 0.0)
        end
        local bus = _EventBus()
        if bus and bus.Trigger then
            bus:Trigger("HDG_PROJECTS_CAPTURE_COMPLETE", {
                houseID = activeSweep.houseID,
                floors  = activeSweep.maxFloor,
                rooms   = activeSweep.totalRooms,
            })
        end
        activeSweep = nil
        return
    end

    -- Advance to next floor: finalize current, begin next
    activeSweep.floor = nextFloor

    -- Finalize current floor's capture before switching
    if M.IsCapturing() then
        local snap = M.EndCapture()
        local houseID = _currentHouseID()
        if snap and houseID then M.Finalize(snap, houseID) end
        if snap then
            local n = 0; for _ in pairs(snap.rooms) do n = n + 1 end
            activeSweep.totalRooms = (activeSweep.totalRooms or 0) + n
        end
    end

    -- Switch to the new floor; pin frames will be re-emitted for it
    if C_HousingLayout and C_HousingLayout.SetViewedFloor then
        C_HousingLayout.SetViewedFloor(nextFloor)
    end

    -- Begin capture for the new floor (don't rely on the floor-changed event)
    M.BeginCapture(nextFloor)

    -- Schedule next step
    if C_Timer and C_Timer.After then
        C_Timer.After(activeSweep.settleSeconds or 0.6, _stepSweep)
    end
end

function M.CaptureAllFloors()
    if activeSweep then return false, "already in progress" end
    if not C_HouseEditor or not C_HouseEditor.IsHouseEditorStatusAvailable() then
        return false, "house editor not available — visit your house first"
    end

    local houseID = _currentHouseID()
    if not houseID then return false, "could not determine faction" end

    -- Enter editor in Layout mode
    if C_HouseEditor.EnterHouseEditor then C_HouseEditor.EnterHouseEditor() end
    if C_HouseEditor.ActivateHouseEditorMode and Enum and Enum.HouseEditorMode then
        C_HouseEditor.ActivateHouseEditorMode(Enum.HouseEditorMode.Layout)
    end

    local maxFloor = (C_HousingLayout and C_HousingLayout.GetNumFloors()) or 1
    activeSweep = {
        houseID       = houseID,
        floor         = 1,             -- start at floor 1; we begin capture explicitly below
        maxFloor      = maxFloor,
        settleSeconds = 0.6,
        totalRooms    = 0,
        cancelled     = false,
    }

    -- Explicitly begin capture for floor 1 — don't rely on SetViewedFloor(1)
    -- firing HOUSING_LAYOUT_VIEWED_FLOOR_CHANGED, which won't happen if the
    -- player is already viewing floor 1.
    if C_Timer and C_Timer.After then
        C_Timer.After(0.5, function()
            if not activeSweep or activeSweep.cancelled then return end
            -- Ensure we're on floor 1 (no-op if already there, no event needed)
            if C_HousingLayout and C_HousingLayout.SetViewedFloor then
                C_HousingLayout.SetViewedFloor(1)
            end
            -- Begin capture for floor 1 if _onModeChanged didn't already do it
            if not M.IsCapturing() then M.BeginCapture(1) end
            -- Schedule first step (advances to floor 2 or wraps up)
            C_Timer.After(activeSweep.settleSeconds or 0.6, _stepSweep)
        end)
    end
    return true
end

function M.IsSweeping()
    return activeSweep ~= nil
end

function M.CancelSweep()
    if activeSweep then activeSweep.cancelled = true end
end

privateDB.HDG_TopologyCapture = M
