--[[
  platform.channels — Channel registry for cc-platform-core

  Centralizes modem channel allocation across all platform services.
  Supports controlled migration between legacy and target channels
  via three modes: "current", "target", and "dual".

  Usage:
    local Channels = require('platform.channels')

    -- Get the active channel for a service endpoint
    local ch = Channels.get('inventory.broadcast')  -- => 4200

    -- During migration, listen on both old and new channels
    Channels.setMode('dual')
    local both = Channels.getBoth('remoteturtle.command')  -- => {100, 4210}

    -- Get all channels for a service
    local chs = Channels.forService('inventory')

    -- Register a custom channel
    Channels.register('myservice.data', 5000)
]]

local Config = require('opus.config')

local Channels = {}

--- Default channel allocations.
-- Each entry maps a logical channel name to current and target channel numbers.
-- During migration, 'current' is the legacy channel and 'target' is the new one.
-- When current == target, no migration is needed for that channel.
Channels.registry = {
  -- RemoteTurtle service (legacy: 100-103, target: 4210-4213)
  ['remoteturtle.command']   = { current = 100,  target = 4210 },
  ['remoteturtle.response']  = { current = 101,  target = 4211 },
  ['remoteturtle.status']    = { current = 102,  target = 4212 },
  ['remoteturtle.pocket']    = { current = 103,  target = 4213 },

  -- Inventory Manager service (no migration needed — channels stay the same)
  ['inventory.broadcast']    = { current = 4200, target = 4200 },
  ['inventory.order']        = { current = 4201, target = 4201 },
  ['inventory.client']       = { current = 4202, target = 4202 },
  ['inventory.craft_tx']     = { current = 4203, target = 4203 },
  ['inventory.craft_rx']     = { current = 4204, target = 4204 },
  ['inventory.system']       = { current = 4205, target = 4205 },
  ['inventory.bridge']       = { current = 4206, target = 4206 },

  -- Platform-level shared channels (reserved range: 4250-4259)
  ['platform.discovery']     = { current = 4250, target = 4250 },
  ['platform.heartbeat']     = { current = 4251, target = 4251 },
}

--- Channel mode controls which channel number is returned by get().
-- "current" — use legacy channel numbers (default, safe)
-- "target"  — use new channel numbers (post-migration)
-- "dual"    — listeners open both; senders use target
Channels.mode = 'current'

--- Load channel mode from persistent config if available.
-- Called automatically on require; can be called again to reload.
function Channels.loadConfig()
  local cfg = Config.load('platform', { channelMode = 'current' })
  if cfg.channelMode then
    Channels.mode = cfg.channelMode
  end
  -- Allow per-channel overrides from config
  if cfg.channels then
    for name, entry in pairs(cfg.channels) do
      if type(entry) == 'number' then
        Channels.registry[name] = { current = entry, target = entry }
      elseif type(entry) == 'table' then
        Channels.registry[name] = entry
      end
    end
  end
end

--- Get the active channel number for a logical channel name.
-- In "current" mode, returns the legacy channel.
-- In "target" mode, returns the new channel.
-- In "dual" mode, returns the target channel (for sending).
-- @param name string  Logical channel name (e.g. 'inventory.broadcast')
-- @return number  Channel number
function Channels.get(name)
  local entry = Channels.registry[name]
  if not entry then
    error('Unknown channel: ' .. tostring(name), 2)
  end
  if Channels.mode == 'current' then
    return entry.current
  end
  return entry.target
end

--- Get both current and target channels for a logical name.
-- Returns a deduplicated list. Used for opening channels in dual mode.
-- @param name string  Logical channel name
-- @return table  Array of channel numbers (1 or 2 entries)
function Channels.getBoth(name)
  local entry = Channels.registry[name]
  if not entry then
    error('Unknown channel: ' .. tostring(name), 2)
  end
  if entry.current == entry.target then
    return { entry.current }
  end
  return { entry.current, entry.target }
end

--- Check whether a modem channel number matches a logical channel name.
-- In "current" mode, matches only the legacy channel.
-- In "target" mode, matches only the new channel.
-- In "dual" mode, matches EITHER legacy or new channel.
-- Use this in os.pullEvent("modem_message") handlers for dual-mode safety.
-- @param name string  Logical channel name (e.g. 'remoteturtle.command')
-- @param ch number    Incoming modem channel number to test
-- @return boolean     true if ch is an active channel for this name
function Channels.match(name, ch)
  local entry = Channels.registry[name]
  if not entry then
    error('Unknown channel: ' .. tostring(name), 2)
  end
  if Channels.mode == 'dual' then
    return ch == entry.current or ch == entry.target
  elseif Channels.mode == 'target' then
    return ch == entry.target
  else
    return ch == entry.current
  end
end

--- Set the channel migration mode.
-- @param mode string  One of: "current", "target", "dual"
function Channels.setMode(mode)
  assert(mode == 'current' or mode == 'target' or mode == 'dual',
    'Invalid channel mode: ' .. tostring(mode))
  Channels.mode = mode
end

--- Get all channels for a service prefix.
-- @param prefix string  Service prefix (e.g. 'inventory', 'remoteturtle')
-- @return table  Map of channel name → active channel number
function Channels.forService(prefix)
  local result = {}
  local pattern = '^' .. prefix:gsub('%-', '%%-') .. '%.'
  for name, _ in pairs(Channels.registry) do
    if name:find(pattern) then
      result[name] = Channels.get(name)
    end
  end
  return result
end

--- Get all channels that should be opened for a service (respects dual mode).
-- @param prefix string  Service prefix
-- @return table  Array of unique channel numbers
function Channels.openListForService(prefix)
  local seen = {}
  local result = {}
  local pattern = '^' .. prefix:gsub('%-', '%%-') .. '%.'
  for name, _ in pairs(Channels.registry) do
    if name:find(pattern) then
      for _, ch in ipairs(Channels.getBoth(name)) do
        if not seen[ch] then
          seen[ch] = true
          result[#result + 1] = ch
        end
      end
    end
  end
  table.sort(result)
  return result
end

--- Register a custom channel (for extensions or new services).
-- @param name string  Logical channel name
-- @param channel number|table  Channel number or {current=N, target=N}
function Channels.register(name, channel)
  if type(channel) == 'number' then
    channel = { current = channel, target = channel }
  end
  assert(type(channel) == 'table' and channel.current and channel.target,
    'Channel must be a number or {current=N, target=N}')
  Channels.registry[name] = channel
end

--- Check whether a modem channel belongs to a known service.
-- @param ch number  Channel number
-- @return string|nil  Channel name, or nil if unknown
function Channels.identify(ch)
  for name, entry in pairs(Channels.registry) do
    if entry.current == ch or entry.target == ch then
      return name
    end
  end
  return nil
end

-- Auto-load config on require
pcall(Channels.loadConfig)

return Channels
