local kong_meta = require "kong.meta"
local http = require "resty.http"
local cjson_safe = require "cjson.safe"

local CookieAuth = { PRIORITY = 1260, VERSION = kong_meta.version }

-- Cookie names map
local COOKIE_SID_FIELDS = {
    prod = "account_sid",
    dev = "account_sid_dev",
    staging = "account_sid_staging"
}

-- Check if path matches static resource suffixes
local function is_static_resource(path, suffixes)
    if not suffixes then return false end
    for _, suffix in ipairs(suffixes) do
        if string.match(path, suffix) then
            return true
        end
    end
    return false
end

-- Check if path and method are in whitelist
local function is_whitelisted(path, method, whitelist)
    if not whitelist then return false end
    for _, item in ipairs(whitelist) do
        if ngx.re.match(path, "^" .. item.path .. "$") then
            if not item.methods or #item.methods == 0 then
                return true
            end
            for _, m in ipairs(item.methods) do
                if string.upper(m) == string.upper(method) then
                    return true
                end
            end
        end
    end
    return false
end

-- Check if origin is allowed by whitelist
local function check_origin(conf, request_origin)
    if not conf.cors_allow_origins or #conf.cors_allow_origins == 0 then
        return false
    end

    for _, pattern in ipairs(conf.cors_allow_origins) do
        if ngx.re.match(request_origin, "^" .. pattern .. "$", "jo") then
            kong.log.notice("match pattern: ", pattern, ", trace_id: ",
                kong.ctx.shared.trace_id or "-")
            return true
        end
    end
    return false
end

-- Set CORS headers for allowed origins
local function set_cors_headers(conf)
    local origin = kong.request.get_header("Origin")
    if not origin or not check_origin(conf, origin) then
        kong.log.notice("skip setting CORS because origin not allowed, origin: ", origin, ", trace_id: ",
            kong.ctx.shared.trace_id or "-")
        return
    end

    kong.log.notice("setting CORS for allowed origin: ", origin, ", trace_id: ",
        kong.ctx.shared.trace_id or "-")

    kong.response.set_header("x-cloud-cors-managed-by", "api-gateway")
    kong.response.set_header("Access-Control-Allow-Origin", origin)
    kong.response.set_header("Access-Control-Allow-Credentials", "true")
    kong.response.set_header("Access-Control-Allow-Headers",
        "Content-Type, Authorization, X-Region-Id, X-Biz-Type, X-Org-Id" ..
        ", X-Application-Id, X-Project-Id, X-Current-Project-Id, x-deploy-type, x-provider, x-region" ..
        ", cursor, github_access_token, x-upload-permission, x-upload-org" ..
        ", x-databend-session-id, x-databend-route-hint, x-databendcloud-warehouse, x-databendcloud-org, x-request-id, x-requested-with, x-databend-version")
    kong.response.set_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE")
    kong.response.set_header("Access-Control-Expose-Headers",
        "x-debug-trace-id,x-kong-proxy-latency,x-kong-upstream-latency,x-kong-response-latency,x-pingcap-requestid" ..
        ", x-databend-version, x-databend-route-hint, x-databend-session-id, x-request-id")
end

local function exit_with_error(conf, status, message)
    set_cors_headers(conf)
    kong.response.set_header("Content-Type", "application/json")
    kong.response.exit(status, { message = message })
end

-- Extract value from cookie header by cookie name
local function extract_value_from_cookie(cookie_header, cookie_name)
    if not cookie_header then
        return nil
    end
    -- Match <cookie_name>=<value> format
    local pattern = cookie_name .. "=([^;]+)"
    local val = string.match(cookie_header, pattern)
    return val
end

-- Bypass specific requests such as OPTIONS, static resources, and whitelisted paths
local function is_bypassed(conf, cookie_header)
    local path = kong.request.get_path()
    local method = kong.request.get_method()

    -- TODO: remove this after rollout completes
    -- Check if already authenticated by bearer fallback
    if kong.ctx.shared.has_authed then
        kong.log.notice("skip cookie auth: already authed by fallback, trace_id: ",
            kong.ctx.shared.trace_id or "-")
        return true
    end

    -- Bypass OPTIONS requests (CORS preflight)
    if method == "OPTIONS" then
        kong.log.notice("skip cookie auth: OPTIONS preflight request, trace_id: ",
            kong.ctx.shared.trace_id or "-")
        return true
    end

    -- Bypass static resources
    if is_static_resource(path, conf.static_suffixes) then
        kong.log.notice("skip cookie auth: static resource ", path, ", trace_id: ",
            kong.ctx.shared.trace_id or "-")
        return true
    end

    -- Bypass requests in whitelist
    if is_whitelisted(path, method, conf.whitelist) then
        kong.log.notice("skip cookie auth: whitelisted ", method, " ", path, ", trace_id: ",
            kong.ctx.shared.trace_id or "-")
        return true
    end

    return false
end

function CookieAuth:access(conf)
    local cookie_header = kong.request.get_header("cookie")

    -- Set debug trace id header for all responses
    kong.response.set_header("X-Debug-Trace-Id", kong.ctx.shared.trace_id or "-")

    -- Bypass specific requests
    if is_bypassed(conf, cookie_header) then
        return
    end

    -- Determine the cookie name based on environment
    local env = conf.env_type
    local cookie_name = COOKIE_SID_FIELDS[env]
    if not cookie_name then
        kong.log.notice("Unknown env_type: ", env, ", defaulting to prod cookie name, trace_id: ",
            kong.ctx.shared.trace_id or "-")
        cookie_name = COOKIE_SID_FIELDS["prod"]
    end
    local sid = extract_value_from_cookie(cookie_header, cookie_name)

    -- Auth cookie.sid requests only, and bypass all other requests
    if not sid then
        kong.log.err("cookie auth skipped: no ", cookie_name, " in cookie, trace_id: ",
            kong.ctx.shared.trace_id or "-")
        return
    end

    -- Call auth service to validate session
    local httpc = http.new()
    httpc:set_timeout(conf.timeout)

    local res, err = httpc:request_uri(conf.auth_url, {
        method = "POST",
        headers = {
            ["Content-Type"] = "application/json",
            ["X-Debug-Trace-Id"] = kong.ctx.shared.trace_id or "-"
        },
        body = cjson_safe.encode({ sid = sid })
    })

    if not res then
        kong.log.err("cookie auth: auth service error: ", err, ", trace_id: ",
            kong.ctx.shared.trace_id or "-")
        if err then
            exit_with_error(conf, 500, "Auth service unavailable: " .. err)
        end
        exit_with_error(conf, 500, "Auth service unavailable: empty response")
    end

    kong.log.notice("cookie auth: auth service response: status: ", res.status,
        ", body: ", res.body, ", trace_id: ", kong.ctx.shared.trace_id or "-")

    local body = cjson_safe.decode(res.body)

    if res.status ~= 200 then
        kong.log.err("cookie auth failed: auth service returned ", res.status, ", body: ", res.body, ", trace_id: ",
            kong.ctx.shared.trace_id or "-")

        if body and body.message then
            exit_with_error(conf, 401, "Unauthorized: " .. body.message)
        end
        exit_with_error(conf, 401, "Unauthorized")
    end

    -- Check auth service response
    if not body then
        kong.log.err("cookie auth: invalid auth response: cannot parse JSON, trace_id: ",
            kong.ctx.shared.trace_id or "-")
        exit_with_error(conf, 500, "Invalid auth response: empty body")
    end

    -- Check authentication result
    if not body.ok then
        kong.log.err("cookie auth failed: auth not ok, message: ", body.message or "unknown error", ", trace_id: ",
            kong.ctx.shared.trace_id or "-")
        if body.message then
            exit_with_error(conf, 401, "Unauthorized: " .. body.message)
        end
        exit_with_error(conf, 401, "Unauthorized")
    end

    -- Set headers for upstream services
    if body.auth0_token then
        kong.service.request.set_header("Authorization", "Bearer " .. body.auth0_token)
    end
    if body.user_meta then
        kong.service.request.set_header("account_user_meta", cjson_safe.encode(body.user_meta))
        if type(body.user_meta) == "table" then
            local user_id = body.user_meta.id or body.user_meta.user_id or
                body.user_meta.userId
            if user_id then
                kong.ctx.shared.investibook_user_id = tostring(user_id)
            end
            local org_id = body.user_meta.org_id or body.user_meta.orgId or
                body.user_meta.current_org_id or body.user_meta.currentOrgId
            if org_id then
                kong.ctx.shared.investibook_org_id = tostring(org_id)
            end
        end
    end

    kong.ctx.shared.has_authed = true
    -- kong.service.request.set_header("gw-auth-method", "cookie")
    kong.log.notice("cookie auth success, env: ", env, ", trace_id: ", kong.ctx.shared.trace_id or "-")
end

function CookieAuth:header_filter(conf)
    -- Always verify and enforce CORS headers
    set_cors_headers(conf)
end

return CookieAuth
