--[[ Offline license keys and the 30-image trial counter.

Key format: CC-XXXXX-XXXXX-XXXXX in Crockford base32 (no I/L/O/U). The
first 5 data characters are a random key id; the remaining 10 encode a
50-bit signature recomputed here from the id and an embedded secret, so
validation needs no network. `tools/keygen.py` mirrors this arithmetic
exactly — change either side and keys stop verifying.

All arithmetic stays below 2^53 so Lua doubles compute it exactly:
the rolling hash keeps its accumulator under 2^31 (times mul 37 plus a
byte is < 2^37), and the packed signature is < 2^50.

An offline scheme with an embedded secret is only tamper-resistant, not
tamper-proof; that is the standard trade-off for Lightroom plugins, which
cannot depend on network access at crop time.

The trial counter lives in the plugin prefs and is mirrored to a dot-file
in the user's home directory, so wiping one store alone doesn't reset it;
the larger of the two counts wins.
]]

local LrBinding = import 'LrBinding'
local LrDialogs = import 'LrDialogs'
local LrFileUtils = import 'LrFileUtils'
local LrHttp = import 'LrHttp'
local LrPathUtils = import 'LrPathUtils'
local LrPrefs = import 'LrPrefs'
local LrView = import 'LrView'

local prefs = LrPrefs.prefsForPlugin()

local License = {}

License.TRIAL_LIMIT = 30
License.BUY_URL = 'https://cutcorners.app/download#buy'

local ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
local SECRET = 'cut-corners-v1/8c1f4b7e29a6d053'
local BASE25 = 33554432 -- 2^25; each hash contributes 25 bits of signature

local function hash(s, seed, mul)
	local acc = seed
	for i = 1, #s do
		acc = (acc * mul + s:byte(i)) % 2147483647
	end
	return acc
end

local function encode32(n, count)
	local out = {}
	for i = count, 1, -1 do
		local d = n % 32
		out[i] = ALPHABET:sub(d + 1, d + 1)
		n = math.floor(n / 32)
	end
	return table.concat(out)
end

local function signature(id)
	local h1 = hash(id .. SECRET, 5381, 33) % BASE25
	local h2 = hash(SECRET .. id, 40503, 37) % BASE25
	return encode32(h1 * BASE25 + h2, 10)
end

-- Uppercase, drop separators, map the characters Crockford base32 excludes
-- for being ambiguous (O->0, I/L->1, U->V) so hand-typed keys still verify.
function License.normalize(key)
	local s = tostring(key or ''):upper()
	s = s:gsub('[^0-9A-Z]', '')
	s = s:gsub('O', '0'):gsub('[IL]', '1'):gsub('U', 'V')
	-- The CC prefix is cosmetic; strip it only when the length says it is
	-- present, since key data may legitimately start with "CC".
	if #s == 17 and s:sub(1, 2) == 'CC' then
		s = s:sub(3)
	end
	return s
end

function License.validate(key)
	local s = License.normalize(key)
	if #s ~= 15 then
		return false
	end
	return signature(s:sub(1, 5)) == s:sub(6)
end

function License.isLicensed()
	return License.validate(prefs.licenseKey)
end

-- ---------------------------------------------------------------- trial --

local function trialFile()
	return LrPathUtils.child(LrPathUtils.getStandardFilePath('home'),
		'.cutcorners')
end

local function fileChecksum(count)
	return hash(tostring(count) .. SECRET, 977, 31) % 100000
end

local function readTrialFile()
	local path = trialFile()
	if not LrFileUtils.exists(path) then
		return 0
	end
	local raw = LrFileUtils.readFile(path) or ''
	local count, check = raw:match('^(%d+):(%d+)')
	count, check = tonumber(count), tonumber(check)
	if count == nil or check ~= fileChecksum(count) then
		return 0 -- unreadable or edited: fall back to the prefs count
	end
	return count
end

local function writeTrialFile(count)
	local fh = io.open(trialFile(), 'w')
	if fh then
		fh:write(string.format('%d:%d\n', count, fileChecksum(count)))
		fh:close()
	end
end

function License.trialUsed()
	return math.max(tonumber(prefs.trialCount) or 0, readTrialFile())
end

function License.trialRemaining()
	return math.max(0, License.TRIAL_LIMIT - License.trialUsed())
end

function License.consumeTrial(n)
	if License.isLicensed() or (tonumber(n) or 0) <= 0 then
		return
	end
	local used = License.trialUsed() + n
	prefs.trialCount = used
	writeTrialFile(used)
end

-- One-line status for dialogs and the Plug-in Manager section.
function License.statusText()
	if License.isLicensed() then
		return 'Licensed - thank you for supporting Cut Corners.'
	end
	return string.format('Trial: %d of %d images used.',
		math.min(License.trialUsed(), License.TRIAL_LIMIT),
		License.TRIAL_LIMIT)
end

-- Modal key-entry dialog. Loops until a valid key is activated or the user
-- cancels; returns whether the plugin is licensed afterwards. Must run in an
-- async task with a function context (same requirement as the run dialog).
function License.promptForKey(context)
	local f = LrView.osFactory()
	local props = LrBinding.makePropertyTable(context)
	props.key = tostring(prefs.licenseKey or '')

	while true do
		local result = LrDialogs.presentModalDialog({
			title = 'Cut Corners: License',
			contents = f:column {
				bind_to_object = props,
				spacing = f:control_spacing(),
				f:static_text { title = License.statusText() },
				f:row {
					f:static_text { title = 'License key:' },
					f:edit_field {
						value = LrView.bind 'key',
						width_in_chars = 24,
						immediate = true,
					},
				},
				f:row {
					f:static_text {
						title = 'Keys look like CC-XXXXX-XXXXX-XXXXX.',
						font = '<system/small>',
					},
					f:push_button {
						title = 'Buy a license...',
						action = function()
							LrHttp.openUrlInBrowser(License.BUY_URL)
						end,
					},
				},
			},
			actionVerb = 'Activate',
		})
		if result ~= 'ok' then
			return License.isLicensed()
		end
		if License.validate(props.key) then
			prefs.licenseKey = License.normalize(props.key)
			LrDialogs.message('Cut Corners',
				'License activated - thank you for supporting Cut Corners.')
			return true
		end
		LrDialogs.message('Cut Corners',
			'That key did not verify. Check it for typos (keys look like'
				.. ' CC-XXXXX-XXXXX-XXXXX), or contact support@cutcorners.app.')
	end
end

return License
