Updater = {}

Updater.maxRetries = 5

local updaterWindow
local loadModulesFunction
local scheduledEvent
local httpOperationId = 0
local checking = false
local windowShownAtMs = nil

-- Maioria dos boots ja esta atualizada e o updater.php (manifesto de
-- checksums cacheado no servidor) responde rapido, mas a viagem ate a VM +
-- overhead do PHP costuma ficar bem perto de meio segundo -- um grace period
-- curto so fazia a janela "Verificando..." piscar e sumir quase no ato (pior
-- que nunca mostrar nada). Por isso o prazo aqui e generoso: so mostra algo
-- se o check realmente estiver demorando de verdade. Ver docs/PLANO_DEPLOY.md
-- "Atualizacao do client".
local GRACE_TICKS = 15    -- ~1.5s (tick = 100ms) antes de mostrar a janela
local TIMEOUT_TICKS = 100 -- ~10s: desiste e entra sem atualizar (nao bloqueia)
local MIN_VISIBLE_MS = 500 -- se a janela chegou a aparecer, fica visivel por pelo menos isso (evita flash)

local function onLog(level, message, time)
  if level == LogError then
    Updater.error(message)
    g_logger.setOnLog(nil)
  end
end

-- Constroi (e mostra) a janela do updater sob demanda -- so quando o check
-- demora (grace period) ou quando ja sabemos que ha algo pra baixar. Assim
-- o caso comum (client ja atualizado, resposta rapida do updater.php) nunca
-- pisca UI nenhuma no boot: entra direto no jogo.
local function ensureWindow()
  if updaterWindow then return end
  updaterWindow = g_ui.displayUI('updater')
  updaterWindow:show()
  updaterWindow:focus()
  updaterWindow:raise()
  windowShownAtMs = g_clock.millis()
end

local function loadModules()
  if loadModulesFunction then
    local tmpLoadFunc = loadModulesFunction
    loadModulesFunction = nil
    tmpLoadFunc()
  end
end

local function downloadFiles(url, files, index, retries, doneCallback)
  if not updaterWindow then return end
  local entry = files[index]
  if not entry then -- finished
    return doneCallback()
  end
  local file = entry[1]
  local file_checksum = entry[2]

  if retries > 0 then
    updaterWindow.downloadStatus:setText(tr("Downloading (%i retry):\n%s", retries, file))
  else
    updaterWindow.downloadStatus:setText(tr("Downloading:\n%s", file))
  end
  updaterWindow.downloadProgress:setPercent(0)
  updaterWindow.mainProgress:setPercent(math.floor(100 * index / #files))

  httpOperationId = HTTP.download(url .. file, file,
    function(file, checksum, err)
      if not err and checksum ~= file_checksum then
        err = "Invalid checksum of: " .. file .. ".\nShould be " .. file_checksum .. ", is: " .. checksum
      end
      if err then
        if retries >= Updater.maxRetries then
          Updater.error("Can't download file: " .. file .. ".\nError: " .. err)
        else
          scheduledEvent = scheduleEvent(function()
            downloadFiles(url, files, index, retries + 1, doneCallback)
          end, 250)
        end
        return
      end
      downloadFiles(url, files, index + 1, 0, doneCallback)
    end,
    function(progress, speed)
      updaterWindow.downloadProgress:setPercent(progress)
      updaterWindow.downloadProgress:setText(speed .. " kbps")
    end)
end

local function updateFiles(data, keepCurrentFiles)
  if type(data) ~= "table" then
    ensureWindow()
    return Updater.error("Invalid data from updater api (not table)")
  end

  if type(data.error) == 'string' and data.error:len() > 0 then
    ensureWindow()
    return Updater.error(data.error)
  end

  if not data.files or type(data.url) ~= 'string' or data.url:len() < 4 then
    ensureWindow()
    return Updater.error("Invalid data from updater api: " .. json.encode(data, 2))
  end

  if data.keepFiles then
    keepCurrentFiles = true
  end

  local newFiles = false
  local finalFiles = {}
  local localFiles = g_resources.filesChecksums()

  local toUpdate = {}
  local toUpdateFiles = {}
  -- keep all files or files from data/things
  for file, checksum in pairs(localFiles) do
    if keepCurrentFiles or string.find(file, "data/things") then
      table.insert(finalFiles, file)
    end
  end

  -- update files
  for file, checksum in pairs(data.files) do
    table.insert(finalFiles, file)
    if not localFiles[file] or localFiles[file] ~= checksum then
      table.insert(toUpdate, { file, checksum })
      table.insert(toUpdateFiles, file)
      newFiles = true
    end
  end

  -- update binary
  local binary = nil
  if type(data.binary) == "table" and data.binary.file:len() > 1 then
    local selfChecksum = g_resources.selfChecksum()
    if selfChecksum:len() > 0 and selfChecksum ~= data.binary.checksum then
      binary = data.binary.file
      table.insert(toUpdate, { binary, data.binary.checksum })
    end
  end

  if #toUpdate == 0 then -- nothing to update: nunca precisou mostrar janela nenhuma
    return Updater.abort()
  end

  -- daqui pra baixo ha arquivos de verdade pra baixar -- agora sim mostra a janela
  ensureWindow()

  -- update of some files require full client restart
  local forceRestart = false
  local reloadModules = false
  local forceRestartPattern = { "init.lua", "corelib", "updater", "otmod" }
  for _, file in ipairs(toUpdate) do
    for __, pattern in ipairs(forceRestartPattern) do
      if string.find(file[1], pattern) then
        forceRestart = true
      end
      if not string.find(file[1], "data/things") then
        reloadModules = true
      end
    end
  end

  updaterWindow.status:setText(tr("Updating %i files", #toUpdate))
  updaterWindow.mainProgress:setPercent(0)
  updaterWindow.downloadProgress:setPercent(0)
  updaterWindow.downloadProgress:show()
  updaterWindow.downloadStatus:show()
  updaterWindow.changeUrlButton:hide()

  downloadFiles(data["url"], toUpdate, 1, 0, function()
    updaterWindow.status:setText(tr("Updating client (may take few seconds)"))
    updaterWindow.mainProgress:setPercent(100)
    updaterWindow.downloadProgress:hide()
    updaterWindow.downloadStatus:hide()
    scheduledEvent = scheduleEvent(function()
      local restart = binary or (not loadModulesFunction and reloadModules) or forceRestart
      if newFiles then
        g_resources.updateFiles(toUpdateFiles, not restart)
      end

      if binary then
        g_resources.updateExecutable(binary)
      end

      if restart then
        g_app.restart()
      else
        if reloadModules then
          g_textures.clearCache()
          g_modules.reloadModules()
        end
        Updater.abort()
      end
    end, 100)
  end)
end

-- public functions
function Updater.init(loadModulesFunc)
  g_logger.setOnLog(onLog)
  loadModulesFunction = loadModulesFunc
  Updater.check()
end

function Updater.terminate()
  loadModulesFunction = nil
  Updater.abort(true)
end

function Updater.abort(terminate)
  HTTP.cancel(httpOperationId)
  removeEvent(scheduledEvent)

  -- Se a janela chegou a aparecer ha pouco tempo, segura o teardown ate
  -- completar MIN_VISIBLE_MS -- sem isso, um check que resolve logo depois
  -- do grace period faz a janela "Verificando..." piscar e sumir quase no
  -- mesmo frame (pior UX que nunca ter mostrado nada). Nao se aplica ao
  -- terminate=true (app fechando/modulo descarregando -- ai o teardown tem
  -- que ser imediato).
  if not terminate and updaterWindow and windowShownAtMs then
    local elapsed = g_clock.millis() - windowShownAtMs
    if elapsed < MIN_VISIBLE_MS then
      scheduledEvent = scheduleEvent(function() Updater.abort(terminate) end, MIN_VISIBLE_MS - elapsed)
      return
    end
  end

  checking = false
  if updaterWindow then
    updaterWindow:destroy()
    updaterWindow = nil
  end
  windowShownAtMs = nil
  loadModules()
  if not terminate then
    signalcall(g_app.onUpdateFinished, g_app)
  end
end

function Updater.check(args)
  if checking then return end
  checking = true

  local checkDone = false
  local checkError = nil
  local updateData = nil

  local function progressUpdater(value)
    removeEvent(scheduledEvent)

    if checkDone then
      if checkError then
        -- Backend do updater fora do ar / lento demais (ex.: cache de
        -- checksums frio na VM, ver docs/PLANO_DEPLOY.md). Nao trava o
        -- jogador com dialog de erro -- entra sem atualizar e tenta de
        -- novo no proximo boot (mesma logica do "offline" do client Godot).
        g_logger.warning("[Updater] check falhou (" .. tostring(checkError) .. "), entrando sem atualizar")
        return Updater.abort()
      end
      if updateData then
        -- updateFiles decide sozinho se ha algo real pra baixar; so mostra
        -- a janela (ensureWindow) se de fato houver arquivo/binario a atualizar.
        return updateFiles(updateData)
      end
      -- checkDone, sem erro, mas resposta vazia/nula: nada pra atualizar.
      return Updater.abort()
    end

    if value >= TIMEOUT_TICKS then
      g_logger.warning("[Updater] check demorou demais (timeout), entrando sem atualizar")
      return Updater.abort()
    end

    if value >= GRACE_TICKS then
      ensureWindow()
      if updaterWindow then
        updaterWindow.mainProgress:setPercent(math.min(value, 99))
      end
    end

    scheduledEvent = scheduleEvent(function() progressUpdater(value + 1) end, 100)
  end
  progressUpdater(0)

  httpOperationId = HTTP.postJSON(Services.updater, {
    version = g_app.getBuildRevision(),
    build = g_app.getVersion(),
    os = g_app.getOs(),
    platform = g_window.getPlatformType(),
    args = args or {}
  }, function(data, err)
    checkDone = true
    checkError = err
    updateData = data
  end)
end

function Updater.error(message)
  removeEvent(scheduledEvent)
  if not updaterWindow then return end
  displayErrorBox(tr("Updater Error"), message).onOk = function()
    Updater.abort()
  end
end
