-- Core/Evidence.lua
-- Per-frame evidence: which addons CREATED a frame and which addons ALTER it, with
-- ALL callers aggregated per behavior category (not just the first). Keyed by a
-- stable Fingerprint key so evidence accumulates across reloads via SavedVariables.

local ADDON, ns = ...

local E = {}
ns.Evidence = E

local live = {}   -- [key] = record (this session, holds richer transient data)
E.live = live

local function now() return GetTime() end
local function clock() return date("%H:%M:%S") end

-- Plain, serialisable snapshot of an identity (drops anything non-persistable).
local function plainIdentity(id)
	if not id then return nil end
	return {
		name = id.name, type = id.type, ancestor = id.ancestor,
		width = id.width, height = id.height,
		numChildren = id.numChildren, numRegions = id.numRegions,
	}
end

---Get (or create + restore-from-persistence) the live record for a frame.
function E.For(frame)
	local key = ns.Fingerprint.Key(frame)
	local rec = live[key]
	if rec then return rec end

	rec = {
		key          = key,
		name         = ns.ObjectName(frame) or tostring(frame),
		type         = ns.ObjectType(frame),
		identity     = plainIdentity(ns.Fingerprint.Identify(frame)),
		createdBy    = nil,                 -- {type,folder,title,file,line}
		categories   = {},                  -- [cat][folder] = {title,count,methods,lastArgs,lastTime,sampleStack}
		contributors = {},                  -- [folder] = title
		events       = 0,
		firstSeen    = clock(),
		lastSeen     = clock(),
		firstSeenAt  = time(),              -- epoch; firstSeen/lastSeen are display-only HH:MM:SS
		lastSeenAt   = time(),              -- epoch; drives age-based auto-clear at login
	}

	-- Merge any persisted history for this key so counts keep accumulating.
	local saved = ns.db and ns.db.investigations and ns.db.investigations[key]
	if saved then
		rec.createdBy    = saved.createdBy or rec.createdBy
		rec.contributors = saved.contributors or {}
		rec.categories   = saved.categories or {}
		rec.events       = saved.events or 0
		rec.firstSeen    = saved.firstSeen or rec.firstSeen
		rec.firstSeenAt  = saved.firstSeenAt or rec.firstSeenAt
	end

	-- Creator via the engine (deep mode), else a name-prefix guess.
	rec.createdBy = rec.createdBy or ns.SourceOf(frame)
	if not rec.createdBy then
		local guess = ns.GuessFromName(rec.identity and rec.identity.name)
		if guess then rec.createdBy = { type = "guess", title = guess } end
	end

	live[key] = rec
	return rec
end

---Re-query the creator (e.g. after deep mode is enabled).
function E.RefreshCreator(frame)
	local rec = E.For(frame)
	local src = ns.SourceOf(frame)
	if src then rec.createdBy = src end
	return rec.createdBy
end

---Record one alteration. folder/title identify the responsible addon (external
---preferred); chain/stack are kept for the most recent sample.
function E.AddAlter(frame, cat, method, args, folder, title, chain, stack)
	local rec = E.For(frame)
	folder = folder or "?"
	title  = title or folder

	local byCat = rec.categories[cat]
	if not byCat then byCat = {}; rec.categories[cat] = byCat end
	local entry = byCat[folder]
	if not entry then
		entry = { title = title, count = 0, methods = {} }
		byCat[folder] = entry
	end
	entry.count    = entry.count + 1
	entry.methods[method] = (entry.methods[method] or 0) + 1
	entry.lastArgs = args
	entry.lastTime = clock()
	entry.sampleStack = stack or entry.sampleStack

	if folder ~= "?" then rec.contributors[folder] = title end
	rec.events     = rec.events + 1
	rec.lastSeen   = clock()
	rec.lastSeenAt = time()
	return rec
end

---Sorted list of contributor titles for display.
function E.ContributorList(rec)
	local out = {}
	for _, title in pairs(rec.contributors) do out[#out + 1] = title end
	table.sort(out)
	return out
end

-- ---------------------------------------------------------------------------
-- Persistence
-- ---------------------------------------------------------------------------

---True when a record holds actual findings worth saving: at least one attributed
---alteration, or a creator pinned to a real addon. The picker pre-creates a record for
---every targeted frame (E.For in Monitor.SetTargets) before anything is observed, so a
---frame nothing touches still has a live record at SaveAll time — without this gate those
---empty records would pollute the saved history. A name-prefix guess or a Blizzard/unknown
---creator does not count: a guess attributes nothing concrete, and Blizzard authorship is
---not an addon tweak. Works on a live record or a persisted entry (same shape).
function E.HasFindings(rec)
	if not rec then return false end
	if (rec.events or 0) > 0 then return true end
	local c = rec.createdBy
	return c ~= nil and c.type == "addon"
end

function E.Persist(rec)
	if not (ns.db and rec) then return end
	if not E.HasFindings(rec) then return end  -- only log investigations with findings
	ns.db.investigations = ns.db.investigations or {}
	ns.db.investigations[rec.key] = {
		name = rec.name, type = rec.type, identity = rec.identity,
		createdBy = rec.createdBy, categories = rec.categories,
		contributors = rec.contributors, events = rec.events,
		firstSeen = rec.firstSeen, lastSeen = rec.lastSeen,
		firstSeenAt = rec.firstSeenAt, lastSeenAt = rec.lastSeenAt,
	}
end

function E.SaveAll()
	for _, rec in pairs(live) do E.Persist(rec) end
end

function E.LoadPersisted()
	-- Persisted records live in ns.db.investigations; merged lazily in E.For. At login
	-- we (1) auto-forget stale records past the retention window, then (2) bound
	-- pathological growth as a backstop.
	if not (ns.db and ns.db.investigations) then return end

	-- (1) Age-based auto-clear, plus drop any findingless entry persisted before the
	-- "only log investigations with findings" rule existed. Records carry lastSeenAt
	-- (epoch); legacy records without one are stamped now so they age from this upgrade
	-- rather than vanishing immediately.
	local days = ns.db.autoClearDays or 0
	local nowAt = time()
	for k, v in pairs(ns.db.investigations) do
		if not v.lastSeenAt then v.lastSeenAt = nowAt end
		if not E.HasFindings(v) then
			ns.db.investigations[k] = nil
		elseif days > 0 and (nowAt - v.lastSeenAt) > (days * 86400) then
			ns.db.investigations[k] = nil
		end
	end

	-- (2) Growth backstop: keep it sane: oldest-unknown order is fine to trim hard.
	local n = 0
	for _ in pairs(ns.db.investigations) do n = n + 1 end
	if n > 200 then
		local keep, count = {}, 0
		for k, v in pairs(ns.db.investigations) do
			count = count + 1
			if count <= 150 then keep[k] = v end
		end
		ns.db.investigations = keep
	end
end

---Forget a frame's accumulated evidence (live + persisted).
function E.Forget(key)
	live[key] = nil
	if ns.db and ns.db.investigations then ns.db.investigations[key] = nil end
end

---Forget every persisted investigation. Live records for frames being monitored this
---session are kept so an in-progress investigation isn't wiped mid-run.
function E.ClearPersisted()
	if ns.db then ns.db.investigations = {} end
end

---List persisted investigations for the UI browser. Each entry carries the full stored
---record (categories/contributors/identity/timestamps) so the browser can render the same
---per-category addon breakdown the live Evidence panel shows. Sorted by event count desc.
function E.ListPersisted()
	local out = {}
	if ns.db and ns.db.investigations then
		for key, v in pairs(ns.db.investigations) do
			out[#out + 1] = {
				key = key, name = v.name, type = v.type, events = v.events or 0,
				createdBy = v.createdBy, categories = v.categories, contributors = v.contributors,
				firstSeen = v.firstSeen, lastSeen = v.lastSeen, identity = v.identity,
			}
		end
	end
	table.sort(out, function(a, b) return (a.events or 0) > (b.events or 0) end)
	return out
end
