--[[
    Collection/SightingTracker.lua

    Marks a pet as SEEN the moment you target it in the world.

    This is the half of the journal that was missing. A collection has
    "collected"; a field journal has "seen" as well, and the gap between the two
    is what makes it worth filling. You can walk past Loque'nahak at level 20,
    fail to tame it, and the journal should still remember that you found it.

    ---------------------------------------------------------------------------
    How a target becomes a creature id
    ---------------------------------------------------------------------------

    A unit GUID looks like:

        Creature-0-<server>-<instance>-<zoneUID>-<creatureID>-<spawnUID>

    so the sixth field is the NPC id. Anything whose first field is not
    "Creature" is ignored: players, pets and objects have their own GUID
    shapes.

    ---------------------------------------------------------------------------
    The second job: enumeration
    ---------------------------------------------------------------------------

    When you target a tameable beast the database does NOT know, it is recorded
    in a sighting log. That log is first-party data, gathered from your own
    client by playing, and it is exactly the candidate list phase 0.9 needs —
    with no external source and no licence question attached to it.

    A logged sighting is not collection data. It never marks anything as
    collected and never enters the shipped database without being verified
    first.

    Cost: one GUID split and one hash lookup per target change. No polling.
]]

local _, ns = ...

local Tracker = ns.NewModule("SightingTracker")
ns.Collection = ns.Collection or {}
ns.Collection.SightingTracker = Tracker

-- Beyond this the log has long since served its purpose, and saved variables
-- should not grow without bound on someone else's account.
local MAX_SIGHTINGS = 1000

-- Returns the localized pet family of a unit, or nil when it has none. Not
-- observed in use by any addon on this client, so it is capability-checked:
-- without it the seen-marking still works and only the enumeration log stops.
local UnitCreatureFamily = UnitCreatureFamily
local IsSecretValue = issecretvalue

local function IsSecret(value)
    return IsSecretValue and IsSecretValue(value)
end

--=============================================================================
-- Internals
--=============================================================================

--- @param guid string|nil
-- @return number|nil The NPC id, for creatures only.
local function CreatureIDFromGUID(guid)
    -- Midnight can deliberately hide nameplate GUIDs. A protected string must
    -- never reach strsplit, a table key or a SavedVariable; simply wait until
    -- the client exposes this creature through a target or a later update.
    if not guid or IsSecret(guid) then
        return nil
    end

    local unitType, _, _, _, _, creatureID = strsplit("-", guid)
    if unitType ~= "Creature" then
        return nil
    end

    return tonumber(creatureID)
end

--=============================================================================
-- Sighting log
--=============================================================================

-- Creatures logged for the first time since login. Session scoped on purpose:
-- it is what the nameplate marker uses to say "this one is new to you", and
-- that should mean new *now*, not new at some point months ago.
local newThisSession = {}

--- Records a tameable creature the database does not know about.
local function LogUnknownCreature(creatureID, unit, familyName, classification)
    if not familyName or IsSecret(familyName) or IsSecret(classification) then
        return
    end

    local name = UnitName(unit)
    if IsSecret(name) then return end

    local account = ns.Database:GetAccountData()
    if not account then
        return
    end

    account.sightings = account.sightings or {}

    if account.sightings[creatureID] then
        return
    end

    if ns.Util.CountKeys(account.sightings) >= MAX_SIGHTINGS then
        ns.Debug:Warn("SIGHT", "sighting log is full at %d entries", MAX_SIGHTINGS)
        return
    end

    account.sightings[creatureID] = {
        name = name,
        family = familyName,
        classification = classification,
        seenAt = time(),
    }

    newThisSession[creatureID] = true

    ns.Debug:Log("SIGHT", "logged unknown creature %d (%s, %s, %s)",
        creatureID, tostring(name), tostring(familyName),
        tostring(classification))

    ns.Events:Send("SIGHTING_ADDED", creatureID)
end

--- Announces encounters that are worth stopping for. This is independent of
-- whether the creature was already logged: seeing Aotona again is still an
-- encounter. The UI owns deduplication and sound so tracked hunts cannot ring
-- twice through two separate systems.
local function AnnounceNotableUnit(unit, creatureID, appearance, familyName,
    classification, rareRecord)
    local isRare = rareRecord ~= nil or classification == "rare"
        or classification == "rareelite"
    local family = ns.Data.API:GetFamilyByLocalizedName(familyName)
    local isExotic = family ~= nil and family.exotic == true

    if not isRare and not isExotic then
        return
    end

    -- Sightings continue to be recorded regardless of this preference; it
    -- controls only the interruptive card and sound. "catalog" is deliberately
    -- strict: the exact NPC must belong to the independent 604-entry ledger.
    if ns.Config:Get("alertScope") ~= "all" and not rareRecord then
        return
    end

    ns.Events:Send("BEAST_DETECTED", unit, creatureID, appearance, familyName,
        rareRecord and "rare" or classification, UnitGUID(unit))
end

--- @return boolean Whether a sighting is a candidate for the collection.
-- Rare sightings sort first in the report; ordinary family members remain in
-- the queue because the complete appearance catalogue also needs their skins.
local function IsNotable(sighting)
    return sighting.classification == "rare" or sighting.classification == "rareelite"
end

--=============================================================================
-- Public API
--=============================================================================

--- Inspects a unit and records what is worth recording.
-- Called for the target and for every nameplate that appears, so the journal
-- fills in by walking around rather than by clicking on things.
-- @param unit string A unit token, e.g. "target" or "nameplate3".
-- @return number|nil The creature id, when the unit was a creature at all.
function Tracker:InspectUnit(unit)
    local creatureID = CreatureIDFromGUID(UnitGUID(unit))
    if not creatureID then
        return nil
    end

    local appearance = ns.Data.API:GetAppearanceOfCreature(creatureID)
    local rareRecord = ns.Data.RareCatalog:Get(creatureID)
    local familyName = UnitCreatureFamily and UnitCreatureFamily(unit) or nil
    local classification = UnitClassification and UnitClassification(unit) or nil

    if IsSecret(familyName) then familyName = nil end
    if IsSecret(classification) then classification = nil end

    -- A pet-family name is the client's strongest cheap runtime filter. Known
    -- records are already verified; unknown rare family members are presented
    -- as encounters and retained for later data review.
    if familyName or rareRecord then
        AnnounceNotableUnit(unit, creatureID, appearance, familyName,
            classification, rareRecord)
    end

    if rareRecord then
        ns.Collection.RareState:MarkSeen(creatureID)
    end

    if appearance then
        ns.Collection.State:MarkSeen(appearance.id)
    elseif not rareRecord then
        LogUnknownCreature(creatureID, unit, familyName, classification)
    end

    return creatureID
end

--- @param creatureID number
-- @return boolean Whether this creature was first logged during this session.
function Tracker:IsNewThisSession(creatureID)
    return newThisSession[creatureID] == true
end

--- @return table The sighting log, keyed by creature id. Possibly empty.
function Tracker:GetSightings()
    local account = ns.Database:GetAccountData()
    return account and account.sightings or {}
end

--- Prints the creatures seen that the database does not yet contain.
function Tracker:PrintReport()
    local L = ns.L
    local sightings = self:GetSightings()
    local total = ns.Util.CountKeys(sightings)

    ns.Print(L["SIGHTINGS_TITLE"])

    if total == 0 then
        ns.Print(L["SIGHTINGS_EMPTY"])
        return
    end

    -- Notable first, then by id, so the list is stable between runs and the
    -- entries worth acting on are at the top.
    local ordered = {}
    local notable = 0

    for creatureID, sighting in pairs(sightings) do
        ordered[#ordered + 1] = { id = creatureID, sighting = sighting }
        if IsNotable(sighting) then
            notable = notable + 1
        end
    end

    table.sort(ordered, function(a, b)
        local aNotable, bNotable = IsNotable(a.sighting), IsNotable(b.sighting)
        if aNotable ~= bNotable then
            return aNotable
        end
        return a.id < b.id
    end)

    ns.Print(L["SIGHTINGS_SUMMARY"]:format(total, notable))

    for _, entry in ipairs(ordered) do
        local mark = IsNotable(entry.sighting)
            and ("|cffff8800[%s]|r "):format(L["SIGHTINGS_RARE_MARK"])
            or ""

        ns.Print(("%s|cffffd100%d|r  %s  (%s)"):format(
            mark, entry.id, tostring(entry.sighting.name),
            tostring(entry.sighting.family)))
    end

    ns.Print(L["SIGHTINGS_HINT"])
end

--=============================================================================
-- Lifecycle
--=============================================================================

function Tracker:OnEnable()
    ns.Events:RegisterEvent("PLAYER_TARGET_CHANGED", self, function()
        Tracker:InspectUnit("target")
    end)

    ns.Debug:Log("SIGHT", "watching targets (family lookup: %s)",
        UnitCreatureFamily and "available" or "unavailable")
end
