-- Portal Pro :: Core/Database.lua
-- All persistent writes go through here. Schema carries a version and forward migrations,
-- so an old SavedVariables file never gets read against a newer expectation.

local ADDON_NAME, PP = ...
local U = PP.U

local DB = {}
PP.DB = DB

local SCHEMA_VERSION = 2

--------------------------------------------------------------------------------
-- Defaults
--------------------------------------------------------------------------------

-- Conservative on purpose. Auto-whisper and auto-invite are opt-in: blasting strangers
-- gets you server-squelched and is a ToS risk. See PLAN.md §1.4.
local PROFILE_DEFAULTS = {
	detection = {
		enabled       = true,
		autoWhisper   = false,
		autoInvite    = false,
		channels      = {
			SAY     = true,
			YELL    = true,
			GENERAL = true,
			TRADE   = true,
			LFG     = true,
			GUILD   = false,
			PARTY   = false,
			WHISPER = true,
		},
		playerCooldown   = 300,  -- seconds before the same person can generate a new lead
		leadExpiry       = 240,  -- seconds a lead stays in the queue
		requireDest      = false, -- drop leads where no destination could be parsed
		ignoreCompetitors = true,
		sound            = true,
		flash            = false,
		toast            = true,
	},

	-- Copper. `default` is used when a destination has no explicit price.
	prices = {
		default      = 100000, -- 10g
		stormwind    = nil,
		ironforge    = nil,
		darnassus    = nil,
		orgrimmar    = nil,
		undercity    = nil,
		thunderbluff = nil,
	},

	-- Which casts count as business. A portal opened for your dungeon group is not a sale,
	-- and billing its rune against your margin makes the ledger lie.
	tracking = {
		ignoreInInstances = true,   -- dungeon/raid ports are almost never sales
		trackTeleports    = false,  -- Teleport moves only the caster, so it can never be a sale
		excludedDests     = {},     -- [destKey] = true, never tracked
	},

	-- Vendor prices for runes. Blizzard's API exposes sell price, not buy price, so these
	-- are configurable defaults rather than something we can read off the item.
	reagents = {
		portalCost   = 2000, -- 20s, Rune of Portals
		teleportCost = 1000, -- 10s, Rune of Teleportation
		lowWarning   = 5,
	},

	ads = {
		selected = 1,
		rotation = "off",        -- off | sequential | random
		throttle = 60,           -- seconds between broadcasts, per channel
		armEnabled = false,      -- opt-in: flush queued ad on next keypress
		list = {
			{
				name = "Standard",
				text = "{rt1} Portals to any Alliance city - {price} {rt1} Whisper me!",
				channels = { TRADE = true, GENERAL = false, LFG = false, SAY = false, YELL = false },
				-- greetingId = nil, meaning "use the default greeting below". An ad that
				-- advertises tips wants a different opening line from one that quotes a fixed
				-- price, and the reply should follow the ad that brought them in.
			},
		},
	},

	whispers = {
		greeting  = "Hi {name}! I can port you to {dest}. {price}, whisper me and I'll invite.",
		quote     = "{dest} is {price}. Ready when you are!",
		location  = "I'm at the {city} bank - come find me and I'll open it.",
		invited   = "Invite sent! Accept and I'll port you to {dest}.",
		portalUp  = "Portal to {dest} is up - hop in!",
		thanks    = "Thanks {name}, safe travels! Whisper me any time.",
		busy      = "Sorry {name}, tied up right now - try me in a few minutes.",

		-- Named greetings you can write once and point an ad at. The seven above are roles -
		-- there is exactly one "thanks" - whereas these are alternatives for the same role,
		-- which is why they're a list rather than more fixed keys.
		--
		-- Referenced by `id`, never by name or position: renaming one must not silently
		-- repoint an ad, and deleting one must not shift every ad below it onto the wrong text.
		library = {},
		nextId  = 1,
	},

	group = {
		announceToParty = true,
		autoUninvite    = false,
		uninviteDelay   = 20,
		autoThanks      = false,
	},

	ui = {
		font        = "auto",  -- registry key from Theme.fonts, or "auto" / "custom"
		fontPath    = "",      -- only consulted when font == "custom"
		scale       = 1.0,
		width       = 720,
		height      = 480,
		point       = { "CENTER", "UIParent", "CENTER", 0, 0 },
		compact     = false,
		compactPoint = { "TOP", "UIParent", "TOP", 0, -80 },
		toastPoint  = { "TOP", "UIParent", "TOP", 0, -200 },
		locked      = false,
		aboutMusic  = true,   -- play the About tab theme; the Stop button clears this
		minimap     = { hide = false, angle = 200 },
	},
}

local GLOBAL_DEFAULTS = {
	debug     = false,
	firstRun  = true,
	ledger    = {},   -- append-only transaction list
	customers = {},   -- [name] = { first, last, count, totalCopper, vip, blacklisted, note }
	sessions  = {},   -- { { start, stop, netCopper, casts, leads, conversions } }
	counters  = { leads = 0, whispers = 0, invites = 0, casts = 0, broadcasts = 0, personalCasts = 0 },
	-- Observed competitor asks and customer bids, harvested from chat the scanner already
	-- reads. Realm-tagged, since prices don't transfer between realms.
	market    = { observations = {} },
}

--------------------------------------------------------------------------------
-- Migrations
--------------------------------------------------------------------------------

-- Each entry upgrades from (index) to (index + 1). Add, never edit.
local migrations = {
	-- Ad rotation was forced on with no way to disable it, so every broadcast silently
	-- moved the selection and the next click sent an ad you hadn't chosen. Existing
	-- profiles move to "off"; rotation is now something you opt into.
	[1] = function(db)
		for _, profile in pairs(db.profiles or {}) do
			if profile.ads and profile.ads.rotation == "sequential" then
				profile.ads.rotation = "off"
			end
		end
	end,
}

local function migrate(db)
	local from = db.version or SCHEMA_VERSION
	while from < SCHEMA_VERSION do
		local step = migrations[from]
		if step then
			local ok, err = pcall(step, db)
			if not ok then
				PP:Error(("migration %d->%d failed: %s"):format(from, from + 1, tostring(err)))
				break
			end
		end
		from = from + 1
	end
	db.version = SCHEMA_VERSION
end

--------------------------------------------------------------------------------
-- Lifecycle
--------------------------------------------------------------------------------

local function profileKey()
	return (UnitName("player") or "Unknown") .. " - " .. (GetRealmName() or "Unknown")
end

function DB:Initialize()
	if type(_G.PortalProDB) ~= "table" then
		_G.PortalProDB = {}
	end
	local db = _G.PortalProDB

	db.version     = db.version or SCHEMA_VERSION
	db.profiles    = db.profiles or {}
	db.profileKeys = db.profileKeys or {}
	db.global      = db.global or {}

	migrate(db)

	U.CopyDefaults(db.global, GLOBAL_DEFAULTS)

	local key = profileKey()
	local profileName = db.profileKeys[key] or key
	db.profileKeys[key] = profileName

	if type(db.profiles[profileName]) ~= "table" then
		db.profiles[profileName] = {}
	end
	U.CopyDefaults(db.profiles[profileName], PROFILE_DEFAULTS)

	PP.db      = db
	PP.profile = db.profiles[profileName]
	PP.global  = db.global
	PP.profileName = profileName
end

--------------------------------------------------------------------------------
-- Profile management
--------------------------------------------------------------------------------

function DB:ListProfiles()
	local out = {}
	for name in pairs(PP.db.profiles) do out[#out + 1] = name end
	table.sort(out)
	return out
end

function DB:SwitchProfile(name)
	if not name or name == "" then return false end
	if type(PP.db.profiles[name]) ~= "table" then
		PP.db.profiles[name] = {}
	end
	U.CopyDefaults(PP.db.profiles[name], PROFILE_DEFAULTS)
	PP.db.profileKeys[profileKey()] = name
	PP.profile = PP.db.profiles[name]
	PP.profileName = name
	PP:Fire("PROFILE_CHANGED", name)
	return true
end

function DB:CopyProfile(fromName)
	local src = PP.db.profiles[fromName]
	if not src or fromName == PP.profileName then return false end
	PP.db.profiles[PP.profileName] = U.DeepCopy(src)
	PP.profile = PP.db.profiles[PP.profileName]
	PP:Fire("PROFILE_CHANGED", PP.profileName)
	return true
end

function DB:ResetProfile()
	PP.db.profiles[PP.profileName] = {}
	U.CopyDefaults(PP.db.profiles[PP.profileName], PROFILE_DEFAULTS)
	PP.profile = PP.db.profiles[PP.profileName]
	PP:Fire("PROFILE_CHANGED", PP.profileName)
end

function DB:GetDefaults()
	return PROFILE_DEFAULTS
end
