local deflate_gzip = require("kong.tools.gzip").deflate_gzip
local ai_plugin_ctx = require("kong.llm.plugin.ctx")

local get_global_ctx, _ = ai_plugin_ctx.get_global_accessors("_base")

-- Our own "phases", to avoid confusion with Kong's phases we use a different name
local STAGES = {
  SETUP = 0,

  REQ_INTROSPECTION = 1,
  REQ_TRANSFORMATION = 2,

  REQ_POST_PROCESSING = 3,
  RES_INTROSPECTION = 4,
  RES_TRANSFORMATION = 5,
  
  STREAMING = 6,
  RES_PRE_PROCESSING = 7, -- specially usage for konnect analytics
  RES_POST_PROCESSING = 8,
}

-- Filters in those stages are allowed to execute more than one time in a request
-- TODO: implement singleton support, that in one iteration of of body_filter only one filter
-- only ran one times. This is not an issue today as they are only used in one plugin.
local REPEATED_PHASES = {
  [STAGES.STREAMING] = true,
}

local MetaPlugin = {}

local all_filters = {}

local function run_stage(stage, sub_plugin, conf)
  local _filters = sub_plugin.filters[stage]
  if not _filters then
    return
  end

  -- if ngx.ctx.ai_executed_filters is not set, meaning we are before access phase
  -- just provide empty table to make following logic happy
  local ai_executed_filters = ngx.ctx.ai_executed_filters or {}

  for _, name in ipairs(_filters) do
    local f = all_filters[name]
    if not f then
      kong.log.err("no filter named '" .. name .. "' registered")

    elseif not ai_executed_filters[name] or REPEATED_PHASES[stage] then
      ai_executed_filters[name] = true

      kong.log.trace("executing filter ", name)

      local ok, err = f:run(conf)
      if not ok then
        kong.log.err("error running filter '", name, "': ", err)
        local phase = ngx.get_phase()
        if phase == "access" or phase == "header_filter" then
          return kong.response.exit(500)
        end
        return ngx.exit(500)
      end
    end
  end
end

function MetaPlugin:init_worker(sub_plugin)
  run_stage(STAGES.SETUP, sub_plugin)
end


function MetaPlugin:configure(sub_plugin, configs)
  run_stage(STAGES.SETUP, sub_plugin, configs)
end

function MetaPlugin:access(sub_plugin, conf)
  ngx.ctx.ai_namespaced_ctx = ngx.ctx.ai_namespaced_ctx or {}
  ngx.ctx.ai_executed_filters = ngx.ctx.ai_executed_filters or {}

  if sub_plugin.enable_balancer_retry then
    kong.service.set_target_retry_callback(function()
      ngx.ctx.ai_executed_filters = {}

      MetaPlugin:retry(sub_plugin, conf)

      return true
    end)
  end

  run_stage(STAGES.REQ_INTROSPECTION, sub_plugin, conf)
  run_stage(STAGES.REQ_TRANSFORMATION, sub_plugin, conf)
end


function MetaPlugin:retry(sub_plugin, conf)
  run_stage(STAGES.REQ_TRANSFORMATION, sub_plugin, conf)
end

function MetaPlugin:rewrite(sub_plugin, conf)
  -- TODO
end

function MetaPlugin:header_filter(sub_plugin, conf)
  run_stage(STAGES.REQ_POST_PROCESSING, sub_plugin, conf)
  -- TODO: order this in better place
  run_stage(STAGES.RES_INTROSPECTION, sub_plugin, conf)
  run_stage(STAGES.RES_TRANSFORMATION, sub_plugin, conf)
end

function MetaPlugin:body_filter(sub_plugin, conf)
  -- check if a response is already sent in access phase by any filter
  local sent, source = get_global_ctx("response_body_sent")
  if sent then
    kong.log.debug("response already sent from source: ", source, " skipping body_filter")
    return
  end

  -- check if we have generated a full body
  local body, source = get_global_ctx("response_body")
  if not get_global_ctx("stream_mode") and body and source ~= ngx.ctx.ai_last_sent_response_source then
    -- now do anything required before the LOG phase
    run_stage(STAGES.RES_PRE_PROCESSING, sub_plugin, conf)

    assert(source, "response_body source not set")

    if get_global_ctx("accept_gzip") then
      body = deflate_gzip(body)
    end

    ngx.arg[1] = body
    ngx.arg[2] = true
    kong.log.debug("sent out response from source: ", source)

    ngx.ctx.ai_last_sent_response_source = source
    return
  end

  -- else run the streaming handler
  run_stage(STAGES.STREAMING, sub_plugin, conf)

  if ngx.arg[2] then  -- streaming has finished
    run_stage(STAGES.RES_PRE_PROCESSING, sub_plugin, conf)
  end
end

function MetaPlugin:log(sub_plugin, conf)
  run_stage(STAGES.RES_POST_PROCESSING, sub_plugin, conf)
end


local _M = {
  STAGES = STAGES,
}

function _M.define(name, priority)
  return setmetatable({
    name = name,
    priority = priority,
    filters = {},
    balancer_retry_enabled = false,
  }, { __index = _M })
end

-- register a filter into the runtime
function _M.register_filter(f)
  if not f or type(f.run) ~= "function" then
    error("expected a filter with a 'run' method", 2)
  end

  local stage = f.STAGE

  if not stage then
    error("expected a filter with a 'STAGE' property", 2)
  end

  if not STAGES[stage] then
    error("unknown stage: " .. stage, 2)
  end

  local filter_name = f.NAME

  if not filter_name then
    error("expected a filter with a 'NAME' property", 2)
  end

  if all_filters[filter_name] then
    return all_filters[filter_name]
  end

  all_filters[filter_name] = f

  return f
end

function _M.has_filter_executed(name)
  return ngx.ctx.ai_executed_filters and ngx.ctx.ai_executed_filters[name]
end

-- enable the filter for current sub plugin
function _M:enable(filter)
  if type(filter) ~= "table" or not filter.NAME then
    error("expected a filter table with a 'NAME' property", 2)
  end

  if not all_filters[filter.NAME] then
    error("unregistered filter: " .. filter.NAME, 2)
  end

  -- the filter has done sanity test when registering

  local stage_id = STAGES[filter.STAGE]

  if not self.filters[stage_id] then
    self.filters[stage_id] = {}
  end

  table.insert(self.filters[stage_id], filter.NAME)
end

function _M:enable_balancer_retry()
  self.balancer_retry_enabled = true
end

function _M:as_kong_plugin()
  local Plugin = {
    PRIORITY = self.priority,
    VERSION = require("kong.meta").version
  }

  if self.filters[STAGES.SETUP] then
    Plugin.init_worker = function(_)
      return MetaPlugin:init_worker(self)
    end

    Plugin.configure = function(_, configs)
      return MetaPlugin:configure(self, configs)
    end
  end

  if self.filters[STAGES.REQ_INTROSPECTION] or self.filters[STAGES.REQ_TRANSFORMATION] then
    Plugin.access = function(_, conf)
      return MetaPlugin:access(self, conf)
    end
  end

  -- TODO: XXX
  -- rewrite = function(_, conf)
  --   return MetaPlugin:rewrite(self, conf)
  -- end,

  if self.filters[STAGES.REQ_POST_PROCESSING] or self.filters[STAGES.RES_INTROSPECTION] or self.filters[STAGES.RES_TRANSFORMATION] then
    Plugin.header_filter = function(_, conf)
      return MetaPlugin:header_filter(self, conf)
    end
  end

  if self.filters[STAGES.STREAMING] then
    Plugin.body_filter = function(_, conf)
      return MetaPlugin:body_filter(self, conf)
    end
  end

  if self.filters[STAGES.RES_POST_PROCESSING] then
    Plugin.log = function(_, conf)
      return MetaPlugin:log(self, conf)
    end
  end

  return Plugin
end

return _M
