import type { Env } from "./bindings.js";
import {
  type AuthContext,
  AuthError,
  AuthSessionCacheSchemaError,
  authenticatedRateLimitKey,
  readAuthContext,
} from "./auth.js";
import { jsonResponse } from "./responses.js";

const RATE_LIMIT_WINDOW_SECONDS = 60;

export type ApiHandler = () => Promise<Response>;
export type AuthenticatedApiHandler = (context: AuthContext) => Promise<Response>;

const APPROVED_DEVICE_QUERY = `
  SELECT device_id
  FROM user_devices
  WHERE user_id = ? AND device_id = ? AND approval_status = 'approved' AND revoked_at IS NULL
`;

export async function withPublicApiControls(
  request: Request,
  env: Env,
  route: string,
  allowedMethods: readonly string[],
  handler: ApiHandler,
): Promise<Response> {
  if (!allowedMethods.includes(request.method)) {
    const response = jsonResponse({ error: "method_not_allowed" }, 405, {
      Allow: allowedMethods.join(", "),
    });
    recordApiAuditEvent(request, env, route, response, "method_not_allowed");
    return response;
  }

  const limit = await env.ELY_RATE_LIMITER.limit({
    key: rateLimitKey(env.ELY_ENVIRONMENT, route),
  });
  if (!limit.success) {
    const response = jsonResponse({ error: "rate_limited" }, 429, {
      "Cache-Control": "no-store",
      "Retry-After": RATE_LIMIT_WINDOW_SECONDS.toString(),
    });
    recordApiAuditEvent(request, env, route, response, "rate_limited");
    return response;
  }

  try {
    const response = await handler();
    recordApiAuditEvent(request, env, route, response, "handled");
    return response;
  } catch (error) {
    recordApiAuditEvent(request, env, route, internalErrorResponse(), "exception");
    throw error;
  }
}

export async function withAuthenticatedApiControls(
  request: Request,
  env: Env,
  route: string,
  allowedMethods: readonly string[],
  handler: AuthenticatedApiHandler,
): Promise<Response> {
  if (!allowedMethods.includes(request.method)) {
    const response = methodNotAllowedResponse(allowedMethods);
    recordApiAuditEvent(request, env, route, response, "method_not_allowed");
    return response;
  }

  const limit = await env.ELY_RATE_LIMITER.limit({
    key: await authenticatedRateLimitKey(env.ELY_ENVIRONMENT, route, request),
  });
  if (!limit.success) {
    const response = rateLimitedResponse();
    recordApiAuditEvent(request, env, route, response, "rate_limited");
    return response;
  }

  let context: AuthContext;
  try {
    context = await readAuthContext(request, env);
  } catch (error) {
    if (error instanceof AuthError) {
      const response = authErrorResponse(error);
      recordApiAuditEvent(request, env, route, response, error.code);
      return response;
    }
    if (error instanceof AuthSessionCacheSchemaError) {
      const response = jsonResponse(
        { error: "auth_session_cache_invalid" },
        500,
        { "Cache-Control": "no-store" },
      );
      recordApiAuditEvent(request, env, route, response, "auth_session_cache_invalid");
      return response;
    }
    throw error;
  }

  try {
    const response = await handler(context);
    recordApiAuditEvent(request, env, route, response, "handled", context);
    return response;
  } catch (error) {
    recordApiAuditEvent(request, env, route, internalErrorResponse(), "exception", context);
    throw error;
  }
}

export async function withApprovedDeviceApiControls(
  request: Request,
  env: Env,
  route: string,
  allowedMethods: readonly string[],
  handler: AuthenticatedApiHandler,
): Promise<Response> {
  return withAuthenticatedApiControls(request, env, route, allowedMethods, async (context) => {
    if (context.deviceId === undefined) {
      return jsonResponse({ error: "device_context_required" }, 403, {
        "Cache-Control": "no-store",
      });
    }

    const row = await env.ELY_DB.prepare(APPROVED_DEVICE_QUERY)
      .bind(context.userId, context.deviceId)
      .first<ApprovedDeviceRow>();
    if (row === null) {
      return jsonResponse({ error: "device_not_approved" }, 403, {
        "Cache-Control": "no-store",
      });
    }

    return handler(context);
  });
}

interface ApprovedDeviceRow {
  device_id: unknown;
}

function rateLimitKey(environment: string, route: string): string {
  return `${environment}:${route}`;
}

function recordApiAuditEvent(
  request: Request,
  env: Env,
  route: string,
  response: Response,
  outcome: string,
  context?: AuthContext,
): void {
  const url = new URL(request.url);
  const blobs = [
    route,
    request.method,
    url.pathname,
    outcome,
    request.headers.get("cf-ray") ?? "",
    request.headers.get("user-agent") ?? "",
  ];
  if (context !== undefined) {
    blobs.push(context.userId, context.deviceId ?? "");
  }

  env.ELY_API_AUDIT.writeDataPoint({
    indexes: [env.ELY_ENVIRONMENT],
    blobs,
    doubles: [response.status, Date.now()],
  });
}

function methodNotAllowedResponse(allowedMethods: readonly string[]): Response {
  return jsonResponse({ error: "method_not_allowed" }, 405, {
    Allow: allowedMethods.join(", "),
  });
}

function rateLimitedResponse(): Response {
  return jsonResponse({ error: "rate_limited" }, 429, {
    "Cache-Control": "no-store",
    "Retry-After": RATE_LIMIT_WINDOW_SECONDS.toString(),
  });
}

function authErrorResponse(error: AuthError): Response {
  return jsonResponse({ error: error.code }, 401, {
    "Cache-Control": "no-store",
    "WWW-Authenticate": "Bearer",
  });
}

function internalErrorResponse(): Response {
  return jsonResponse({ error: "internal_error" }, 500, { "Cache-Control": "no-store" });
}
