local EnsureApiRegistry = rawget(_G, "LIDCOUNCIL_EnsureApiRegistry")
local API, INTERNAL_API
if type(EnsureApiRegistry) == "function" then
  API, INTERNAL_API = EnsureApiRegistry()
else
  API = rawget(_G, "LIDCOUNCIL_API")
  if type(API) ~= "table" then
    API = {}
    _G.LIDCOUNCIL_API = API
  end
  if type(API.__internal) ~= "table" then
    API.__internal = {}
  end
  INTERNAL_API = API.__internal
end

local COMBAT_ACTION = {
  IMMEDIATE = "immediate",
  ASSEMBLE_ONLY = "assemble_only",
  STAGE_DATA = "stage_data",
  REJECT = "reject",
}

local UI_ACTION = {
  NONE = "none",
  DEFER = "defer",
}

local ACK_PHASE = {
  QUEUED = "QUEUED",
  APPLIED = "APPLIED",
}

local opcodePolicies = {}
local registrationProblems = {}

local function TrimText(value)
  return tostring(value or ""):match("^%s*(.-)%s*$")
end

local function NormalizeOpcode(value)
  return string.upper(TrimText(value))
end

local function NormalizeAckPhase(value)
  local phase = string.upper(TrimText(value))
  if phase == "" then
    return ACK_PHASE.APPLIED
  end
  if phase == ACK_PHASE.QUEUED or phase == "RECEIVED_QUEUED" or phase == "WAITING_COMBAT" then
    return ACK_PHASE.QUEUED
  end
  if phase == ACK_PHASE.APPLIED then
    return ACK_PHASE.APPLIED
  end
  return ""
end

local function ClonePolicy(policy)
  if type(policy) ~= "table" then
    return nil
  end
  return {
    opcode = policy.opcode,
    category = policy.category,
    combatAction = policy.combatAction,
    uiAction = policy.uiAction,
    allowTransport = policy.allowTransport == true,
  }
end

local function RegisterPolicyList(opcodes, category, combatAction, uiAction)
  for _, opcode in ipairs(type(opcodes) == "table" and opcodes or {}) do
    local normalizedOpcode = NormalizeOpcode(opcode)
    if normalizedOpcode == "" then
      registrationProblems[#registrationProblems + 1] = "empty_opcode"
    elseif type(opcodePolicies[normalizedOpcode]) == "table" then
      registrationProblems[#registrationProblems + 1] = "duplicate_opcode:" .. normalizedOpcode
    else
      opcodePolicies[normalizedOpcode] = {
        opcode = normalizedOpcode,
        category = TrimText(category),
        combatAction = combatAction,
        uiAction = uiAction,
        allowTransport = true,
      }
    end
  end
end

-- Session lifecycle state must keep moving so joins, leaves, and host changes do not
-- time out while one client is combat-locked. Any resulting UI work is deferred.
RegisterPolicyList({
  "START",
  "IACK",
  "BEGIN",
  "JOIN",
  "JACK",
  "MEMBER",
  "LOCALAP",
  "APNOTICE",
  "DECLINE",
  "LEAVE",
  "PSTATE",
  "PSTATE_PART",
  "STOP",
}, "session_control", COMBAT_ACTION.IMMEDIATE, UI_ACTION.DEFER)

-- Snapshot chunks may be validated and assembled in combat. Only the completed
-- payload application represented by PSYNC_END belongs in the future combat inbox.
RegisterPolicyList({
  "PSYNC_START",
  "PSYNC_CHUNK",
}, "profile_snapshot_transport", COMBAT_ACTION.ASSEMBLE_ONLY, UI_ACTION.DEFER)

RegisterPolicyList({
  "PSYNC_END",
}, "profile_snapshot_apply", COMBAT_ACTION.STAGE_DATA, UI_ACTION.DEFER)

RegisterPolicyList({
  "PSYNC_REQ",
  "PSYNC_ACK",
  "PSUB_REQ",
  "PSUB_STATE",
  "PSUBPATCH_REQ",
  "PSUBPATCH_STATE",
  "PDELTA_ACK",
  "PDELTA_NACK",
}, "profile_sync_control", COMBAT_ACTION.IMMEDIATE, UI_ACTION.DEFER)

RegisterPolicyList({
  "PDELTA_REQ",
  "PDELTA",
}, "profile_delta_apply", COMBAT_ACTION.STAGE_DATA, UI_ACTION.DEFER)

-- Roll-session traffic remains operational in combat. This data is required for
-- the active raid workflow and is separate from profile/configuration alignment.
RegisterPolicyList({
  "CHOICE",
  "RCHOICE",
  "ITEM_REPORT",
  "ITEM_OPEN",
  "AWARD",
  "UNASSIGN",
  "TRADE",
  "VOTE",
}, "live_session_operation", COMBAT_ACTION.IMMEDIATE, UI_ACTION.DEFER)

RegisterPolicyList({
  "LL_ADD_REQ",
  "LL_ADD",
  "LL_DEL_REQ",
  "LL_DEL",
  "LL_MA_REQ",
  "LL_MA",
  "LL_NOTE_REQ",
  "LL_NOTE",
  "LL_CHAR_DEL_REQ",
  "LL_CHAR_DEL",
}, "loot_ledger_apply", COMBAT_ACTION.STAGE_DATA, UI_ACTION.DEFER)

RegisterPolicyList({
  "BSYNC_START",
  "BSYNC_ITEM",
  "BSYNC_SEL",
  "BSYNC_VOTE",
  "BSYNC_END",
}, "live_board_snapshot_apply", COMBAT_ACTION.STAGE_DATA, UI_ACTION.DEFER)

RegisterPolicyList({
  "BSYNC_MISS",
  "BSYNC_ACK",
  "ITEM_ACK",
  "RCHOICE_ACK",
}, "live_board_snapshot_receipt", COMBAT_ACTION.IMMEDIATE, UI_ACTION.NONE)

local function IsCombatLocked(combatLockedOverride)
  if type(combatLockedOverride) == "boolean" then
    return combatLockedOverride
  end
  return type(InCombatLockdown) == "function" and InCombatLockdown() and true or false
end

local function GetOpcodePolicy(opcode)
  return ClonePolicy(opcodePolicies[NormalizeOpcode(opcode)])
end

local function ResolveCombatDecision(opcode, opts)
  opts = type(opts) == "table" and opts or {}
  local normalizedOpcode = NormalizeOpcode(opcode)
  local policy = opcodePolicies[normalizedOpcode]
  local combatLocked = IsCombatLocked(opts.combatLocked)

  if type(policy) ~= "table" then
    return {
      opcode = normalizedOpcode,
      known = false,
      combatLocked = combatLocked,
      allowTransport = false,
      combatAction = COMBAT_ACTION.REJECT,
      deferUi = combatLocked,
      stageData = false,
      assembleOnly = false,
      reason = "unknown_opcode",
    }
  end

  local combatAction = combatLocked and policy.combatAction or COMBAT_ACTION.IMMEDIATE
  return {
    opcode = normalizedOpcode,
    category = policy.category,
    known = true,
    combatLocked = combatLocked,
    allowTransport = policy.allowTransport == true,
    combatAction = combatAction,
    deferUi = combatLocked and policy.uiAction == UI_ACTION.DEFER,
    stageData = combatLocked and combatAction == COMBAT_ACTION.STAGE_DATA,
    assembleOnly = combatLocked and combatAction == COMBAT_ACTION.ASSEMBLE_ONLY,
    reason = combatLocked and "combat_policy" or "out_of_combat",
  }
end

local function GetPolicySnapshot()
  local snapshot = {}
  for _, policy in pairs(opcodePolicies) do
    snapshot[#snapshot + 1] = ClonePolicy(policy)
  end
  table.sort(snapshot, function(left, right)
    return tostring(left.opcode or "") < tostring(right.opcode or "")
  end)
  return snapshot
end

local function ValidatePolicyCoverage(opcodes)
  local missing = {}
  local seen = {}
  if type(opcodes) == "table" then
    for key, value in pairs(opcodes) do
      local opcode = type(key) == "string" and key or value
      local normalizedOpcode = NormalizeOpcode(opcode)
      if normalizedOpcode ~= "" and not seen[normalizedOpcode] then
        seen[normalizedOpcode] = true
        if type(opcodePolicies[normalizedOpcode]) ~= "table" then
          missing[#missing + 1] = normalizedOpcode
        end
      end
    end
  end
  table.sort(missing)
  return #missing == 0, missing
end

local function RunPolicyValidation()
  local problems = {}
  local counts = {
    immediate = 0,
    assembleOnly = 0,
    stageData = 0,
    total = 0,
  }
  local validCombatActions = {
    [COMBAT_ACTION.IMMEDIATE] = true,
    [COMBAT_ACTION.ASSEMBLE_ONLY] = true,
    [COMBAT_ACTION.STAGE_DATA] = true,
  }
  local validUiActions = {
    [UI_ACTION.NONE] = true,
    [UI_ACTION.DEFER] = true,
  }

  for _, problem in ipairs(registrationProblems) do
    problems[#problems + 1] = problem
  end
  for opcode, policy in pairs(opcodePolicies) do
    counts.total = counts.total + 1
    if not validCombatActions[policy.combatAction] then
      problems[#problems + 1] = "invalid_combat_action:" .. opcode
    elseif policy.combatAction == COMBAT_ACTION.IMMEDIATE then
      counts.immediate = counts.immediate + 1
    elseif policy.combatAction == COMBAT_ACTION.ASSEMBLE_ONLY then
      counts.assembleOnly = counts.assembleOnly + 1
    elseif policy.combatAction == COMBAT_ACTION.STAGE_DATA then
      counts.stageData = counts.stageData + 1
    end
    if not validUiActions[policy.uiAction] then
      problems[#problems + 1] = "invalid_ui_action:" .. opcode
    end
    if policy.allowTransport ~= true then
      problems[#problems + 1] = "transport_blocked:" .. opcode
    end
  end

  local requiredActions = {
    PSYNC_CHUNK = COMBAT_ACTION.ASSEMBLE_ONLY,
    PSYNC_END = COMBAT_ACTION.STAGE_DATA,
    PDELTA_REQ = COMBAT_ACTION.STAGE_DATA,
    PDELTA = COMBAT_ACTION.STAGE_DATA,
    CHOICE = COMBAT_ACTION.IMMEDIATE,
    LL_ADD = COMBAT_ACTION.STAGE_DATA,
    BSYNC_END = COMBAT_ACTION.STAGE_DATA,
  }
  for opcode, expectedAction in pairs(requiredActions) do
    local policy = opcodePolicies[opcode]
    if type(policy) ~= "table" or policy.combatAction ~= expectedAction then
      problems[#problems + 1] = "required_action_mismatch:" .. opcode
    end
  end

  table.sort(problems)
  return {
    valid = #problems == 0,
    problems = problems,
    counts = counts,
  }
end

local function RunAsymmetricCombatValidation()
  local checks = {}
  local passedCount = 0
  local function AddCheck(name, expected, actual)
    local passed = expected == actual
    checks[#checks + 1] = {
      name = name,
      passed = passed,
      expected = tostring(expected),
      actual = tostring(actual),
    }
    if passed then
      passedCount = passedCount + 1
    end
  end

  local scenarios = {
    {
      name = "host combat to member free",
      recipientCombat = false,
      expectedAction = COMBAT_ACTION.IMMEDIATE,
      expectedUiDeferred = false,
    },
    {
      name = "host free to member combat",
      recipientCombat = true,
      expectedAction = COMBAT_ACTION.STAGE_DATA,
      expectedUiDeferred = true,
    },
    {
      name = "member combat to host free",
      recipientCombat = false,
      expectedAction = COMBAT_ACTION.IMMEDIATE,
      expectedUiDeferred = false,
    },
    {
      name = "member free to host combat",
      recipientCombat = true,
      expectedAction = COMBAT_ACTION.STAGE_DATA,
      expectedUiDeferred = true,
    },
  }
  for _, scenario in ipairs(scenarios) do
    local decision = ResolveCombatDecision("PSYNC_END", {
      combatLocked = scenario.recipientCombat,
    })
    AddCheck(scenario.name .. " keeps transport available", true, decision.allowTransport == true)
    AddCheck(scenario.name .. " resolves receiver data action", scenario.expectedAction, decision.combatAction)
    AddCheck(scenario.name .. " resolves receiver UI action", scenario.expectedUiDeferred, decision.deferUi == true)
  end

  local combatChunk = ResolveCombatDecision("PSYNC_CHUNK", { combatLocked = true })
  AddCheck("combat receiver assembles snapshot chunks", COMBAT_ACTION.ASSEMBLE_ONLY, combatChunk.combatAction)
  AddCheck("combat receiver does not apply snapshot chunks", false, combatChunk.stageData == true)

  local combatLiveOperation = ResolveCombatDecision("ITEM_OPEN", { combatLocked = true })
  AddCheck("combat live operation remains immediate", COMBAT_ACTION.IMMEDIATE, combatLiveOperation.combatAction)
  AddCheck("combat live operation defers presentation", true, combatLiveOperation.deferUi == true)

  return passedCount == #checks, {
    suite = "combat_sync_asymmetric",
    version = 1,
    total = #checks,
    passed = passedCount,
    failed = #checks - passedCount,
    checks = checks,
  }
end

INTERNAL_API.LIDCOUNCIL_GetCombatSyncOpcodePolicy_Impl = GetOpcodePolicy
INTERNAL_API.LIDCOUNCIL_ResolveCombatSyncDecision_Impl = ResolveCombatDecision
INTERNAL_API.LIDCOUNCIL_GetCombatSyncPolicySnapshot_Impl = GetPolicySnapshot
INTERNAL_API.LIDCOUNCIL_ValidateCombatSyncPolicyCoverage_Impl = ValidatePolicyCoverage
INTERNAL_API.LIDCOUNCIL_RunCombatSyncPolicyValidation_Impl = RunPolicyValidation
INTERNAL_API.LIDCOUNCIL_RunAsymmetricCombatSyncValidation_Impl = RunAsymmetricCombatValidation
INTERNAL_API.LIDCOUNCIL_NormalizeCombatSyncAckPhase_Impl = NormalizeAckPhase
