import { Hono } from "hono";
import { loadAuthConfig, type AuthConfig } from "./config";
import { makeSession } from "./session";
import { makeStateStore } from "./state";
import { makeGoogleProvider } from "./providers/google";
import { makeFirebaseProvider } from "./providers/firebase";
import { makeLocalProviderFromPassword } from "./providers/local";
import {
  SNOLAB_FIREBASE_CONFIG,
  isSnolabFirebaseConfigured,
  snolabSupportsDomain,
  snolabUnavailableMessage,
} from "./snolabDefaults";
import { healthRoute } from "./routes/health";
import { verifyRoute } from "./routes/verify";
import { loginRoute } from "./routes/login";
import { callbackRoute } from "./routes/callback";
import { logoutRoute } from "./routes/logout";
import { meRoute } from "./routes/me";
import { firebaseRoute } from "./routes/firebase";
import { firebaseLoginRoute } from "./routes/firebaseLogin";
import { localLoginRoute } from "./routes/localLogin";
import { makeAuditLogger, type AuditLogger } from "./audit";

export async function buildApp(
  configOverride?: AuthConfig,
  options?: { audit?: AuditLogger },
) {
  const config = configOverride ?? (await loadAuthConfig());
  const ssoOrigin = `https://${config.ssoHost}`;
  const redirectUri = `${ssoOrigin}/callback`;

  const session = makeSession({
    secret: config.sessionSecret,
    audience: config.domain,
    ttlSeconds: parsePositiveInt(process.env.FBI_AUTH_SESSION_TTL_SECONDS),
    refreshThresholdSeconds: parsePositiveInt(
      process.env.FBI_AUTH_REFRESH_THRESHOLD_SECONDS,
    ),
  });
  const states = makeStateStore();
  const audit = options?.audit ?? makeAuditLogger();

  const app = new Hono();
  app.route("/", healthRoute());
  app.route("/", verifyRoute({ config, session, audit }));
  app.route("/", logoutRoute({ config, session, audit }));
  app.route("/", meRoute({ session }));

  if (config.provider === "google") {
    if (!config.clientId)
      throw new Error("provider 'google' requires clientId");
    const provider = await makeGoogleProvider({
      clientId: config.clientId,
      clientSecret: config.clientSecret,
      redirectUri,
    });
    app.route("/", loginRoute({ provider, states, ssoOrigin }));
    app.route("/", callbackRoute({ config, provider, session, states, audit }));
  } else if (config.provider === "snolab") {
    if (!isSnolabFirebaseConfigured() || !snolabSupportsDomain(config.domain)) {
      throw new Error(snolabUnavailableMessage(config.domain));
    }
    const firebaseConfig = SNOLAB_FIREBASE_CONFIG!;
    const provider = makeFirebaseProvider({
      projectId: firebaseConfig.projectId,
    });
    app.route("/", firebaseRoute({ config, provider, session, audit }));
    app.route(
      "/",
      firebaseLoginRoute({
        ssoOrigin,
        domain: config.domain,
        firebaseConfig,
      }),
    );
  } else if (config.provider === "firebase") {
    if (!config.firebase?.projectId)
      throw new Error("provider 'firebase' requires firebase.projectId");
    const provider = makeFirebaseProvider({
      projectId: config.firebase.projectId,
    });
    app.route("/", firebaseRoute({ config, provider, session, audit }));
    if (config.firebase.apiKey && config.firebase.authDomain) {
      app.route(
        "/",
        firebaseLoginRoute({
          ssoOrigin,
          domain: config.domain,
          firebaseConfig: {
            apiKey: config.firebase.apiKey,
            authDomain: config.firebase.authDomain,
            projectId: config.firebase.projectId,
          },
        }),
      );
    }
  } else if (config.provider === "local") {
    if (!config.local?.email)
      throw new Error("provider 'local' requires local.email");
    const password = process.env.FBI_AUTH_LOCAL_PASSWORD;
    if (!password)
      throw new Error(
        "provider 'local' requires FBI_AUTH_LOCAL_PASSWORD env var (the password is hashed in-memory each boot; nothing is persisted to disk).",
      );
    console.warn(
      "[fbi-auth] provider: local — username/password has no MFA, account lockout, or rate limiting. Use for local dev / trusted intranet only.",
    );
    const provider = await makeLocalProviderFromPassword({
      email: config.local.email,
      name: config.local.name,
      password,
    });
    app.route(
      "/",
      localLoginRoute({ config, provider, session, ssoOrigin, audit }),
    );
  } else {
    throw new Error(`Unknown provider: ${(config as AuthConfig).provider}`);
  }

  app.get("/", (c) => c.redirect("/api/auth/me", 302));

  return { app, config, ssoOrigin };
}

function parsePositiveInt(s: string | undefined): number | undefined {
  if (!s) return undefined;
  const n = Number.parseInt(s, 10);
  return Number.isFinite(n) && n > 0 ? n : undefined;
}

if (import.meta.main) {
  const { app, config, ssoOrigin } = await buildApp();
  const port = Number(process.env.FBI_AUTH_PORT ?? 2433);

  console.log(`[fbi-auth] listening on http://127.0.0.1:${port}`);
  console.log(`[fbi-auth] sso origin: ${ssoOrigin}`);
  console.log(
    `[fbi-auth] domain: ${config.domain}, provider: ${config.provider}`,
  );

  Bun.serve({
    port,
    hostname: "127.0.0.1",
    fetch: app.fetch,
  });
}
