local http_client = require "kong.plugins.o11y-proxy.http_client"
local cjson = require "cjson.safe"
local lrucache = require "resty.lrucache"
local resty_lock = require "resty.lock"

local Router = {}

-- Cache for routing data
-- Max 10000 entries, 5 minutes TTL (routing config is relatively stable)
local route_cache, cache_err = lrucache.new(10000)
if not route_cache then
  error("Failed to create route_cache: " .. (cache_err or "unknown error"))
end

-- Internal function to query router service via HTTP.
-- Either logical_region OR (region_id, org_id, biz_type) must be supplied.
-- When logical_region is non-empty it is sent as a query parameter and the
-- three-header validation is skipped.
local function query_router_http(router_url, logical_region, region_id, org_id, biz_type, project_id, application_id, timeout)
  local httpc = http_client.new({
    timeout = timeout or 5000,  -- Default 5 seconds for internal service
    max_retries = 2,
    retry_delay = 50,
  })

  local url = router_url
  local headers = {}

  if logical_region and logical_region ~= "" then
    -- Append as query parameter; preserve any existing query string.
    local escaped_logical_region = ngx.escape_uri(logical_region)
    if url:find("?", 1, true) then
      url = url .. "&logical_region=" .. escaped_logical_region
    else
      url = url .. "?logical_region=" .. escaped_logical_region
    end
    headers["X-Logical-Region"] = logical_region
    headers["X-Region-Id"] = region_id
  else
    headers["X-Region-Id"] = region_id
    headers["X-Org-Id"] = org_id
    headers["X-Biz-Type"] = biz_type
  end

  -- Forward 4-segment routing headers so GS can do project-level
  -- and child→parent cluster resolution.
  if project_id and project_id ~= "" then
    headers["X-Project-Id"] = project_id
  end
  if application_id and application_id ~= "" then
    headers["X-Application-Id"] = application_id
  end

  local res, err = httpc:request_uri(url, {
    method = "GET",
    headers = headers,
  })

  if err or not res then
    return nil, "Failed to query router service: " .. (err or "unknown error")
  end

  if res.status ~= 200 then
    return nil, "Router service returned error status: " .. res.status
  end

  local body, decode_err = cjson.decode(res.body)
  if decode_err or not body then
    return nil, "Failed to decode router response: " .. (decode_err or "unknown error")
  end

  if body.code ~= 0 then
    return nil, "Router service returned error: " .. (body.message or "unknown error")
  end

  if not body.data then
    return nil, "Router service response missing data"
  end

  if not body.data.dataplaneAddress then
    return nil, "Router service response missing dataplaneAddress"
  end

  -- Return the complete route data including credentials information
  return {
    dns_address = body.data.dataplaneAddress,
    access_principal = body.data.accessPrincipal,
    access_external_id = body.data.accessExternalID,
    logical_region_name = body.data.logicalRegionName,
  }, nil
end

-- Query router service to get routing information.
-- Parameters:
--   router_url:      The full URL of the router service.
--   logical_region:  Optional. Logical region name. When non-empty, region_id is still required
--                    for scoping the GS lookup; (org_id, biz_type) are ignored.
--   region_id:       Always required. Scopes the lookup to a specific vendor-region.
--   org_id:          Required when logical_region is empty.
--   biz_type:        Required when logical_region is empty.
--   timeout:         Optional timeout in ms (default: 5000).
--   cache_ttl:       Optional cache TTL in seconds (default: 300, 0 disables).
function Router.query_target(router_url, logical_region, region_id, org_id, biz_type, project_id, application_id, timeout, cache_ttl)
  if not router_url or router_url == "" then
    return nil, "Router service URL is not configured"
  end

  local has_logical = logical_region and logical_region ~= ""
  if not region_id or region_id == "" then
    return nil, "X-Region-Id header is required"
  end
  if not has_logical then
    if not org_id or org_id == "" then
      return nil, "X-Org-Id header is required when X-Logical-Region is not set"
    end
    if not biz_type or biz_type == "" then
      return nil, "X-Biz-Type header is required when X-Logical-Region is not set"
    end
  end

  -- Default cache TTL is 300 seconds (5 minutes)
  cache_ttl = cache_ttl or 300

  -- Normalize optional dims for cache key (empty string when absent)
  local pid = project_id or ""
  local aid = application_id or ""

  local cache_key
  if has_logical then
    cache_key = "logical:" .. region_id .. ":" .. logical_region
  else
    cache_key = "region:" .. region_id .. ":" .. org_id .. ":" .. biz_type .. ":" .. pid .. ":" .. aid
  end

  -- First check: return cached data if valid (fast path)
  if cache_ttl > 0 then
    local cached = route_cache:get(cache_key)
    if cached then
      return cached, nil
    end
  end

  -- Cache miss, acquire lock to query router service
  local lock_key = "router:" .. cache_key
  local lock, lock_err = resty_lock:new("o11y_proxy_locks", {
    timeout = 5,
    exptime = 10,
  })

  if not lock then
    kong.log.warn("Failed to create lock, querying without lock: ", lock_err)
  else
    local elapsed, err = lock:lock(lock_key)
    if not elapsed then
      kong.log.warn("Failed to acquire lock, querying without lock: ", err)
    else
      -- Double-check: another request might have populated cache
      local cached = route_cache:get(cache_key)
      if cached then
        lock:unlock()
        return cached, nil
      end
    end
  end

  local route_data, query_err = query_router_http(router_url, logical_region, region_id, org_id, biz_type, project_id, application_id, timeout)
  if query_err or not route_data then
    if lock then lock:unlock() end
    return nil, query_err
  end

  -- Cache the result with configured TTL
  if cache_ttl > 0 then
    route_cache:set(cache_key, route_data, cache_ttl)
  end

  if lock then lock:unlock() end
  return route_data, nil
end

return Router
