local VERSION = "v1.5.7"
local MACHINE_SIDE = "bottom"
local PAYMENT_SIDE = "right"
local PRESET_AMOUNTS = { 5, 10, 15, 20 }
local DEFAULT_API_BASE = "http://ticket.fse-media.group/api"

local termDev = term.current()
local w, h = termDev.getSize()
local Buttons = {}
local UI = {
  bg = colors.black,
  panel = colors.gray,
  panelDark = colors.black,
  text = colors.white,
  muted = colors.lightGray,
  accent = colors.cyan,
  success = colors.lime,
  warning = colors.yellow,
  danger = colors.red,
  info = colors.lightBlue,
  action = colors.green,
  neutral = colors.gray,
}

local state = {
  page = "home",
  amount = 0,
  amountInput = "",
  paid = 0,
  finalBalance = nil,
  cardInfo = nil,
}

local function trim(value)
  return tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", "")
end

local function readApiEndpointFile(path)
  if not fs.exists(path) then return nil end
  local f = fs.open(path, "r")
  if not f then return nil end
  local text = f.readAll()
  f.close()
  return trim(text)
end

local function resolveApiBase()
  local base = readApiEndpointFile("API_ENDPOINT_REFILL.txt") or readApiEndpointFile("API_ENDPOINT.txt") or DEFAULT_API_BASE
  base = trim(base)
  if base:match("/api$") then
    return base
  end
  return base:gsub("/+$", "") .. "/api"
end

local API_BASE = resolveApiBase()

local function urlEncodeComponent(value)
  local s = tostring(value or "")
  return (s:gsub("([^%w%-_%.~])", function(c)
    return string.format("%%%02X", string.byte(c))
  end))
end

local function postJSON(url, payload)
  if not http then
    return false, "HTTP API disabled"
  end
  local okBody, body = pcall(textutils.serializeJSON, payload or {})
  if not okBody or type(body) ~= "string" then
    return false, "serializeJSON failed"
  end
  local headers = { ["Content-Type"] = "application/json" }

  if http.post then
    local ok, res, err = pcall(http.post, url, body, headers)
    if not ok or not res then
      return false, tostring(err or res or "http.post failed")
    end
    local data = res.readAll and res.readAll() or ""
    res.close()
    local okJson, parsed = pcall(textutils.unserializeJSON, data or "")
    return true, okJson and parsed or data
  end

  local okReq, reqErr = pcall(http.request, { url = url, method = "POST", headers = headers, body = body })
  if not okReq or not reqErr then
    return false, tostring(reqErr or "http.request failed")
  end
  while true do
    local ev, p1, p2, p3 = os.pullEvent()
    if ev == "http_success" and p1 == url then
      local res = p2
      local data = res.readAll and res.readAll() or ""
      res.close()
      local okJson, parsed = pcall(textutils.unserializeJSON, data or "")
      return true, okJson and parsed or data
    end
    if ev == "http_failure" and p1 == url then
      local err = p2
      local res = p3
      if type(p2) == "table" and type(p2.readAll) == "function" then
        res = p2
        err = p3
      end
      if type(res) == "table" and type(res.readAll) == "function" then
        local data = res.readAll()
        res.close()
        return false, data
      end
      return false, tostring(err or "http_failure")
    end
    os.queueEvent(ev, p1, p2, p3)
    sleep(0)
  end
end

local function syncRefillResult(cardInfo, amount, balance)
  local cardId = trim(cardInfo and cardInfo.cardId or "")
  if #cardId == 0 then return false, "missing_card_id" end
  local url = API_BASE .. "/ic-cards/" .. urlEncodeComponent(cardId) .. "/sync"
  return postJSON(url, {
    type = "topup",
    device = "refill_machine",
    amount = tonumber(amount) or 0,
    balance = tonumber(balance) or 0,
    result = "pass",
    last_action = "topup"
  })
end

local function refreshSize()
  w, h = termDev.getSize()
end

local function clearScreen()
  refreshSize()
  termDev.setBackgroundColor(colors.black)
  termDev.setTextColor(colors.white)
  termDev.clear()
  termDev.setCursorPos(1, 1)
end

local function centerText(y, text, color)
  text = tostring(text or "")
  termDev.setTextColor(color or colors.white)
  termDev.setCursorPos(math.max(1, math.floor((w - #text) / 2) + 1), y)
  termDev.write(text)
end

local function writeText(x, y, text, fg, bg)
  if bg then termDev.setBackgroundColor(bg) end
  if fg then termDev.setTextColor(fg) end
  termDev.setCursorPos(x, y)
  termDev.write(text)
end

local function fitText(text, maxLen)
  text = tostring(text or "")
  maxLen = math.max(1, tonumber(maxLen) or #text)
  if #text <= maxLen then return text end
  if maxLen <= 3 then return text:sub(1, maxLen) end
  return text:sub(1, maxLen - 3) .. "..."
end

local function drawFooterHint(text)
  centerText(h - 1, fitText(text, math.max(1, w - 2)), UI.muted)
end

local function addButton(x, y, label, bw, bh, bg, fg, onClick)
  Buttons[#Buttons + 1] = {
    x = x,
    y = y,
    w = bw,
    h = bh,
    onClick = onClick,
  }

  termDev.setBackgroundColor(bg)
  termDev.setTextColor(fg)
  for row = 0, bh - 1 do
    termDev.setCursorPos(x, y + row)
    termDev.write(string.rep(" ", bw))
  end

  local labelX = x + math.max(0, math.floor((bw - #label) / 2))
  local labelY = y + math.floor((bh - 1) / 2)
  termDev.setCursorPos(labelX, labelY)
  termDev.write(label)
end

local function inRect(btn, px, py)
  return px >= btn.x and px <= (btn.x + btn.w - 1) and py >= btn.y and py <= (btn.y + btn.h - 1)
end

local function handleTouch(x, y)
  for _, btn in ipairs(Buttons) do
    if inRect(btn, x, y) then
      if btn.onClick then btn.onClick() end
      return true
    end
  end
  return false
end

local function getRefillMachine()
  local sideType = peripheral.getType and peripheral.getType(MACHINE_SIDE) or nil
  if sideType == "ic_refill_machine" then
    local ok, dev = pcall(peripheral.wrap, MACHINE_SIDE)
    if ok and type(dev) == "table" then
      return dev
    end
  end

  local dev = peripheral.find("ic_refill_machine")
  if type(dev) == "table" then return dev end
  return nil
end

local function readCardInfo()
  local dev = getRefillMachine()
  if not dev or type(dev.getCardInfo) ~= "function" then
    return nil, "machine_unavailable"
  end

  local ok, info = pcall(dev.getCardInfo)
  if not ok or type(info) ~= "table" then
    return nil, "no_card"
  end

  local hasData = info.cardId ~= nil or info.ownerName ~= nil or info.balance ~= nil
  if not hasData then
    return nil, "no_card"
  end

  return {
    cardId = tostring(info.cardId or ""),
    ownerName = tostring(info.ownerName or "UNKNOWN"),
    balance = tonumber(info.balance) or 0,
  }
end

local function drawHeader(title, subTitle)
  clearScreen()
  centerText(2, fitText(title, math.max(1, w - 4)), UI.text)
  if subTitle and #tostring(subTitle) > 0 then
    centerText(3, fitText(subTitle, math.max(1, w - 4)), UI.info)
  end

  writeText(1, 1, VERSION, UI.muted, UI.bg)

  local hasMachine = getRefillMachine() ~= nil
  local status = hasMachine and "S* OK" or "S* ERR"
  local statusColor = hasMachine and UI.success or UI.danger
  local statusX = math.max(1, w - #status + 1)
  writeText(statusX, 1, status, statusColor, UI.bg)
  termDev.setBackgroundColor(UI.bg)
  termDev.setTextColor(UI.text)
end

local function getAmountFromInput()
  local value = tonumber(state.amountInput)
  if not value then return 0 end
  return math.max(0, math.floor(value))
end

local function setAmount(value)
  value = math.max(0, math.floor(tonumber(value) or 0))
  state.amount = value
  state.amountInput = value > 0 and tostring(value) or ""
end

local function appendDigit(ch)
  ch = tostring(ch or "")
  if not ch:match("^%d$") then return end
  if #state.amountInput >= 4 then return end
  if state.amountInput == "0" then
    state.amountInput = ch
  else
    state.amountInput = state.amountInput .. ch
  end
  state.amount = getAmountFromInput()
end

local function drawProgressBar(current, total, y)
  local barW = math.max(10, w - 10)
  local barX = math.floor((w - barW) / 2) + 1
  local ratio = total <= 0 and 1 or math.min(1, current / total)
  local fill = math.floor(barW * ratio)

  termDev.setCursorPos(barX, y)
  termDev.setTextColor(colors.green)
  termDev.write(string.rep("=", fill))
  termDev.setTextColor(colors.gray)
  termDev.write(string.rep("-", math.max(0, barW - fill)))
end

local function resetFlowToHome()
  state.amount = 0
  state.amountInput = ""
  state.paid = 0
  state.finalBalance = nil
  state.cardInfo = nil
  state.page = "home"
end

local function waitForEventWithTimer(seconds)
  local timer = os.startTimer(seconds)
  while true do
    local ev, p1, p2, p3 = os.pullEvent()
    if ev == "timer" and p1 == timer then
      return "timer"
    end
    if ev == "mouse_click" or ev == "monitor_touch" then
      return ev, p1, p2, p3
    end
    if ev == "redstone" or ev == "term_resize" or ev == "peripheral" or ev == "peripheral_detach" then
      return ev, p1, p2, p3
    end
  end
end

local function drawHomePage()
  drawHeader("FSE Refill Machine", "IC Card Balance Refill")
  Buttons = {}

  local btnW = 18
  local btnH = 5
  local btnX = math.floor((w - btnW) / 2) + 1
  local btnY = math.floor((h - btnH) / 2)
  addButton(btnX, btnY, "START REFILL", btnW, btnH, UI.action, UI.text, function()
    state.amount = 0
    state.amountInput = ""
    state.paid = 0
    state.finalBalance = nil
    state.cardInfo = nil
    state.page = "amount"
  end)

  drawFooterHint("Touch the button to start")
end

local function drawAmountPage()
  drawHeader("Select Refill Amount", "Preset amount or keyboard input")
  Buttons = {}

  local displayText = state.amountInput ~= "" and state.amountInput or "0"
  local boxW = math.min(20, w - 8)
  local boxX = math.floor((w - boxW) / 2) + 1
  local presetY = 8
  local presetW = 8
  local gap = 2
  local totalPresetW = (presetW * 4) + (gap * 3)
  local presetX = math.floor((w - totalPresetW) / 2) + 1
  local notesY = presetY + 4
  local clearY = notesY + 3
  local bottomY = h - 3

  if clearY + 2 >= bottomY then
    clearY = math.max(notesY + 2, bottomY - 4)
  end

  termDev.setBackgroundColor(colors.gray)
  termDev.setTextColor(colors.yellow)
  termDev.setCursorPos(boxX, 5)
  termDev.write(string.rep(" ", boxW))
  termDev.setCursorPos(boxX + boxW - #displayText - 2, 5)
  termDev.write(displayText)
  termDev.setBackgroundColor(colors.black)
  centerText(4, "Amount", colors.white)

  for index, amount in ipairs(PRESET_AMOUNTS) do
    local x = presetX + (index - 1) * (presetW + gap)
    local selected = getAmountFromInput() == amount
    addButton(x, presetY, tostring(amount), presetW, 3, selected and colors.green or colors.gray, colors.white, function()
      setAmount(amount)
    end)
  end

  centerText(notesY, "Custom amount: type on keyboard", colors.lightGray)
  centerText(notesY + 1, "Enter to continue, Backspace to delete", colors.lightGray)

  addButton(math.floor((w - 10) / 2) + 1, clearY, "CLEAR", 10, 3, colors.red, colors.white, function()
    setAmount(0)
  end)

  addButton(2, h - 3, "BACK", 8, 3, colors.red, colors.white, function()
    state.page = "home"
  end)
  addButton(w - 11, h - 3, "NEXT", 10, 3, getAmountFromInput() > 0 and colors.green or colors.gray, colors.black, function()
    local amount = getAmountFromInput()
    if amount > 0 then
      state.amount = amount
      state.paid = 0
      state.page = "payment"
    end
  end)
end

local function drawPaymentPage()
  drawHeader("Payment", "Insert payment from RIGHT side")
  Buttons = {}

  centerText(6, "Refill Amount: " .. tostring(state.amount), colors.yellow)
  centerText(8, "Paid: " .. tostring(state.paid), colors.white)
  centerText(9, "Remaining: " .. tostring(math.max(0, state.amount - state.paid)), colors.red)
  drawProgressBar(state.paid, state.amount, 11)
  centerText(13, "1 redstone pulse = 1 amount", colors.lightGray)
  centerText(14, "Auto continue after full payment", colors.lightGray)

  addButton(2, h - 3, "BACK", 8, 3, colors.red, colors.white, function()
    state.page = "amount"
  end)
end

local function drawWaitingCardPage(cardDetected)
  drawHeader("Refill Processing", "Please keep the IC card inserted")
  Buttons = {}

  if cardDetected then
    centerText(7, "Refilling, Please do not remove the IC card", colors.yellow)
    if state.cardInfo then
      centerText(10, "Card ID: " .. tostring(state.cardInfo.cardId ~= "" and state.cardInfo.cardId or "UNKNOWN"), colors.cyan)
      centerText(12, "Current: " .. tostring(state.cardInfo.balance), colors.white)
      centerText(13, "Add: " .. tostring(state.amount), colors.lightBlue)
      centerText(14, "Target: " .. tostring(state.cardInfo.balance + state.amount), colors.green)
    end
  else
    centerText(8, "Please Insert IC card", colors.red)
    centerText(11, "Hold card and right click to insert", colors.lightGray)
    centerText(12, "The program will continue automatically", colors.lightGray)
  end

  addButton(2, h - 3, "CANCEL", 10, 3, colors.red, colors.white, function()
    resetFlowToHome()
  end)
end

local function drawDonePage()
  drawHeader("Refill Complete", "Refill completed")
  centerText(math.floor(h / 2) - 1, "DONE", colors.lime)
  if state.finalBalance ~= nil then
    centerText(math.floor(h / 2) + 1, "New Balance: " .. tostring(state.finalBalance), colors.white)
  end
end

local function drawErrorPage(message)
  drawHeader("Refill Failed", "Please try again")
  centerText(math.floor(h / 2), tostring(message or "Unknown error"), colors.red)
end

local function homeLoop()
  while state.page == "home" do
    drawHomePage()
    local ev, p1, p2, p3 = os.pullEvent()
    if ev == "mouse_click" or ev == "monitor_touch" then
      handleTouch(p2, p3)
    elseif ev == "term_resize" then
      refreshSize()
    end
  end
end

local function amountLoop()
  while state.page == "amount" do
    drawAmountPage()
    local ev, p1, p2, p3 = os.pullEvent()
    if ev == "mouse_click" or ev == "monitor_touch" then
      handleTouch(p2, p3)
    elseif ev == "char" and tostring(p1):match("%d") then
      appendDigit(p1)
    elseif ev == "key" and p1 == keys.backspace then
      state.amountInput = state.amountInput:sub(1, -2)
      state.amount = getAmountFromInput()
    elseif ev == "key" and (p1 == keys.enter or p1 == keys.numPadEnter) then
      if getAmountFromInput() > 0 then
        state.amount = getAmountFromInput()
        state.paid = 0
        state.page = "payment"
      end
    elseif ev == "term_resize" then
      refreshSize()
    end
  end
end

local function paymentLoop()
  state.paid = 0
  local lastSignal = redstone.getInput(PAYMENT_SIDE)

  while state.page == "payment" do
    drawPaymentPage()
    if state.paid >= state.amount then
      sleep(0.3)
      state.page = "refill"
      return
    end

    local ev, p1, p2, p3 = os.pullEvent()
    if ev == "redstone" then
      local now = redstone.getInput(PAYMENT_SIDE)
      if now and not lastSignal then
        state.paid = state.paid + 1
      end
      lastSignal = now
    elseif ev == "mouse_click" or ev == "monitor_touch" then
      handleTouch(p2, p3)
    elseif ev == "term_resize" then
      refreshSize()
    end
  end
end

local function refillLoop()
  while state.page == "refill" do
    local dev = getRefillMachine()
    if not dev then
      drawErrorPage("Refill machine not found on bottom")
      sleep(2)
      state.page = "home"
      return
    end

    local info = readCardInfo()
    if not info then
      drawWaitingCardPage(false)
      local ev, p1, p2, p3 = waitForEventWithTimer(0.2)
      if ev == "mouse_click" or ev == "monitor_touch" then
        handleTouch(p2, p3)
      elseif ev == "term_resize" then
        refreshSize()
      end
    else
      state.cardInfo = info
      drawWaitingCardPage(true)
      sleep(0.2)

      local currentBalance = tonumber(info.balance) or 0
      local refillAmount = tonumber(state.amount) or 0
      local expectedBalance = currentBalance + refillAmount
      local okCall, okRefill, newBalance = pcall(dev.refill, refillAmount)

      if okCall and okRefill then
        state.finalBalance = tonumber(newBalance) or expectedBalance
        state.cardInfo.balance = state.finalBalance
        syncRefillResult(state.cardInfo, refillAmount, state.finalBalance)
        state.page = "done"
        return
      end

      local errMessage = okCall and tostring(newBalance or "refill_failed") or "refill_call_failed"
      drawErrorPage(errMessage)
      sleep(2)
      state.page = "home"
      return
    end
  end
end

local function doneLoop()
  drawDonePage()
  sleep(3)
  resetFlowToHome()
end

while true do
  if state.page == "home" then
    homeLoop()
  elseif state.page == "amount" then
    amountLoop()
  elseif state.page == "payment" then
    paymentLoop()
  elseif state.page == "refill" then
    refillLoop()
  elseif state.page == "done" then
    doneLoop()
  else
    state.page = "home"
  end
end
