---@alias AppMeta {id: string, name: string, author: string, version: string, icon: string, description: string, size: integer, detailsURL: string, downloadURL: string, category: string}
---@alias AppInfo {meta: AppMeta, installed: string, installing: boolean|string, displaySize: string, location: string, domainName: string, newApp: boolean?}

local _config
local function config()
  if not _config then
    _config = ac.storage{
      notifyAboutNewApps = false,
      notifyAboutUpdates = true,
      automaticallyInstallUpdates = false,
      settingsOpenedOnce = false,
      openOnceInstalled = true,
      knownApps = ''
    }
  end
  return _config
end

---@param meta AppMeta
---@return AppInfo?
local function createInfo(meta)
  if meta.id:startsWith('.') then return nil end
  local location = ac.getFolder(ac.FolderID.ACAppsLua)..'\\'..meta.id
  local installedManifest = ac.INIConfig.load(location..'\\manifest.ini', ac.INIFormat.Extended)
  local installedVersion = installedManifest:get('ABOUT', 'VERSION', '')
  local domainName = meta.detailsURL and meta.detailsURL:regmatch('(?://)(?:www\\.)?([\\w\\.]+)')
  return {
    meta = meta,
    installed = #installedVersion > 0 and installedVersion,
    installing = false,
    displaySize = meta.size > 1024 * 1024 and '%.1f MB' % (meta.size / (1024 * 1024))
      or '%.0f KB' % (meta.size / 1024),
    domainName = domainName,
    location = location
  }
end

---@param app AppInfo
---@param callback fun(err: string?)? 
local function installApp(app, callback)
  if app.installing then return end
  app.installing = true
  app.newApp = false
  local openOnceInstalled = not app.installed
  local updating = app.installed
  ac.startBackgroundWorker('BackgroundInstall', app, function (err, data)
    if err then
      app.installing = err
      ui.toast(ui.Icons.Warning, '%s: failed to install' % app.meta.name)
      ac.warn(err)
      if callback then callback(err) end
    else
      app.installing = false
      app.installed = data
      ui.toast(ui.Icons.Confirm, (updating and '%s is updated to v%s' or '%s is installed') % {app.meta.name, data})
      ac.noticeNewApp(app.meta.id)
      ac.store('.appShelf.freshlyInstalled.%s' % app.meta.id, 1)
      ac.log('openOnceInstalled', openOnceInstalled)
      ac.log('config().openOnceInstalled', config().openOnceInstalled)
      if openOnceInstalled and config().openOnceInstalled then
        setTimeout(function () ac.setAppOpen(app.meta.id) end, 0.1)
      end
      if callback then callback(nil) end
    end
  end)
end

---@param app AppInfo
local function refreshApp(app)
  local location = ac.getFolder(ac.FolderID.ACAppsLua)..'\\'..app.meta.id
  local installedManifest = ac.INIConfig.load(location..'\\manifest.ini', ac.INIFormat.Extended)
  local installedVersion = installedManifest:get('ABOUT', 'VERSION', '')
  app.installed = #installedVersion > 0 and installedVersion
end

---@param app AppInfo
local function uninstallApp(app)
  if app.installing or not app.installed then
    error('Invalid state')
  end
  if ac.uninstallApp(app.meta.id) then
    app.installing = false
    app.installed = nil
    ui.toast(ui.Icons.Confirm, '%s is uninstalled' % app.meta.name)
  else
    ui.toast(ui.Icons.Warning, 'Failed to uninstall %s' % app.meta.name)
  end
end

local abTestingKeyStorage

local function abTestingKey()
  if not abTestingKeyStorage then
    abTestingKeyStorage = ac.storage('abTestingKeyStorage', math.random())
  end
  return abTestingKeyStorage:get()
end

---@param callback fun(err: string?, data: AppInfo[]?)
---@param installOwnUpdate fun(data: AppMeta)?
local function loadApps(callback, installOwnUpdate)
  web.get('https://acstuff.club/app/app-loader/apps?target=%d' % ac.getPatchVersionCode(), function (err, data)
    if err then
      return callback(err, nil)
    end
    local parsed = JSON.parse(data.body)
    if type(parsed) == 'table' then
      local items = table.map(parsed, function (item)
        if item.id == '.AppShelf' then
          if installOwnUpdate then
            installOwnUpdate(item)
          end
          return nil 
        end
        if item.abTesting and item.abTesting > abTestingKey() then
          return nil
        end
        return createInfo(item)
      end) ---@type AppInfo[]
      table.sort(items, function (a, b)
        local ap = not a.installed and 0 or string.versionCompare(a.meta.version, a.installed) > 0 and 1 or -1
        local bp = not b.installed and 0 or string.versionCompare(b.meta.version, b.installed) > 0 and 1 or -1
        if ap ~= bp then return ap > bp end
        return a.meta.name < b.meta.name
      end)
      callback(nil, items)
    else
      ac.warn(data.body)
      callback('Data is corrupted', nil)
    end
  end)
end

return {
  createInfo = createInfo,
  installApp = installApp,
  refreshApp = refreshApp,
  uninstallApp = uninstallApp,
  loadApps = loadApps,
  config = config
}