local PREFIX = "[TibiaAgentControl]"
local portalItemId = 30001
local firstDelayMs = 1500
local started = false
local loginRequested = false
local characterLoginRequested = false
local loginRequestedAt = 0
local lastCommandId = nil
local commandPath = "/modules/game_tibiaagent_control/command.json"
local autoLoginAccount = "newtibia"
local autoLoginPassword = "newtibia123"
local directionValue = nil

local function log(message)
  g_logger.info(PREFIX .. " " .. message)
end

local function say(msg)
  log("say " .. msg)
  local sent = false

  if modules and modules.game_console and modules.game_console.enableChat and modules.game_console.setTextEditText and modules.game_console.sendCurrentMessage then
    local ok = pcall(function()
      modules.game_console.enableChat(true)
      modules.game_console.setTextEditText(msg)
      modules.game_console.sendCurrentMessage()
    end)
    log("say route=consoleTextEdit ok=" .. tostring(ok))
    if ok then
      return true
    end
  end

  if modules and modules.game_console and modules.game_console.sendMessage then
    local tab = nil
    if modules.game_console.getTab then
      tab = modules.game_console.getTab("Default")
    end
    local ok = pcall(function() modules.game_console.sendMessage(msg, tab) end)
    log("say route=console ok=" .. tostring(ok))
    if ok then
      return true
    end
  end

  if g_game and g_game.talkChannel and MessageModes then
    local ok = pcall(function() g_game.talkChannel(MessageModes.Say, 0, msg) end)
    log("say route=talkChannel0 ok=" .. tostring(ok))
    if ok then
      return true
    end
  end

  if g_game and g_game.talk then
    local ok = pcall(function() g_game.talk(msg) end)
    log("say route=talk ok=" .. tostring(ok))
    if ok then
      return true
    end
  end

  if not sent then
    log("no chat API available")
  end
  return sent
end

local function useInventoryItem(itemId)
  itemId = tonumber(itemId or portalItemId)
  log("useInventoryItem " .. tostring(itemId))

  if g_game and g_game.useInventoryItem then
    g_game.useInventoryItem(itemId)
    return true
  end

  log("no useInventoryItem API available")
  return false
end

local function useInventoryItemWithSelf(itemId)
  itemId = tonumber(itemId or 0) or 0
  local player = g_game and g_game.getLocalPlayer and g_game.getLocalPlayer() or nil
  log("useInventoryItemWithSelf " .. tostring(itemId))
  if itemId > 0 and player and g_game and g_game.useInventoryItemWith then
    g_game.useInventoryItemWith(itemId, player)
    return true
  end
  log("no self target API available")
  return false
end

local function useInventoryItemWithTile(itemId, dx, dy)
  itemId = tonumber(itemId or 0) or 0
  dx = tonumber(dx or 0) or 0
  dy = tonumber(dy or 0) or 0
  local player = g_game and g_game.getLocalPlayer and g_game.getLocalPlayer() or nil
  local pos = player and player:getPosition() or nil
  log("useInventoryItemWithTile " .. tostring(itemId) .. " " .. tostring(dx) .. "," .. tostring(dy))
  if not pos or not g_map or not g_map.getTile or not g_game or not g_game.useInventoryItemWith then
    log("no tile target API available")
    return false
  end
  local tile = g_map.getTile({ x = pos.x + dx, y = pos.y + dy, z = pos.z })
  local target = tile and tile.getTopCreature and tile:getTopCreature() or nil
  if target then
    g_game.useInventoryItemWith(itemId, target)
    return true
  end
  log("no creature on target tile")
  return false
end

local function walk(dirName, dirValue)
  log("walk " .. dirName)

  if g_game and g_game.walk then
    g_game.walk(dirValue)
    return true
  end

  log("no walk API available")
  return false
end

local function useTileOffset(dx, dy)
  dx = tonumber(dx or 0) or 0
  dy = tonumber(dy or 0) or 0
  log("useTileOffset " .. tostring(dx) .. "," .. tostring(dy))

  if not g_game or not g_map or not g_map.getTile then
    log("no map API available")
    return false
  end

  local player = g_game.getLocalPlayer and g_game.getLocalPlayer() or nil
  local pos = player and player:getPosition() or nil
  if not pos then
    log("no player position available")
    return false
  end

  local tile = g_map.getTile({ x = pos.x + dx, y = pos.y + dy, z = pos.z })
  if not tile or not tile.getTopUseThing then
    log("target tile not found")
    return false
  end

  local thing = nil
  if tile.getThings then
    local things = tile:getThings()
    if things then
      for _, candidate in ipairs(things) do
        if candidate and candidate.getId and candidate:getId() == 1948 then
          thing = candidate
          break
        end
      end
    end
  end
  if not thing and tile.getTopUseThing then
    thing = tile:getTopUseThing()
  end
  if thing then
    if thing.getId then
      log("useTileOffset target id=" .. tostring(thing:getId()))
    end
    g_game.use(thing)
    return true
  end

  log("no use thing on target tile")
  return false
end

local function autoWalkOffset(dx, dy)
  dx = tonumber(dx or 0) or 0
  dy = tonumber(dy or 0) or 0
  log("autoWalkOffset " .. tostring(dx) .. "," .. tostring(dy))

  local player = g_game and g_game.getLocalPlayer and g_game.getLocalPlayer() or nil
  local pos = player and player:getPosition() or nil
  if not pos then
    log("no player position available")
    return false
  end

  local target = { x = pos.x + dx, y = pos.y + dy, z = pos.z }
  if player and player.autoWalk then
    player:autoWalk(target)
    return true
  end
  if autoWalk then
    autoWalk(target)
    return true
  end
  log("no autoWalk API available")
  return false
end

local function inspectSelf(label)
  local player = g_game and g_game.getLocalPlayer and g_game.getLocalPlayer() or nil
  if not player then
    log("inspectSelf no local player")
    return false
  end

  local pos = player:getPosition()
  local name = player.getName and player:getName() or "unknown"
  local hp = player.getHealth and player:getHealth() or "?"
  local maxHp = player.getMaxHealth and player:getMaxHealth() or "?"
  local mana = player.getMana and player:getMana() or "?"
  local maxMana = player.getMaxMana and player:getMaxMana() or "?"
  local level = player.getLevel and player:getLevel() or "?"
  local positionText = pos and (tostring(pos.x) .. "," .. tostring(pos.y) .. "," .. tostring(pos.z)) or "unknown"
  log("inspectSelf label=" .. tostring(label or "state") .. " name=" .. tostring(name) .. " level=" .. tostring(level) .. " hp=" .. tostring(hp) .. "/" .. tostring(maxHp) .. " mana=" .. tostring(mana) .. "/" .. tostring(maxMana) .. " pos=" .. positionText)
  return true
end

local function describeThing(thing)
  if not thing then return "nil" end
  local id = thing.getId and thing:getId() or "?"
  local count = thing.getCount and thing:getCount() or nil
  if count and tonumber(count) and tonumber(count) > 1 then
    return "id=" .. tostring(id) .. "x" .. tostring(count)
  end
  return "id=" .. tostring(id)
end

local function scanTiles(radius)
  radius = tonumber(radius or 1) or 1
  if radius < 0 then radius = 0 end
  if radius > 3 then radius = 3 end
  if not g_game or not g_map or not g_map.getTile then
    log("scanTiles no map API available")
    return false
  end
  local player = g_game.getLocalPlayer and g_game.getLocalPlayer() or nil
  local pos = player and player:getPosition() or nil
  if not pos then
    log("scanTiles no player position available")
    return false
  end
  log("scanTiles radius=" .. tostring(radius) .. " center=" .. tostring(pos.x) .. "," .. tostring(pos.y) .. "," .. tostring(pos.z))
  for dy = -radius, radius do
    for dx = -radius, radius do
      local tile = g_map.getTile({ x = pos.x + dx, y = pos.y + dy, z = pos.z })
      if tile then
        local parts = {}
        if tile.getGround then table.insert(parts, "ground=" .. describeThing(tile:getGround())) end
        if tile.getTopUseThing then table.insert(parts, "topUse=" .. describeThing(tile:getTopUseThing())) end
        if tile.getTopLookThing then table.insert(parts, "topLook=" .. describeThing(tile:getTopLookThing())) end
        log("tile dx=" .. tostring(dx) .. " dy=" .. tostring(dy) .. " " .. table.concat(parts, " "))
      end
    end
  end
  return true
end

local function useTileId(itemId, radius)
  itemId = tonumber(itemId or 0) or 0
  radius = tonumber(radius or 1) or 1
  if radius < 0 then radius = 0 end
  if radius > 5 then radius = 5 end
  log("useTileId itemId=" .. tostring(itemId) .. " radius=" .. tostring(radius))
  if itemId <= 0 or not g_game or not g_map or not g_map.getTile then
    log("useTileId invalid item or missing map API")
    return false
  end
  local player = g_game.getLocalPlayer and g_game.getLocalPlayer() or nil
  local pos = player and player:getPosition() or nil
  if not pos then
    log("useTileId no player position available")
    return false
  end
  for dy = -radius, radius do
    for dx = -radius, radius do
      local tile = g_map.getTile({ x = pos.x + dx, y = pos.y + dy, z = pos.z })
      if tile and tile.getThings then
        local things = tile:getThings()
        if things then
          for _, thing in ipairs(things) do
            if thing and thing.getId and thing:getId() == itemId then
              log("useTileId target dx=" .. tostring(dx) .. " dy=" .. tostring(dy) .. " " .. describeThing(thing))
              g_game.use(thing)
              return true
            end
          end
        end
      end
    end
  end
  log("useTileId target not found")
  return false
end

local function autoWalkTo(x, y, z)
  x = tonumber(x or 0)
  y = tonumber(y or 0)
  z = tonumber(z or 7)
  if not x or not y or not z then
    log("autoWalkTo invalid position")
    return false
  end
  log("autoWalkTo " .. tostring(x) .. "," .. tostring(y) .. "," .. tostring(z))
  local target = { x = x, y = y, z = z }
  local player = g_game and g_game.getLocalPlayer and g_game.getLocalPlayer() or nil
  if player and player.autoWalk then
    player:autoWalk(target)
    return true
  end
  if autoWalk then
    autoWalk(target)
    return true
  end
  log("no autoWalk API available")
  return false
end

local function turn(name)
  local value = directionValue(name)
  log("turn " .. tostring(name))
  if value ~= nil and g_game and g_game.turn then
    g_game.turn(value)
    return true
  end
  log("no turn API available or unknown direction")
  return false
end

local function stopWalk()
  log("stop")
  if g_game and g_game.stop then
    g_game.stop()
    return true
  end
  log("no stop API available")
  return false
end

function directionValue(name)
  if not Directions then return nil end
  local key = string.lower(tostring(name or ""))
  if key == "north" or key == "n" then return Directions.North end
  if key == "east" or key == "e" then return Directions.East end
  if key == "south" or key == "s" then return Directions.South end
  if key == "west" or key == "w" then return Directions.West end
  if key == "northeast" or key == "ne" then return Directions.NorthEast end
  if key == "southeast" or key == "se" then return Directions.SouthEast end
  if key == "southwest" or key == "sw" then return Directions.SouthWest end
  if key == "northwest" or key == "nw" then return Directions.NorthWest end
  return nil
end

local function executeAction(action)
  if type(action) ~= "table" then
    log("invalid action")
    return
  end

  local kind = tostring(action.type or "")
  if kind == "say" then
    say(tostring(action.text or ""))
  elseif kind == "useInventoryItem" then
    useInventoryItem(action.itemId or action.id)
  elseif kind == "useInventoryItemWithSelf" then
    useInventoryItemWithSelf(action.itemId or action.id)
  elseif kind == "useInventoryItemWithTile" then
    useInventoryItemWithTile(action.itemId or action.id, action.dx, action.dy)
  elseif kind == "useTileOffset" then
    useTileOffset(action.dx, action.dy)
  elseif kind == "autoWalkOffset" then
    autoWalkOffset(action.dx, action.dy)
  elseif kind == "autoWalkTo" then
    autoWalkTo(action.x, action.y, action.z)
  elseif kind == "useTileId" then
    useTileId(action.itemId or action.id, action.radius)
  elseif kind == "inspectSelf" then
    inspectSelf(action.label)
  elseif kind == "scanTiles" then
    scanTiles(action.radius)
  elseif kind == "turn" then
    turn(action.direction or action.dir or "east")
  elseif kind == "stop" then
    stopWalk()
  elseif kind == "wait" then
    log("wait " .. tostring(action.afterMs or action.delayAfterMs or 0))
  elseif kind == "walk" then
    local name = action.direction or action.dir or "east"
    local value = directionValue(name)
    if value ~= nil then
      walk(tostring(name), value)
    else
      log("unknown direction " .. tostring(name))
    end
  elseif kind == "log" then
    log("plan-log " .. tostring(action.message or ""))
  else
    log("unknown action type " .. kind)
  end
end

function executePlan(plan)
  if not plan or type(plan.actions) ~= "table" then
    log("plan has no actions")
    return
  end

  local baseDelay = tonumber(plan.firstDelayMs or firstDelayMs) or firstDelayMs
  local cursor = baseDelay
  log("plan start id=" .. tostring(plan.id or "none") .. " actions=" .. tostring(#plan.actions))
  for _, action in ipairs(plan.actions) do
    cursor = cursor + (tonumber(action.beforeMs or action.delayBeforeMs or 0) or 0)
    local scheduledAction = action
    scheduleEvent(function() executeAction(scheduledAction) end, cursor)
    cursor = cursor + (tonumber(action.afterMs or action.delayAfterMs or plan.defaultAfterMs or 900) or 900)
  end
  scheduleEvent(function()
    log("plan complete id=" .. tostring(plan.id or "none"))
  end, cursor + 250)
end

local function readCommandPlan()
  if not g_resources or not g_resources.fileExists or not g_resources.readFileContents then
    return nil
  end
  if not g_resources.fileExists(commandPath) then
    return nil
  end

  local ok, contents = pcall(function() return g_resources.readFileContents(commandPath) end)
  if not ok or not contents or contents:len() == 0 then
    return nil
  end

  local decodedOk, plan = pcall(function() return json.decode(contents) end)
  if not decodedOk or type(plan) ~= "table" then
    log("invalid command json")
    return nil
  end
  return plan
end

local function pollCommandPlan()
  if g_game and g_game.isOnline and g_game.isOnline() then
    local plan = readCommandPlan()
    if plan and plan.enabled ~= false and tostring(plan.id or "") ~= "" and tostring(plan.id) ~= tostring(lastCommandId or "") then
      lastCommandId = tostring(plan.id)
      executePlan(plan)
    end
  end
  scheduleEvent(pollCommandPlan, 1000)
end

local function canTryCharacterLogin()
  if not CharacterList or not CharacterList.doLogin or not CharacterList.isVisible then
    return false
  end

  local ok, visible = pcall(function() return CharacterList.isVisible() end)
  return ok and visible
end

local function shouldRetryLogin()
  if not loginRequested then
    return true
  end

  local now = g_clock and g_clock.millis and g_clock.millis() or 0
  return now > 0 and loginRequestedAt > 0 and now - loginRequestedAt > 15000
end

local function ensureOnline()
  if not g_game or not g_game.isOnline or not g_game.isOnline() then
    if shouldRetryLogin() and EnterGame and EnterGame.quickLogin and autoLoginAccount ~= "" then
      loginRequested = true
      loginRequestedAt = g_clock and g_clock.millis and g_clock.millis() or 0
      characterLoginRequested = false
      log("auto login via Admin quickLogin")
      _G.NewTibia_AdminMode = true
      _G.EnableTibiaAgentControl = true
      EnterGame.quickLogin(autoLoginAccount, autoLoginPassword)
    elseif not characterLoginRequested and canTryCharacterLogin() then
      characterLoginRequested = true
      log("auto character login via CharacterList.doLogin")
      local ok, err = pcall(function() CharacterList.doLogin() end)
      if not ok then
        characterLoginRequested = false
        log("auto character login deferred: " .. tostring(err))
      end
    else
      log("waiting for online game")
    end
    scheduleEvent(ensureOnline, 2000)
    return
  end

  if started then
    return
  end
  started = true
  local player = g_game.getLocalPlayer and g_game.getLocalPlayer() or nil
  local name = player and player:getName() or "unknown"
  log("online as " .. name)
end

function init()
  log("init")
  connect(g_game, {
    onGameStart = ensureOnline,
    onGameEnd = function()
      log("game ended")
      started = false
      loginRequested = false
      loginRequestedAt = 0
      characterLoginRequested = false
    end
  })
  scheduleEvent(ensureOnline, 1000)
  scheduleEvent(pollCommandPlan, 1500)
end

function terminate()
  log("terminate")
  disconnect(g_game, {
    onGameStart = ensureOnline
  })
end
