local OPCODE = 209
local window
local selectedItem
local state = {level=1, recovery=25, catalog={}}

local function captureKeyboard()
  if not window then return end
  window:grabKeyboard()
  window.onKeyDown = function(_, keyCode, keyboardModifiers)
    if keyboardModifiers == KeyboardNoModifier and keyCode == KeyEscape then hide() end
    return true
  end
end

local function releaseKeyboard()
  if window then window:ungrabKeyboard() end
end

local function ui(id)
  return window and window:recursiveGetChildById(id)
end

local function clearSelection()
  selectedItem = nil
  if ui('itemSlot') then ui('itemSlot'):setItem(nil) end
  if ui('selectedName') then ui('selectedName'):setText('No item selected') end
  if ui('materialList') then ui('materialList'):destroyChildren() end
  if ui('previewNotice') then ui('previewNotice'):setText('Select an item to see its materials.') end
end

local function updateProgress(data)
  local level = tonumber(data.level) or state.level
  local current = tonumber(data.xp) or 0
  local needed = math.max(1, tonumber(data.nextXp) or 80)
  ui('profession'):setText(string.format('Salvaging %d', level))
  ui('progress'):setPercent(math.min(100, math.floor(current * 100 / needed)))
  ui('xp'):setText(string.format('%d / %d XP', current, needed))
end

local function recoveryText(base, fallback)
  local exact = (tonumber(base) or 0) * state.recovery / 100
  local minimum, maximum = math.floor(exact), math.ceil(exact)
  if minimum == maximum then return string.format('%d guaranteed', minimum) end
  local chance = math.floor((exact - minimum) * 100 + 0.5)
  if fallback and minimum == 0 then
    return string.format('0-%d (%d%%) | fallback', maximum, chance)
  end
  return string.format('%d-%d (%d%% for %d)', minimum, maximum, chance, maximum)
end

local function fillMaterials(materials, actual)
  local list = ui('materialList')
  list:destroyChildren()
  for _, material in ipairs(materials or {}) do
    local row = g_ui.createWidget('SalvageMaterialRow', list)
    row.icon:setItemId(material.clientId)
    row.materialName:setText(material.name)
    row.amount:setText(actual and string.format('%dx received', material.count or 0) or recoveryText(material.base, material.fallback))
  end
end

local function itemLabel(item)
  if not item then return 'No item selected' end
  local tooltip = item.getTooltip and item:getTooltip() or nil
  if tooltip and tooltip ~= '' then return tooltip:match('^[^\n]+') or tooltip end
  return 'Selected item'
end

local function onDrop(slot, widget)
  local item = widget and widget.currentDragThing
  if not item or not item:isItem() or not item:isPickupable() then return false end
  selectedItem = item
  slot:setItem(Item.create(item:getId(), item:getCountOrSubType()))
  local preview = state.catalog and state.catalog[tostring(item:getId())]
  ui('selectedName'):setText(preview and preview.name or itemLabel(item))
  if preview and preview.materials then
    fillMaterials(preview.materials, false)
    ui('previewNotice'):setText('If every roll fails, you always receive 1x of the first material.')
  else
    ui('previewNotice'):setText('Checking this item with the server...')
  end
  local position = item:getPosition()
  local protocol = g_game.getProtocolGame()
  if protocol and position then
    protocol:sendExtendedOpcode(OPCODE, json.encode({action='preview', position={x=position.x,y=position.y,z=position.z}, clientId=item:getId()}))
  end
  return true
end

local function salvage()
  if not selectedItem then
    ui('selectedName'):setText('Drag an equipment item into the slot first.')
    return
  end
  if not g_game.isOnline() then return end
  local position = selectedItem:getPosition()
  local protocol = g_game.getProtocolGame()
  if not protocol or not position then return end
  protocol:sendExtendedOpcode(OPCODE, json.encode({action='salvage', position={x=position.x,y=position.y,z=position.z}, clientId=selectedItem:getId()}))
  clearSelection()
end

local function onOpcode(protocol, opcode, buffer)
  local ok, message = pcall(json.decode, buffer)
  if not ok or type(message) ~= 'table' then return end
  local data = message.data or {}
  state.level = tonumber(data.level) or state.level
  state.recovery = tonumber(data.recovery) or state.recovery
  state.catalog = data.catalog or state.catalog
  updateProgress(data)
  if modules.game_skills and modules.game_skills.setReperaProfession then
    modules.game_skills.setReperaProfession('salvaging',data.level or 1,data.xp or 0,data.nextXp or 80)
  end
  if message.action == 'result' then
    ui('stats'):setText(string.format('Level %d | %.1f%% recovery', state.level, state.recovery))
    if message.result then
      fillMaterials(message.result, true)
      ui('previewNotice'):setText('Complete: these materials were added to your inventory.')
    end
    return
  end
  if message.action == 'preview' then
    if message.result and #message.result > 0 then
      fillMaterials(message.result, false)
      ui('previewNotice'):setText('If every roll fails, you always receive 1x of the first material.')
    else
      ui('materialList'):destroyChildren()
      ui('previewNotice'):setText(data.reason or 'This item cannot be salvaged.')
    end
    return
  end
  if message.action ~= 'open' then return end
  ui('stats'):setText(string.format('Level %d | %.1f%% recovery', state.level, state.recovery))
  clearSelection()
  window:show()
  window:raise()
  window:focus()
  captureKeyboard()
end

function init()
  ProtocolGame.registerExtendedOpcode(OPCODE, onOpcode)
  window = g_ui.displayUI('salvaging')
  window:hide()
  ui('itemSlot').onDrop = onDrop
  ui('salvage').onClick = salvage
  ui('clear').onClick = clearSelection
  ui('close').onClick = hide
  ui('topClose').onClick = hide
  connect(g_game, { onGameStart = requestStatus, onGameEnd = hide })
  if g_game.isOnline() then requestStatus() end
end

function terminate()
  disconnect(g_game, { onGameStart = requestStatus, onGameEnd = hide })
  ProtocolGame.unregisterExtendedOpcode(OPCODE)
  releaseKeyboard()
  if window then window:destroy(); window = nil end
  selectedItem = nil
end

function hide()
  clearSelection()
  releaseKeyboard()
  if window then window:hide() end
end

function requestStatus()
  scheduleEvent(function()
    local protocol = g_game.getProtocolGame()
    if protocol then protocol:sendExtendedOpcode(OPCODE,json.encode({action='sync'})) end
  end, 900)
end
