EbonholdHub.ExportImport = {}

EbonholdHub.ExportImport.WEBSITE_IMPORT_URL = "https://ebonholdhub.vercel.app/import"


local B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

local BASE64_CHUNK = 2000
local function Base64Encode(data)
	local out = {}
	local len = #data
	for i = 1, len, 3 do
		local a, b, c = data:byte(i, i + 2)
		a, b, c = a or 0, b or 0, c or 0
		local n = a * 65536 + b * 256 + c
		out[#out + 1] = B64:byte(math.floor(n / 262144) + 1)
		out[#out + 1] = B64:byte(math.floor((n % 262144) / 4096) + 1)
		out[#out + 1] = i + 1 <= len and B64:byte(math.floor((n % 4096) / 64) + 1) or 61
		out[#out + 1] = i + 2 <= len and B64:byte(math.floor(n % 64) + 1) or 61
	end
	local chunks = {}
	for i = 1, #out, BASE64_CHUNK do
		chunks[#chunks + 1] = string.char(unpack(out, i, math.min(i + BASE64_CHUNK - 1, #out)))
	end
	return table.concat(chunks)
end

local function Base64Decode(s)
	local rev = {}
	for i = 1, #B64 do rev[B64:byte(i)] = i - 1 end
	rev[61] = 0
	local out = {}
	local len = #s
	for i = 1, len, 4 do
		local a = rev[s:byte(i)] or 0
		local b = rev[s:byte(i + 1)] or 0
		local c = rev[s:byte(i + 2)] or 0
		local d = rev[s:byte(i + 3)] or 0
		local n = a * 262144 + b * 4096 + c * 64 + d
		out[#out + 1] = string.char(math.floor(n / 65536))
		if s:byte(i + 2) ~= 61 then
			out[#out + 1] = string.char(math.floor((n % 65536) / 256))
		end
		if s:byte(i + 3) ~= 61 then
			out[#out + 1] = string.char(math.floor(n % 256))
		end
	end
	return table.concat(out)
end


local function IsArray(tbl)
	if type(tbl) ~= "table" then return false end
	local count, maxIdx = 0, 0
	for k in pairs(tbl) do
		if type(k) ~= "number" or k < 1 then return false end
		count = count + 1
		if k > maxIdx then maxIdx = k end
	end
	return count == maxIdx
end

local function JSONEncode(value)
	local t = type(value)
	if t == "nil" then return "null"
	elseif t == "boolean" then return value and "true" or "false"
	elseif t == "number" then
		if value ~= value then return "null" end -- NaN
		if value == math.huge or value == -math.huge then return "null" end
		return tostring(value)
	elseif t == "string" then
		local escaped = value:gsub("\\", "\\\\"):gsub('"', '\\"'):gsub("\n", "\\n"):gsub("\r", "\\r"):gsub("\t", "\\t")
		return '"' .. escaped .. '"'
	elseif t == "table" then
		local parts = {}
		if IsArray(value) then
			for i = 1, #value do parts[#parts + 1] = JSONEncode(value[i]) end
			return "[" .. table.concat(parts, ",") .. "]"
		else
			for k, v in pairs(value) do
				if v ~= nil then
					parts[#parts + 1] = JSONEncode(tostring(k)) .. ":" .. JSONEncode(v)
				end
			end
			return "{" .. table.concat(parts, ",") .. "}"
		end
	end
	return "null"
end

EbonholdHub.ExportImport.JSONEncode = JSONEncode


local function SkipWhitespace(s, pos)
	while pos <= #s do
		local c = s:byte(pos)
		if c ~= 32 and c ~= 9 and c ~= 10 and c ~= 13 then break end
		pos = pos + 1
	end
	return pos
end

local function ParseValue(s, pos)
	pos = SkipWhitespace(s, pos)
	if pos > #s then return nil, pos end
	local c = s:byte(pos)
	if c == 110 then -- null
		return nil, pos + 4
	elseif c == 116 then -- true
		return true, pos + 4
	elseif c == 102 then -- false
		return false, pos + 5
	elseif c == 34 then -- string
		local out = {}
		pos = pos + 1
		while pos <= #s do
			local cc = s:byte(pos)
			if cc == 34 then
				return table.concat(out), pos + 1
			elseif cc == 92 then -- backslash
				pos = pos + 1
				local ec = s:byte(pos)
				if ec == 110 then out[#out + 1] = "\n"
				elseif ec == 114 then out[#out + 1] = "\r"
				elseif ec == 116 then out[#out + 1] = "\t"
				elseif ec == 92 then out[#out + 1] = "\\"
				elseif ec == 34 then out[#out + 1] = '"'
				else out[#out + 1] = s:sub(pos, pos) end
			else
				out[#out + 1] = s:sub(pos, pos)
			end
			pos = pos + 1
		end
		return table.concat(out), pos
	elseif c == 91 then -- array
		local arr = {}
		pos = pos + 1
		pos = SkipWhitespace(s, pos)
		if s:byte(pos) == 93 then return arr, pos + 1 end
		while true do
			if pos > #s then return arr, pos end
			local val
			val, pos = ParseValue(s, pos)
			arr[#arr + 1] = val
			pos = SkipWhitespace(s, pos)
			if s:byte(pos) == 93 then return arr, pos + 1 end
			pos = pos + 1 -- skip comma
		end
	elseif c == 123 then -- object
		local obj = {}
		pos = pos + 1
		pos = SkipWhitespace(s, pos)
		if s:byte(pos) == 125 then return obj, pos + 1 end
		while true do
			if pos > #s then return obj, pos end
			local key
			key, pos = ParseValue(s, pos)
			pos = SkipWhitespace(s, pos)
			pos = pos + 1 -- skip colon
			local val
			val, pos = ParseValue(s, pos)
			if key ~= nil then obj[key] = val end
			pos = SkipWhitespace(s, pos)
			if s:byte(pos) == 125 then return obj, pos + 1 end
			pos = pos + 1 -- skip comma
		end
	else -- number
		local startPos = pos
		if c == 45 then pos = pos + 1 end -- negative sign
		while pos <= #s do
			local nc = s:byte(pos)
			if nc >= 48 and nc <= 57 or nc == 46 or nc == 101 or nc == 69 or nc == 43 then
				pos = pos + 1
			else
				break
			end
		end
		return tonumber(s:sub(startPos, pos - 1)), pos
	end
end

EbonholdHub.ExportImport.JSONDecode = function(s)
	if not s or s == "" then return nil end
	local val = ParseValue(s, 1)
	return val
end


local EXPORT_VERSION = 1

local function BuildExportData(build)
	if EbonholdHub.EquipmentLoadout then
		EbonholdHub.EquipmentLoadout.MigrateBuild(build)
	end

	return {
		v = EXPORT_VERSION,
		title = build.title,
		class = build.class,
		spec = build.spec,
		comments = build.comments,
		lockedEchoes = EbonholdHub.Build.NormalizeLockedEchoes(build.lockedEchoes),
		echoTiers        = build.echoTiers     or {},
		echoTierOrder    = build.echoTierOrder or {},
		echoMaxPicks     = build.echoMaxPicks  or {},
		echoBundles      = build.echoBundles   or {},
		loadoutEchoSpellIds = build.loadoutEchoSpellIds,
		affixLoadout = EbonholdHub.AffixLoadout and EbonholdHub.AffixLoadout.Clone(build.affixLoadout) or build.affixLoadout,
		equipmentLoadout = EbonholdHub.EquipmentLoadout and EbonholdHub.EquipmentLoadout.Clone(build.equipmentLoadout) or build.equipmentLoadout,
		equipmentSets = EbonholdHub.EquipmentLoadout and EbonholdHub.EquipmentLoadout.CloneSets(build.equipmentSets) or build.equipmentSets,
		equipmentSetIndex = build.equipmentSetIndex,
		talents = EbonholdHub.Talents and EbonholdHub.Talents.ClonePlan(build.talents) or build.talents,
		talentOrder = EbonholdHub.Talents and EbonholdHub.Talents.CloneOrder(build.talentOrder) or build.talentOrder,
		settings = build.settings,
		automationEnabled = build.automationEnabled,
		isPublic = build.isPublic or false,
		validated = build.validated or false,
		author = build.author,
		lastModified = build.lastModified,
		copiedFrom = build.copiedFrom or nil,
	}
end

function EbonholdHub.ExportImport.ExportBuild(build)
	if not build then return nil end
	local data = BuildExportData(build)
	local json = EbonholdHub.ExportImport.JSONEncode(data)
	return Base64Encode(json)
end

function EbonholdHub.ExportImport.DecodeBuild(b64String)
	if not b64String or b64String == "" then return nil end
	local json = Base64Decode(b64String)
	if not json or json == "" then return nil end
	local data = EbonholdHub.ExportImport.JSONDecode(json)
	if not data or type(data) ~= "table" then return nil end

	local locked = EbonholdHub.Build.NormalizeLockedEchoes(data.lockedEchoes)

	local build = EbonholdHub.Build.NewObject({
		title       = data.title    or "Imported Build",
		class       = data.class    or EbonholdHub.Build.PlayerClassToken(),
		spec        = data.spec     or 1,
		comments    = data.comments or "",
		lockedEchoes = locked,
		echoTiers        = data.echoTiers     or {},
		echoTierOrder    = data.echoTierOrder or {},
		echoMaxPicks     = data.echoMaxPicks  or {},
		echoBundles      = data.echoBundles   or {},
		loadoutEchoSpellIds = data.loadoutEchoSpellIds,
		affixLoadout = data.affixLoadout,
		equipmentLoadout = data.equipmentLoadout,
		equipmentSets = data.equipmentSets,
		equipmentSetIndex = data.equipmentSetIndex,
		talents = data.talents,
		talentOrder = data.talentOrder,
		settings    = data.settings or EbonholdHub.Build.DefaultSettings(),
		automationEnabled = data.automationEnabled,
		isPublic    = data.isPublic or false,
		validated   = data.validated or false,
		author      = data.author,
		lastModified = data.lastModified,
		copiedFrom  = data.copiedFrom or nil,
	})
	-- Migrate legacy echoWeights from old export strings
	if not next(build.echoTiers) and data.echoWeights and next(data.echoWeights) then
		build.echoWeights = data.echoWeights
		if EbonholdHub.TierData then EbonholdHub.TierData.MigrateWeights(build) end
	end
	EbonholdHub.Build.EnsureSettings(build)
	return build
end

function EbonholdHub.ExportImport.DecodeAnyExport(text)
	text = strtrim(text or "")
	if text == "" then return nil end

	if EbonholdHub.EchoLoadoutCodec and EbonholdHub.EchoLoadoutCodec.IsLoadoutString(text) then
		local build = EbonholdHub.EchoLoadoutCodec.ImportBuild(text)
		return build
	end

	return EbonholdHub.ExportImport.DecodeBuild(text)
end

function EbonholdHub.ExportImport.ImportBuild(text)
	text = strtrim(text or "")

	if EbonholdHub.Catalog and EbonholdHub.Catalog.AddFromExport then
		local id = EbonholdHub.Catalog.AddFromExport(text, {
			source = EbonholdHub.EchoLoadoutCodec and EbonholdHub.EchoLoadoutCodec.IsLoadoutString(text) and "loadout" or "import",
		})
		if not id then return nil end
		return EbonholdHub.Catalog.Get(id)
	end

	return EbonholdHub.ExportImport.DecodeAnyExport(text)
end


local exportDialog
local importLinkDialog

local function EnsureImportLinkDialog()
	if importLinkDialog then return end
	local UI = EbonholdHub.UI
	local C = UI.Colors

	local f = UI.CreateWindow("EbonholdHubImportWebsiteLinkDialog", 520, 130, "Import on website", {
		strata = "FULLSCREEN_DIALOG",
		toplevel = true,
	})
	local content = f.content

	local hint = UI.CreateLabel(content, "Copy the export code, open this page in your browser, and paste it there:", UI.FONT_SIZE_SMALL, C.textDim)
	hint:SetPoint("TOPLEFT", content, "TOPLEFT", 0, 0)
	f._hint = hint

	local editHolder = UI.CreateEditBox(content, 480, 28, false)
	editHolder:SetPoint("TOPLEFT", hint, "BOTTOMLEFT", 0, -8)
	local box = editHolder.editBox
	box:SetMaxLetters(256)
	box:SetScript("OnEscapePressed", function() f:Hide() end)

	local close = UI.CreateButton(content, 80, 26, "Close")
	close:SetPoint("BOTTOMRIGHT", content, "BOTTOMRIGHT", 0, 0)
	close:SetScript("OnClick", function() f:Hide() end)

	f._editBox = box
	importLinkDialog = f
end

function EbonholdHub.ExportImport.ShowImportWebsiteLink(openedInBrowser)
	EnsureImportLinkDialog()
	local url = EbonholdHub.ExportImport.WEBSITE_IMPORT_URL or ""
	if openedInBrowser == nil and EbonholdHub.OpenExternalURL then
		openedInBrowser = EbonholdHub.OpenExternalURL(url)
	end
	if importLinkDialog._hint then
		if openedInBrowser then
			importLinkDialog._hint:SetText("Opened in your browser. Copy the export code above, paste it on the import page:")
		else
			importLinkDialog._hint:SetText("Copy the export code, open this page in your browser, and paste it there:")
		end
	end
	importLinkDialog._editBox:SetText(url)
	importLinkDialog._editBox:HighlightText()

	local anchor = (exportDialog and exportDialog:IsShown()) and exportDialog
		or (EbonholdHub.MainWindow and EbonholdHub.MainWindow._frame)
	if anchor and anchor:IsShown() then
		importLinkDialog:SetFrameStrata("FULLSCREEN_DIALOG")
		importLinkDialog:SetFrameLevel(anchor:GetFrameLevel() + 20)
		importLinkDialog:ClearAllPoints()
		importLinkDialog:SetPoint("CENTER", anchor, "CENTER", 0, 0)
	else
		importLinkDialog:ClearAllPoints()
		importLinkDialog:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
	end

	importLinkDialog:Show()
	importLinkDialog._editBox:SetFocus()
end

local function CreateExportDialog()
	local UI = EbonholdHub.UI
	local C = UI.Colors

	local f = UI.CreateWindow("EbonholdHubExportBuildDialog", 700, 420, "Export Build", {
		strata = "FULLSCREEN_DIALOG",
		toplevel = true,
	})
	local content = f.content

	local editor = UI.CreateScrollEditBox(content)
	editor:SetPoint("TOPLEFT", content, "TOPLEFT", 0, 0)
	editor:SetPoint("BOTTOMRIGHT", content, "BOTTOMRIGHT", 0, 36)
	local box = editor.editBox
	box:SetScript("OnEscapePressed", function() f:Hide() end)

	local website = UI.CreateButton(content, 140, 26, "Export to website", { hoverBorder = C.accent })
	website:SetPoint("BOTTOMLEFT", content, "BOTTOMLEFT", 0, 0)
	website:SetScript("OnClick", function()
		local url = EbonholdHub.ExportImport.WEBSITE_IMPORT_URL or ""
		local opened = EbonholdHub.OpenExternalURL and EbonholdHub.OpenExternalURL(url)
		EbonholdHub.ExportImport.ShowImportWebsiteLink(opened)
	end)

	local close = UI.CreateButton(content, 80, 26, "Close")
	close:SetPoint("BOTTOMRIGHT", content, "BOTTOMRIGHT", 0, 0)
	close:SetScript("OnClick", function() f:Hide() end)

	f._editBox = box
	f._editor = editor
	exportDialog = f
end

function EbonholdHub.ExportImport.ShowExportDialog(build)
	if not exportDialog then CreateExportDialog() end
	local b64 = EbonholdHub.ExportImport.ExportBuild(build)
	if not b64 then return end
	exportDialog._editBox:SetText(b64)
	if exportDialog._editor and exportDialog._editor.UpdateLayout then
		exportDialog._editor.UpdateLayout()
	end
	exportDialog._editBox:HighlightText()
	exportDialog:Show()
end


local importDialog

local function CreateImportDialog()
	local UI = EbonholdHub.UI
	local C  = UI.Colors

	local f = UI.CreateWindow("EbonholdHubImportBuildDialog", 700, 420, "Import Build", {
		strata = "FULLSCREEN_DIALOG",
		toplevel = true,
	})
	local content = f.content

	local editor = UI.CreateScrollEditBox(content)
	editor:SetPoint("TOPLEFT", content, "TOPLEFT", 0, 0)
	editor:SetPoint("BOTTOMRIGHT", content, "BOTTOMRIGHT", 0, 36)
	local box = editor.editBox
	box:SetScript("OnEscapePressed", function() f:Hide() end)

	local hint = editor:CreateFontString(nil, "OVERLAY")
	UI.SetFont(hint, UI.FONT_SIZE_SMALL, C.textDim)
	hint:SetPoint("TOPLEFT",  box, "TOPLEFT",  2, -2)
	hint:SetPoint("TOPRIGHT", box, "TOPRIGHT", -2, -2)
	hint:SetJustifyH("LEFT")
	hint:SetJustifyV("TOP")
	hint:SetText("Paste a website export code or an Echo Journal loadout (EBH1:...), then click Import.")

	box:SetScript("OnEditFocusGained", function() hint:Hide() end)
	box:SetScript("OnEditFocusLost", function()
		if (box:GetText() or "") == "" then hint:Show() end
	end)
	box:SetScript("OnTextChanged", function()
		if box:HasFocus() then hint:Hide()
		elseif (box:GetText() or "") == "" then hint:Show()
		else hint:Hide() end
	end)

	local import = UI.CreateButton(content, 80, 26, "Import")
	import:SetPoint("BOTTOMRIGHT", content, "BOTTOMRIGHT", 0, 0)
	import:SetScript("OnClick", function()
		local text = box:GetText() or ""
		local build = EbonholdHub.ExportImport.ImportBuild(text)
		if build then
			f:Hide()
			EbonholdHub.ViewRouter.Show("publicBuilds")
		else
			f._error:Show()
		end
	end)

	local cancel = UI.CreateButton(content, 80, 26, "Cancel")
	cancel:SetPoint("RIGHT", import, "LEFT", -8, 0)
	cancel:SetScript("OnClick", function() f:Hide() end)

	local error = content:CreateFontString(nil, "OVERLAY")
	UI.SetFont(error, UI.FONT_SIZE_SMALL, C.danger)
	error:SetPoint("BOTTOM", import, "TOP", 0, 4)
	error:SetText("Invalid import string. Please check and try again.")
	error:Hide()
	f._error = error

	f._editBox = box
	f._editor = editor
	importDialog = f
end

function EbonholdHub.ExportImport.ShowImportDialog()
	if not importDialog then CreateImportDialog() end
	importDialog._editBox:SetText("")
	if importDialog._editor and importDialog._editor.UpdateLayout then
		importDialog._editor.UpdateLayout()
	end
	importDialog._error:Hide()
	importDialog:Show()
	importDialog._editBox:SetFocus()
end
