import {
  NodeOAuthClient,
  NodeSavedSession,
  NodeSavedState,
  RuntimeLock,
  OAuthSession,
} from "@atproto/oauth-client-node";
import { JoseKey } from "@atproto/jwk-jose";
import { oauth_metadata } from "app/api/oauth/[route]/oauth-metadata";
import { supabaseServerClient } from "supabase/serverClient";

import Client from "ioredis";
import Redlock from "redlock";
import { Result, Ok, Err } from "./result";

// Module-scoped singleton: NodeOAuthClient, ioredis connection, and Redlock
// have no per-request state — keys/stores live above the user — so building
// them once per Node instance avoids reconnect + keyset re-import on every call.
// Stashed on globalThis so Next.js dev hot-reload doesn't leak Redis sockets.
const globalForOauth = globalThis as unknown as {
  __oauthClient?: Promise<NodeOAuthClient>;
};

export function createOauthClient(): Promise<NodeOAuthClient> {
  if (!globalForOauth.__oauthClient) {
    globalForOauth.__oauthClient = buildOauthClient();
  }
  return globalForOauth.__oauthClient;
}

async function buildOauthClient(): Promise<NodeOAuthClient> {
  let keyset =
    process.env.NODE_ENV === "production"
      ? await Promise.all([
          JoseKey.fromImportable(process.env.JOSE_PRIVATE_KEY_1!),
        ])
      : undefined;
  let requestLock: RuntimeLock | undefined;
  if (process.env.NODE_ENV === "production" && process.env.REDIS_URL) {
    const client = new Client(process.env.REDIS_URL);
    const redlock = new Redlock([client]);
    requestLock = async (key, fn) => {
      // 30 seconds should be enough. Since we will be using one lock per user id
      // we can be quite liberal with the lock duration here.
      const lock = await redlock.acquire([key], 45e3);
      try {
        return await fn();
      } finally {
        await lock.release();
      }
    };
  }
  return new NodeOAuthClient({
    // This object will be used to build the payload of the /client-metadata.json
    // endpoint metadata, exposing the client metadata to the OAuth server.
    clientMetadata: oauth_metadata,

    // Used to authenticate the client to the token endpoint. Will be used to
    // build the jwks object to be exposed on the "jwks_uri" endpoint.
    keyset,

    // Interface to store authorization state data (during authorization flows)
    stateStore,
    // Interface to store authenticated session data
    sessionStore,
    requestLock,
  });
}

let stateStore = {
  async set(key: string, state: NodeSavedState): Promise<void> {
    await supabaseServerClient.from("oauth_state_store").upsert({ key, state });
  },
  async get(key: string): Promise<NodeSavedState | undefined> {
    let { data } = await supabaseServerClient
      .from("oauth_state_store")
      .select("state")
      .eq("key", key)
      .single();
    return (data?.state as NodeSavedState) || undefined;
  },
  async del(key: string): Promise<void> {
    await supabaseServerClient
      .from("oauth_state_store")
      .delete()
      .eq("key", key);
  },
};

let sessionStore = {
  async set(key: string, session: NodeSavedSession): Promise<void> {
    await supabaseServerClient
      .from("oauth_session_store")
      .upsert({ key, session });
  },
  async get(key: string): Promise<NodeSavedSession | undefined> {
    let { data } = await supabaseServerClient
      .from("oauth_session_store")
      .select("session")
      .eq("key", key)
      .single();
    return (data?.session as NodeSavedSession) || undefined;
  },
  async del(key: string): Promise<void> {
    await supabaseServerClient
      .from("oauth_session_store")
      .delete()
      .eq("key", key);
  },
};

export type OAuthSessionError = {
  type: "oauth_session_expired";
  message: string;
  did: string;
};

// In-process dedupe: collapse concurrent restore() calls for the same DID into
// one underlying restore + Redlock acquisition. Successful entries linger
// briefly so a burst of requests (e.g. hover-fired ProfilePopovers) share one
// result; rejected promises evict immediately so a transient failure doesn't
// stick around poisoning subsequent calls.
const RESTORE_DEDUPE_TTL_MS = 5_000;
const inFlightRestores = new Map<string, Promise<OAuthSession>>();

function dedupedRestore(did: string): Promise<OAuthSession> {
  let existing = inFlightRestores.get(did);
  if (existing) return existing;

  const promise = (async () => {
    const oauthClient = await createOauthClient();
    return oauthClient.restore(did);
  })();
  inFlightRestores.set(did, promise);

  promise.then(
    () => {
      setTimeout(() => {
        if (inFlightRestores.get(did) === promise) {
          inFlightRestores.delete(did);
        }
      }, RESTORE_DEDUPE_TTL_MS);
    },
    () => {
      if (inFlightRestores.get(did) === promise) {
        inFlightRestores.delete(did);
      }
    },
  );

  return promise;
}

export async function restoreOAuthSession(
  did: string
): Promise<Result<OAuthSession, OAuthSessionError>> {
  try {
    const session = await dedupedRestore(did);
    return Ok(session);
  } catch (error) {
    return Err({
      type: "oauth_session_expired",
      message:
        error instanceof Error
          ? error.message
          : "OAuth session expired or invalid",
      did,
    });
  }
}
