--[[
  platform.webbridge — Shared webbridge infrastructure for cc-platform-core

  Provides the common infrastructure pattern used by all services that
  bridge modem communication to an external web server. Extracted from
  the structurally identical patterns in remoteturtle/webbridge.lua and
  Inventory-Manager-CC/inventoryWebBridge.lua.

  Components:
    - Config loading (JSON file with path resolution)
    - HTTP helpers (POST/GET with error handling)
    - WebSocket connection manager (connect, reconnect, keepalive)
    - HTTP poll→process→ack cycle (fallback transport)
    - Modem channel management
    - parallel.waitForAny main loop skeleton

  Usage:
    local WebBridge = require('platform.webbridge')

    local config = WebBridge.loadConfig({
      serverUrl = 'http://localhost:3001',
    }, {
      'usr/config/myservice/.webbridge_config',
      '.webbridge_config',
    })

    -- HTTP helpers
    local body = WebBridge.httpGet(url, headers)
    local resp = WebBridge.httpPost(url, data, headers)

    -- WebSocket with auto-reconnect
    WebBridge.wsConnect(wsUrl, {
      onConnect    = function(ws) ... end,
      onMessage    = function(ws, data) ... end,
      onDisconnect = function() ... end,
    })
]]

local textutils  = _G.textutils
local http       = _G.http
local fs         = _G.fs
local os         = _G.os
local peripheral = _G.peripheral

local Channels = require('platform.channels')

local WebBridge = {}

---------------------------------------------------------------------------
-- Config loading
---------------------------------------------------------------------------

--- Load a JSON config file, trying multiple paths in order.
-- Merges found config over the provided defaults.
-- @param defaults table      Default config values
-- @param configPaths table   Array of file paths to try (first match wins)
-- @return table  Merged config
-- @return string|nil  Path of the file that was loaded, or nil if none found
function WebBridge.loadConfig(defaults, configPaths)
  defaults = defaults or {}
  configPaths = configPaths or {}

  for _, path in ipairs(configPaths) do
    if fs.exists(path) then
      local f = fs.open(path, 'r')
      if f then
        local raw = f.readAll()
        f.close()
        local ok, cfg = pcall(textutils.unserialiseJSON, raw)
        if ok and type(cfg) == 'table' then
          for k, v in pairs(cfg) do
            defaults[k] = v
          end
          return defaults, path
        end
      end
    end
  end

  return defaults, nil
end

--- Save a config table to a JSON file.
-- Creates parent directories if needed.
-- @param path string  File path
-- @param config table  Config to save
-- @return boolean  true on success
function WebBridge.saveConfig(path, config)
  local dir = fs.getDir(path)
  if dir and dir ~= '' and not fs.isDir(dir) then
    fs.makeDir(dir)
  end

  local ok = pcall(function()
    local f = fs.open(path, 'w')
    f.write(textutils.serialiseJSON(config))
    f.close()
  end)

  return ok
end

---------------------------------------------------------------------------
-- HTTP helpers
---------------------------------------------------------------------------

--- Perform an HTTP POST with JSON body.
-- @param url string       Target URL
-- @param data table       Data to serialize as JSON
-- @param headers table|nil  Additional headers
-- @return string|nil  Response body, or nil on error
-- @return string|nil  Error message on failure
function WebBridge.httpPost(url, data, headers)
  headers = headers or {}
  headers['Content-Type'] = headers['Content-Type'] or 'application/json'

  local body = textutils.serialiseJSON(data)
  local ok, result = pcall(function()
    local response = http.post(url, body, headers)
    if response then
      local responseBody = response.readAll()
      response.close()
      return responseBody
    end
    return nil
  end)

  if not ok then
    return nil, tostring(result)
  end
  return result
end

--- Perform an HTTP GET request.
-- @param url string       Target URL
-- @param headers table|nil  Additional headers
-- @return string|nil  Response body, or nil on error
-- @return string|nil  Error message on failure
function WebBridge.httpGet(url, headers)
  local ok, result = pcall(function()
    local response = http.get(url, headers)
    if response then
      local body = response.readAll()
      response.close()
      return body
    end
    return nil
  end)

  if not ok then
    return nil, tostring(result)
  end
  return result
end

--- Build authorization headers for API key authentication.
-- Returns an empty table if no key is provided.
-- @param apiKey string|nil  API key
-- @return table  Headers table
function WebBridge.authHeaders(apiKey)
  if apiKey and #apiKey > 0 then
    return { ['Authorization'] = 'Bearer ' .. apiKey }
  end
  return {}
end

---------------------------------------------------------------------------
-- Modem management
---------------------------------------------------------------------------

--- Find a modem peripheral.
-- Prefers the Opus device registry, falls back to peripheral scan.
-- @param preferWireless boolean|nil  If true, prefer wireless modem
-- @return table|nil  Modem peripheral handle
-- @return string|nil  Peripheral name/side
function WebBridge.findModem(preferWireless)
  -- Try Opus device registry first
  if _G.device then
    if preferWireless and _G.device.wireless_modem then
      return _G.device.wireless_modem, 'wireless_modem'
    end
    -- Find any modem in device registry
    for name, dev in pairs(_G.device) do
      if dev.type == 'modem' or dev.type == 'wireless_modem' or dev.type == 'wired_modem' then
        return dev, name
      end
    end
  end

  -- Fallback: scan peripherals
  for _, name in ipairs(peripheral.getNames()) do
    if peripheral.getType(name) == 'modem' then
      return peripheral.wrap(name), name
    end
  end

  return nil
end

--- Open a list of named channels on a modem.
-- Respects dual-mode migration (opens both current and target channels).
-- @param modem table        Modem peripheral handle
-- @param channelNames table  Array of logical channel names
function WebBridge.openChannels(modem, channelNames)
  for _, name in ipairs(channelNames) do
    for _, ch in ipairs(Channels.getBoth(name)) do
      modem.open(ch)
    end
  end
end

---------------------------------------------------------------------------
-- WebSocket connection manager
---------------------------------------------------------------------------

--- Manage a persistent WebSocket connection with auto-reconnect.
-- Blocks indefinitely, reconnecting on failure with a configurable delay.
-- Handles ping/pong keepalive automatically.
-- @param wsUrl string     WebSocket URL
-- @param handlers table   Callbacks:
--   onConnect(ws)          Called when connection is established
--   onMessage(ws, data)    Called for each parsed JSON message (except pong)
--   onDisconnect()         Called when connection drops
--   onError(err)           Called on connection error (optional)
-- @param opts table|nil   Options:
--   reconnectDelay number  Seconds between reconnect attempts (default 5)
--   receiveTimeout number  Seconds before ping on idle (default 30)
function WebBridge.wsConnect(wsUrl, handlers, opts)
  handlers = handlers or {}
  opts = opts or {}
  local reconnectDelay = opts.reconnectDelay or 5
  local receiveTimeout = opts.receiveTimeout or 30

  while true do
    local ok, ws = pcall(http.websocket, wsUrl)

    if ok and ws then
      if handlers.onConnect then
        handlers.onConnect(ws)
      end

      -- Message receive loop
      while true do
        local rok, msg = pcall(ws.receive, receiveTimeout)

        if not rok then
          -- Connection error
          break
        end

        if not msg then
          -- Timeout — send keepalive ping
          local pok = pcall(ws.send, textutils.serialiseJSON({ type = 'ping' }))
          if not pok then break end
        else
          local data = textutils.unserialiseJSON(msg)
          if data then
            if data.type == 'pong' then
              -- Keepalive response, handled
            elseif handlers.onMessage then
              handlers.onMessage(ws, data)
            end
          end
        end
      end

      pcall(ws.close)

      if handlers.onDisconnect then
        handlers.onDisconnect()
      end
    else
      if handlers.onError then
        handlers.onError(ws) -- ws contains the error string on failure
      end
    end

    os.sleep(reconnectDelay)
  end
end

---------------------------------------------------------------------------
-- HTTP poll→process→ack cycle (WebSocket fallback transport)
---------------------------------------------------------------------------

--- Run a poll→process→ack loop for HTTP-based command polling.
-- Blocks indefinitely, polling at the specified interval.
-- Uses monotonic ID-based acknowledgment for deduplication safety.
-- @param opts table  Options:
--   pollUrl     string     URL to GET pending commands
--   ackUrl      string     URL to POST acknowledgment
--   headers     table      Auth/custom headers
--   interval    number     Poll interval in seconds (default 1)
--   onCommand   function   Called for each received command
--   shouldPoll  function   Optional gate — return false to skip this cycle
function WebBridge.pollCommands(opts)
  local pollUrl    = opts.pollUrl
  local ackUrl     = opts.ackUrl
  local headers    = opts.headers or {}
  local interval   = opts.interval or 1
  local onCommand  = opts.onCommand
  local shouldPoll = opts.shouldPoll
  local lastId     = 0

  while true do
    if not shouldPoll or shouldPoll() then
      local body = WebBridge.httpGet(pollUrl, headers)
      if body then
        local ok, commands = pcall(textutils.unserialiseJSON, body)
        if ok and type(commands) == 'table' then
          for _, cmd in ipairs(commands) do
            if onCommand then
              onCommand(cmd)
            end
            -- Track highest processed ID for dedup-safe ack
            if cmd.id and cmd.id > lastId then
              lastId = cmd.id
            end
          end

          if #commands > 0 and ackUrl then
            WebBridge.httpPost(ackUrl, { lastProcessedId = lastId }, headers)
          end
        end
      end
    end

    os.sleep(interval)
  end
end

return WebBridge
