import "server-only";
import {
  NodeOAuthClient,
  OAuthClientMetadataInput,
  Keyset,
  JoseKey,
} from "@atproto/oauth-client-node";
import {
  AuthorizationServerMetadataCache,
  ProtectedResourceMetadataCache,
} from "@atproto/oauth-client";
import { SimpleStoreMemory } from "@atproto-labs/simple-store-memory";
import { StateStore, SessionStore, initAuthTables, requestLock } from "./storage";

// Cache OAuth server metadata for 24 hours (default is 60 seconds)
// This metadata rarely changes - Bluesky would announce OAuth infrastructure changes
const authServerMetadataCache: AuthorizationServerMetadataCache = new SimpleStoreMemory({
  ttl: 24 * 60 * 60 * 1000,
  max: 100,
});
const protectedResourceMetadataCache: ProtectedResourceMetadataCache = new SimpleStoreMemory({
  ttl: 24 * 60 * 60 * 1000,
  max: 100,
});

// Environment variables
const PUBLIC_URL = process.env.PUBLIC_URL; // e.g., https://sidetrail.app
const PRIVATE_KEY_ES256 = process.env.PRIVATE_KEY_ES256; // JWK string for signing
const COOKIE_SECRET = process.env.COOKIE_SECRET;

// Singleton OAuth client
let oauthClient: NodeOAuthClient | null = null;
let initPromise: Promise<NodeOAuthClient> | null = null;

/**
 * Creates and returns the OAuth client singleton.
 * Handles both development (loopback) and production (confidential client) modes.
 */
export async function getOAuthClient(): Promise<NodeOAuthClient> {
  if (oauthClient) return oauthClient;
  if (initPromise) return initPromise;

  initPromise = createOAuthClient();
  oauthClient = await initPromise;
  return oauthClient;
}

async function createOAuthClient(): Promise<NodeOAuthClient> {
  // Initialize database tables
  await initAuthTables();

  // Build keyset for confidential client (production only)
  let keyset: Keyset | undefined;

  if (PUBLIC_URL && PRIVATE_KEY_ES256) {
    try {
      const jwk = JSON.parse(PRIVATE_KEY_ES256);
      const key = await JoseKey.fromJWK(jwk);
      keyset = new Keyset([key]);
    } catch (err) {
      console.error("Failed to parse PRIVATE_KEY_ES256:", err);
      throw new Error("Invalid PRIVATE_KEY_ES256. Must be a valid ES256 JWK JSON string.");
    }
  }

  // For production, we need a keyset
  if (PUBLIC_URL && !keyset) {
    throw new Error(
      "Production mode requires PRIVATE_KEY_ES256 environment variable. " +
        "Generate one with: npx @atproto/oauth-client-node gen-jwk",
    );
  }

  // Get the signing key if we have a keyset
  // The keyset was built from a single key, so just check if it exists
  const hasSigningKey = keyset && keyset.size > 0;

  // Build client metadata
  const clientMetadata: OAuthClientMetadataInput = PUBLIC_URL
    ? {
        // Production: Confidential client
        client_name: "Sidetrail",
        client_id: `${PUBLIC_URL}/oauth-client-metadata.json`,
        client_uri: PUBLIC_URL,
        redirect_uris: [`${PUBLIC_URL}/oauth/callback`],
        scope: "atproto transition:generic",
        grant_types: ["authorization_code", "refresh_token"],
        response_types: ["code"],
        application_type: "web",
        token_endpoint_auth_method: hasSigningKey ? "private_key_jwt" : "none",
        token_endpoint_auth_signing_alg: hasSigningKey ? "ES256" : undefined,
        dpop_bound_access_tokens: true,
        jwks_uri: `${PUBLIC_URL}/oauth/jwks.json`,
      }
    : {
        // Development: Loopback client (no keys needed)
        // ATProto requires 127.0.0.1 for loopback, not localhost (RFC 8252)
        client_name: "Sidetrail (Development)",
        client_id: `http://localhost?redirect_uri=${encodeURIComponent(
          "http://127.0.0.1:3000/oauth/callback",
        )}&scope=${encodeURIComponent("atproto transition:generic")}`,
        redirect_uris: ["http://127.0.0.1:3000/oauth/callback"],
        scope: "atproto transition:generic",
        grant_types: ["authorization_code", "refresh_token"],
        response_types: ["code"],
        application_type: "web",
        token_endpoint_auth_method: "none",
        dpop_bound_access_tokens: true,
      };

  return new NodeOAuthClient({
    clientMetadata,
    keyset,
    stateStore: new StateStore(),
    sessionStore: new SessionStore(),
    requestLock,
    // Use longer-lived cache for OAuth metadata (rarely changes)
    authorizationServerMetadataCache: authServerMetadataCache,
    protectedResourceMetadataCache,
  });
}

/**
 * Get the client metadata for the /oauth/client-metadata.json endpoint.
 */
export async function getClientMetadata() {
  const client = await getOAuthClient();
  return client.clientMetadata;
}

/**
 * Get the JWKS for the /oauth/jwks.json endpoint.
 */
export async function getJwks() {
  const client = await getOAuthClient();
  return client.jwks;
}

/**
 * Cookie secret for iron-session.
 */
export function getCookieSecret(): string {
  if (!COOKIE_SECRET) {
    if (process.env.NODE_ENV === "production") {
      throw new Error("COOKIE_SECRET is required in production");
    }
    // Default for development
    return "development-secret-at-least-32-characters-long";
  }
  return COOKIE_SECRET;
}

/**
 * Check if we're in production mode (has PUBLIC_URL).
 */
export function isProduction(): boolean {
  return !!PUBLIC_URL;
}
