--[[
  platform.protocol — Canonical message envelope for cc-platform-core

  Provides a standard message format that is backward-compatible with
  existing modem messages. All messages retain the `type` field as the
  primary discriminator. New metadata fields (_v, _src, _ts, _id) are
  additive — legacy consumers ignore them.

  Usage:
    local Protocol = require('platform.protocol')
    Protocol.setServiceId('inventory-manager')

    -- Create a well-formed message
    local msg = Protocol.message('state', { items = {...}, total = 42 })
    -- => { type='state', items={...}, total=42, _v=1, _src='inventory-manager', _ts=... }

    -- Parse incoming message (works with both legacy and versioned)
    local parsed = Protocol.parse(msg)
    -- => { type='state', version=1, source='inventory-manager', timestamp=..., data=msg }

    -- Quick type check
    if Protocol.is(msg, 'status') then ... end
]]

local os = _G.os

local Protocol = {
  VERSION = 1,
  serviceId = nil,
}

--- Set the service identity for this process.
-- All messages created after this call will include the service ID.
-- @param id string  Service identifier (e.g. 'remoteturtle', 'inventory-manager')
function Protocol.setServiceId(id)
  Protocol.serviceId = id
end

--- Create a well-formed platform message.
-- Merges data fields into a new table and adds protocol metadata.
-- The `type` field is always set from the first argument.
-- @param msgType string  Message type discriminator
-- @param data table|nil  Service-specific payload fields (merged into message)
-- @param opts table|nil  Options: { src=string, id=string, noMeta=bool }
-- @return table  The constructed message
function Protocol.message(msgType, data, opts)
  opts = opts or {}

  local msg = {}

  -- Merge data fields into the message (flat, not nested)
  if data then
    for k, v in pairs(data) do
      msg[k] = v
    end
  end

  -- Primary discriminator
  msg.type = msgType

  -- Protocol metadata (additive — legacy consumers ignore these)
  if not opts.noMeta then
    msg._v   = Protocol.VERSION
    msg._src = opts.src or Protocol.serviceId
    msg._ts  = os.epoch and os.epoch('utc') or math.floor(os.clock() * 1000)
    if opts.id then
      msg._id = opts.id
    end
  end

  return msg
end

--- Parse a received message, normalizing both legacy and versioned formats.
-- Returns nil if the input is not a valid message table.
-- @param msg table  Raw message from modem or WebSocket
-- @return table|nil  Normalized message descriptor
function Protocol.parse(msg)
  if type(msg) ~= 'table' then
    return nil
  end
  if not msg.type then
    return nil
  end

  return {
    type          = msg.type,
    version       = msg._v or 0,
    source        = msg._src,
    timestamp     = msg._ts,
    correlationId = msg._id,
    data          = msg,      -- full original message for backward-compat field access
  }
end

--- Check whether a message matches a given type.
-- Works on both raw messages and parsed descriptors.
-- @param msg table  Message or parsed descriptor
-- @param msgType string  Expected type
-- @return boolean
function Protocol.is(msg, msgType)
  if type(msg) ~= 'table' then
    return false
  end
  return msg.type == msgType
end

--- Generate a correlation ID for request/response patterns.
-- Uses a combination of computer ID, clock, and random to ensure uniqueness.
-- @return string  Unique correlation ID
function Protocol.correlationId()
  local id = os.getComputerID and os.getComputerID() or 0
  local clock = os.epoch and os.epoch('utc') or os.clock()
  local rand = math.random(0, 0xFFFF)
  return string.format('%d-%d-%04x', id, clock, rand)
end

return Protocol
