/**
 * Auth routes — register, login, logout (web + API).
 */

import { Hono } from "hono";
import { setCookie, deleteCookie, getCookie } from "hono/cookie";
import { and, eq, gte, isNull, sql } from "drizzle-orm";
import { db } from "../db";
import {
  users,
  sessions,
  organizations,
  orgSsoConfigs,
  userTotp,
  userRecoveryCodes,
  loginAttempts,
} from "../db/schema";
import {
  hashPassword,
  verifyPassword,
  generateSessionToken,
  sessionCookieOptions,
  sessionExpiry,
} from "../lib/auth";
import { verifyTotpCode, hashRecoveryCode } from "../lib/totp";
import {
  evaluateLockout,
  retryAfterMinutes,
  LOGIN_FAIL_WINDOW_MS,
  LOGIN_FAIL_LIMIT,
  type LockoutState,
} from "../lib/login-lockout";
import { cancelAccountDeletion } from "../lib/account-deletion";
import { audit } from "../lib/notify";
import {
  getSsoConfig,
  getGithubOauthConfig,
  getGoogleOauthConfig,
} from "../lib/sso";
import { Layout } from "../views/layout";
import { SignInV2 } from "../views/signin-v2";
import {
  Form,
  FormGroup,
  Input,
  Button,
  LinkButton,
  Alert,
  Text,
} from "../views/ui";
import { softAuth } from "../middleware/auth";
import type { AuthEnv } from "../middleware/auth";

const auth = new Hono<AuthEnv>();

// One-shot latch — log the auto-verify warning at most once per process,
// since the misconfiguration is operator-level (env var) and won't change
// between requests.
let _autoVerifyWarned = false;

// ───────────────────────────────────────────────────────────────────────
// Scoped mobile polish — tightens the existing `.auth-container` shell
// from layout.tsx for ≤720px viewports. Only adds rules; does not
// redefine the desktop styling. Kept inline so this file remains the
// single source of truth for the auth surface.
// ───────────────────────────────────────────────────────────────────────
const authMobileCss = `
  @media (max-width: 720px) {
    .auth-container {
      margin: 24px 12px;
      padding: 24px 20px 22px;
      max-width: 100%;
    }
    .auth-container .btn-primary { min-height: 44px; }
    .auth-container .oauth-btn { min-height: 44px; }
    .auth-container input[type="text"],
    .auth-container input[type="email"],
    .auth-container input[type="password"] { min-height: 44px; }
    .auth-forgot { text-align: left !important; }
  }
`;
const AuthMobileStyle = () => (
  <style dangerouslySetInnerHTML={{ __html: authMobileCss }} />
);

// --- Web UI ---

auth.get("/register", softAuth, (c) => {
  // If the user is already signed in, drop them on their dashboard rather
  // than rendering the logged-out sign-up shell over an authed session.
  const existing = c.get("user");
  if (existing) return c.redirect("/dashboard");
  const error = c.req.query("error");
  const csrf = c.get("csrfToken") as string | undefined;
  return c.html(
    <Layout title="Register" user={null}>
      <AuthMobileStyle />
      <div class="auth-container">
        <h2>Create your account</h2>
        <p class="auth-subtitle">
          Get the full AI suite — code review, auto-merge, spec-to-PR — on
          unlimited public repos. No credit card.
        </p>
        {error && <div class="auth-error">{decodeURIComponent(error)}</div>}
        <Form method="post" action="/register" csrfToken={csrf}>
          <FormGroup label="Username" htmlFor="username">
            <Input
              id="username"
              type="text"
              name="username"
              required
              pattern="^[a-zA-Z0-9_-]+$"
              minLength={2}
              maxLength={39}
              placeholder="your-username"
              autocomplete="username"
            />
          </FormGroup>
          <FormGroup label="Email" htmlFor="email">
            <Input
              type="email"
              name="email"
              required
              placeholder="you@example.com"
              autocomplete="email"
              aria-label="Email"
            />
          </FormGroup>
          <FormGroup label="Password" htmlFor="password">
            <Input
              type="password"
              name="password"
              required
              minLength={8}
              placeholder="Min 8 characters"
              autocomplete="new-password"
              aria-label="Password"
            />
          </FormGroup>
          {/* P3 — Terms / Privacy acceptance. Required client-side via the
              `required` attribute; server-side re-checked in POST handler. */}
          <div class="form-group" style="margin: 12px 0">
            <label style="display: flex; gap: 8px; align-items: flex-start; font-size: 13px; color: var(--text-muted)">
              <input
                type="checkbox"
                name="accept_terms"
                value="1"
                required
                style="margin-top: 3px"
                aria-label="Accept Terms of Service and Privacy Policy"
              />
              <span>
                I agree to the{" "}
                <a href="/terms" target="_blank" rel="noopener">
                  Terms of Service
                </a>{" "}
                and{" "}
                <a href="/privacy" target="_blank" rel="noopener">
                  Privacy Policy
                </a>
                .
              </span>
            </label>
          </div>
          <Button type="submit" variant="primary">
            Create account
          </Button>
        </Form>
        <p class="auth-switch">
          <Text>Already have an account? <a href="/login">Sign in</a></Text>
        </p>
      </div>
    </Layout>
  );
});

auth.post("/register", async (c) => {
  const body = await c.req.parseBody();
  const username = String(body.username || "").trim();
  const email = String(body.email || "").trim();
  const password = String(body.password || "");

  if (!username || !email || !password) {
    return c.redirect("/register?error=All+fields+are+required");
  }

  // Block P3 — Terms acceptance is required. The form's checkbox has
  // `required` so browsers normally enforce client-side; the server
  // re-checks for defensive depth (curl, scripted POST, etc.).
  if (!body.accept_terms) {
    return c.redirect(
      "/register?error=Please+accept+the+Terms+of+Service+and+Privacy+Policy"
    );
  }

  if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
    return c.redirect(
      "/register?error=Username+may+only+contain+letters%2C+numbers%2C+hyphens+and+underscores"
    );
  }

  if (password.length < 8) {
    return c.redirect("/register?error=Password+must+be+at+least+8+characters");
  }

  // Check existing
  const [existingUser] = await db
    .select()
    .from(users)
    .where(eq(users.username, username))
    .limit(1);
  if (existingUser) {
    return c.redirect("/register?error=Username+already+taken");
  }

  // B2: usernames share the URL namespace with org slugs; refuse collisions.
  const [existingOrg] = await db
    .select({ id: organizations.id })
    .from(organizations)
    .where(eq(organizations.slug, username.toLowerCase()))
    .limit(1);
  if (existingOrg) {
    return c.redirect("/register?error=Username+already+taken");
  }

  const [existingEmail] = await db
    .select()
    .from(users)
    .where(eq(users.email, email))
    .limit(1);
  if (existingEmail) {
    return c.redirect("/register?error=Email+already+registered");
  }

  const passwordHash = await hashPassword(password);

  // First user ever registered becomes admin automatically
  const [userCount] = await db
    .select({ count: sql`count(*)::int` })
    .from(users);
  const isFirstUser = (userCount?.count as number) === 0;

  const [user] = await db
    .insert(users)
    .values({
      username,
      email,
      passwordHash,
      isAdmin: isFirstUser,
      // P3 — record terms acceptance now. Version bumps when Terms change.
      termsAcceptedAt: new Date(),
      termsVersion: "1.0",
    })
    .returning();

  // If username matches SITE_ADMIN_USERNAME env, grant site admin instantly
  // so the operator doesn't have to wait for the next boot's bootstrap pass.
  await import("../lib/admin-bootstrap")
    .then((m) => m.ensureEnvAdminOnRegister({ userId: user.id, username }))
    .catch((err) => {
      console.warn(
        `[admin-bootstrap] ensureEnvAdminOnRegister failed for ${username}:`,
        err instanceof Error ? err.message : err
      );
    });

  // Create session
  const token = generateSessionToken();
  await db.insert(sessions).values({
    userId: user.id,
    token,
    expiresAt: sessionExpiry(),
  });

  setCookie(c, "session", token, sessionCookieOptions());

  // Block P2 — email verification. If RESEND_API_KEY is configured the
  // verification email goes out and the user clicks the link to verify.
  // If email is NOT configured (EMAIL_PROVIDER=log, no RESEND_API_KEY,
  // etc.), the email would silently never arrive and the user would be
  // locked out — AUDIT-v2.md P0 #3. In that case, auto-verify the
  // account on registration so the user can actually use the site.
  // Operators who want real verification should set EMAIL_PROVIDER=resend
  // + RESEND_API_KEY in their environment.
  const { config: _emailConfig } = await import("../lib/config");
  const emailConfigured =
    _emailConfig.emailProvider === "resend" && !!_emailConfig.resendApiKey;
  if (emailConfigured) {
    import("../lib/email-verification")
      .then((m) => m.startEmailVerification(user.id, email))
      .catch((err) => {
        console.error(
          `[auth] startEmailVerification failed for ${user.id}:`,
          err instanceof Error ? err.message : err
        );
      });
  } else {
    // Auto-verify immediately so the user isn't trapped in an unverified
    // state. Log once so operators notice the misconfiguration.
    if (!_autoVerifyWarned) {
      _autoVerifyWarned = true;
      console.warn(
        "[auth] EMAIL_PROVIDER is not configured (set EMAIL_PROVIDER=resend + RESEND_API_KEY). Auto-verifying new account email addresses to avoid lockout."
      );
    }
    await db
      .update(users)
      .set({ emailVerifiedAt: new Date() })
      .where(eq(users.id, user.id))
      .catch((err) => {
        console.error(
          `[auth] auto-verify failed for ${user.id}:`,
          err instanceof Error ? err.message : err
        );
      });
  }

  // Onboarding drip — T+0 "welcome" email. Fire-and-forget; never blocks
  // the redirect. Silently skips when email is not configured.
  import("../lib/onboarding-drip")
    .then((m) => m.sendWelcomeEmail(user.id))
    .catch((err) => {
      console.error(
        `[auth] onboarding welcome email failed for ${user.id}:`,
        err instanceof Error ? err.message : err
      );
    });

  // P3 — default landing is /onboarding (the guided first-five-minutes
  // flow). The `redirect=` query is still honoured for OAuth-style flows.
  const redirect = c.req.query("redirect") || "/onboarding?welcome=1";
  return c.redirect(redirect);
});

auth.get("/login", softAuth, async (c) => {
  // Already-authed users hitting the sign-in page get bounced to their
  // dashboard (or the `redirect=` target if one was supplied).
  const existing = c.get("user");
  const error = c.req.query("error");
  const success = c.req.query("success");
  const redirect = c.req.query("redirect") || "";
  if (existing) return c.redirect(redirect || "/dashboard");
  const ssoCfg = await getSsoConfig();
  const ssoEnabled =
    !!ssoCfg?.enabled &&
    !!ssoCfg.authorizationEndpoint &&
    !!ssoCfg.tokenEndpoint &&
    !!ssoCfg.userinfoEndpoint &&
    !!ssoCfg.clientId &&
    !!ssoCfg.clientSecret;
  const ssoLabel =
    ssoCfg?.providerName || inferSsoProviderName(ssoCfg) || "SSO";
  // Block L6 — "Sign in with GitHub" (separate row keyed id='github').
  const githubCfg = await getGithubOauthConfig();
  const githubEnabled =
    !!githubCfg?.enabled && !!githubCfg.clientId && !!githubCfg.clientSecret;
  // "Sign in with Google" (separate row keyed id='google'). Same wiring
  // pattern as GitHub OAuth.
  const googleCfg = await getGoogleOauthConfig();
  const googleEnabled =
    !!googleCfg?.enabled && !!googleCfg.clientId && !!googleCfg.clientSecret;
  return c.html(
    <SignInV2
      redirect={redirect}
      error={error ? decodeURIComponent(error) : ""}
      googleEnabled={googleEnabled}
      githubEnabled={githubEnabled}
    />
  );
});

/**
 * Loads the failure aggregate for `email` and evaluates the lockout policy
 * (see src/lib/login-lockout.ts for the semantics).
 *
 * Fails OPEN: if the `login_attempts` table is unreachable (missing
 * migration, transient DB error) login must still work — a broken lockout
 * ledger must never lock every user out of the site. That failure class
 * is exactly what the 0087 migration blockade caused in production.
 */
async function getLockoutState(email: string): Promise<LockoutState> {
  try {
    const since = new Date(Date.now() - LOGIN_FAIL_WINDOW_MS);
    const [row] = await db
      .select({
        count: sql<number>`count(*)::int`,
        newest: sql<string | null>`max(${loginAttempts.createdAt})`,
      })
      .from(loginAttempts)
      .where(
        and(
          eq(loginAttempts.email, email.toLowerCase()),
          eq(loginAttempts.success, false),
          gte(loginAttempts.createdAt, since)
        )
      );
    return evaluateLockout({
      failureCount: row?.count ?? 0,
      newestFailureAt: row?.newest ? new Date(row.newest) : null,
    });
  } catch (err) {
    console.error(
      "[auth] lockout check failed (failing open):",
      err instanceof Error ? err.message : err
    );
    return { locked: false, failureCount: 0, retryAfterMs: 0 };
  }
}

auth.post("/login", async (c) => {
  const body = await c.req.parseBody();
  const identifier = String(body.username || "").trim();
  const password = String(body.password || "");
  const redirect = c.req.query("redirect") || "/";
  const ip =
    c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ||
    c.req.header("x-real-ip") ||
    "unknown";
  const ua = c.req.header("user-agent") || "";

  if (!identifier || !password) {
    return c.redirect("/login?error=All+fields+are+required");
  }

  // Enterprise SSO domain-hint routing: if the identifier is an email and the
  // domain matches an org's `domain_hint`, redirect to that org's SSO flow
  // instead of checking the password.
  // Also: resolve the canonical email for lockout checks regardless of whether
  // the user typed username or email.
  const isEmail = identifier.includes("@");
  if (isEmail) {
    const emailDomain = identifier.split("@")[1]?.toLowerCase();
    if (emailDomain) {
      const [ssoHint] = await db
        .select({
          provider: orgSsoConfigs.provider,
          orgId: orgSsoConfigs.orgId,
        })
        .from(orgSsoConfigs)
        .where(eq(orgSsoConfigs.domainHint, emailDomain))
        .limit(1);

      if (ssoHint) {
        // Resolve org slug from org ID
        const [orgRow] = await db
          .select({ slug: organizations.slug })
          .from(organizations)
          .where(eq(organizations.id, ssoHint.orgId))
          .limit(1);

        if (orgRow) {
          const protocol = ssoHint.provider === "oidc" ? "oidc" : "saml";
          return c.redirect(`/sso/${protocol}/${orgRow.slug}/login`);
        }
      }
    }
  }

  // Find user by username or email
  const [user] = await db
    .select()
    .from(users)
    .where(
      isEmail
        ? eq(users.email, identifier)
        : eq(users.username, identifier)
    )
    .limit(1);

  // Determine the email key for lockout (use identifier if user not found
  // so we still record the attempt without leaking account existence).
  const emailKey = (user?.email ?? identifier).toLowerCase();

  // ── Lockout check ───────────────────────────────────────────────────
  // Locked when ≥ LOGIN_FAIL_LIMIT failures in the trailing window AND the
  // newest failure is younger than LOGIN_LOCKOUT_MS. We check before
  // password verification so brute-forcers can't time-diff their way
  // around it. Blocked attempts are deliberately NOT recorded as failures:
  // recording them rolled the window forward forever, so a user retrying
  // their correct password stayed locked out permanently.
  const lockout = await getLockoutState(emailKey);
  if (lockout.locked) {
    await audit({
      userId: user?.id ?? null,
      action: "auth.login.locked",
      ip,
      userAgent: ua,
      metadata: { email: emailKey, recentFailures: lockout.failureCount },
    });
    const mins = retryAfterMinutes(lockout);
    return c.redirect(
      `/login?error=${encodeURIComponent(
        `Account temporarily locked due to too many failed login attempts. Please try again in ${mins} minute${mins === 1 ? "" : "s"}.`
      )}`
    );
  }
  const recentFailures = lockout.failureCount;

  if (!user) {
    // Record failed attempt (unknown user) and return generic error.
    await db
      .insert(loginAttempts)
      .values({ email: emailKey, ip, success: false })
      .catch(() => {});
    return c.redirect("/login?error=Invalid+credentials");
  }

  const valid = await verifyPassword(password, user.passwordHash);
  if (!valid) {
    // Record failed attempt.
    await db
      .insert(loginAttempts)
      .values({ email: emailKey, ip, success: false })
      .catch(() => {});
    await audit({
      userId: user.id,
      action: "auth.login.failed",
      ip,
      userAgent: ua,
      metadata: { email: emailKey, attempt: recentFailures + 1 },
    });
    // Check if this failure just crossed the threshold.
    if (recentFailures + 1 >= LOGIN_FAIL_LIMIT) {
      await audit({
        userId: user.id,
        action: "auth.login.locked",
        ip,
        userAgent: ua,
        metadata: { email: emailKey, recentFailures: recentFailures + 1 },
      });
      return c.redirect(
        "/login?error=Account+temporarily+locked+due+to+too+many+failed+login+attempts.+Please+try+again+in+15+minutes."
      );
    }
    return c.redirect("/login?error=Invalid+credentials");
  }

  // Successful login — record success and clear the failure history so a
  // stale window can't combine with one future typo to re-trip the lock.
  await db
    .insert(loginAttempts)
    .values({ email: emailKey, ip, success: true })
    .catch(() => {});
  await db
    .delete(loginAttempts)
    .where(
      and(eq(loginAttempts.email, emailKey), eq(loginAttempts.success, false))
    )
    .catch(() => {});

  // B4: if the user has TOTP enabled, issue a pending-2fa session and
  // redirect to the code prompt.
  const [totp] = await db
    .select({ enabledAt: userTotp.enabledAt })
    .from(userTotp)
    .where(eq(userTotp.userId, user.id))
    .limit(1);
  const needs2fa = !!(totp && totp.enabledAt);

  const token = generateSessionToken();
  await db.insert(sessions).values({
    userId: user.id,
    token,
    expiresAt: sessionExpiry(),
    requires2fa: needs2fa,
    ip,
    userAgent: ua,
    lastSeenAt: new Date(),
  });

  setCookie(c, "session", token, sessionCookieOptions());

  // Block P5 — If account was scheduled for deletion but user signed back
  // in, cancel the deletion. Safe regardless of 2FA: password was proven.
  if (user.deletedAt) {
    await cancelAccountDeletion(user.id);
  }

  if (needs2fa) {
    return c.redirect(
      `/login/2fa?redirect=${encodeURIComponent(redirect)}`
    );
  }
  return c.redirect(redirect);
});

// --- 2FA verify (B4) ---
auth.get("/login/2fa", async (c) => {
  const token = getCookie(c, "session");
  if (!token) return c.redirect("/login");
  const error = c.req.query("error");
  const redirect = c.req.query("redirect") || "/";
  return c.html(
    <Layout title="Two-factor authentication" user={null}>
      <AuthMobileStyle />
      <div class="auth-container">
        <h2>Enter your code</h2>
        <p
          class="auth-switch"
          style="margin-bottom: 16px; margin-top: 0"
        >
          Open your authenticator app and enter the 6-digit code. Lost your
          device? Paste a recovery code instead.
        </p>
        {error && <div class="auth-error">{decodeURIComponent(error)}</div>}
        <form
          method="post"
          action={`/login/2fa?redirect=${encodeURIComponent(redirect)}`}
        >
          <input type="hidden" name="_csrf" value={(c.get("csrfToken") as string | undefined) || ""} />
          <div class="form-group">
            <label for="code">Code</label>
            <input
              type="text"
              id="code"
              name="code"
              required
              autocomplete="one-time-code"
              inputmode="numeric"
              maxLength={24}
              placeholder="123456 or xxxx-xxxx-xxxx"
            />
          </div>
          <button type="submit" class="btn btn-primary">
            Verify
          </button>
        </form>
        <p class="auth-switch">
          <a href="/logout">Cancel</a>
        </p>
      </div>
    </Layout>
  );
});

auth.post("/login/2fa", async (c) => {
  const token = getCookie(c, "session");
  if (!token) return c.redirect("/login");
  const body = await c.req.parseBody();
  const code = String(body.code || "").trim();
  const redirect = c.req.query("redirect") || "/";

  if (!code) {
    return c.redirect(
      `/login/2fa?error=Code+is+required&redirect=${encodeURIComponent(redirect)}`
    );
  }

  try {
    const [session] = await db
      .select()
      .from(sessions)
      .where(eq(sessions.token, token))
      .limit(1);
    if (
      !session ||
      new Date(session.expiresAt) < new Date() ||
      !session.requires2fa
    ) {
      return c.redirect("/login");
    }

    const [totp] = await db
      .select()
      .from(userTotp)
      .where(eq(userTotp.userId, session.userId))
      .limit(1);
    if (!totp || !totp.enabledAt) {
      // User doesn't have 2FA actually enabled — clear the flag and let
      // them in. This can only happen if 2FA was disabled in another
      // session between password check and code prompt.
      await db
        .update(sessions)
        .set({ requires2fa: false })
        .where(eq(sessions.token, token));
      return c.redirect(redirect);
    }

    // Try TOTP code first.
    const isSix = /^\d{6}$/.test(code);
    let ok = false;
    if (isSix) {
      ok = await verifyTotpCode(totp.secret, code);
    }
    // Fall through to recovery code.
    if (!ok) {
      const hash = await hashRecoveryCode(code);
      const [rec] = await db
        .select()
        .from(userRecoveryCodes)
        .where(
          and(
            eq(userRecoveryCodes.userId, session.userId),
            eq(userRecoveryCodes.codeHash, hash),
            isNull(userRecoveryCodes.usedAt)
          )
        )
        .limit(1);
      if (rec) {
        await db
          .update(userRecoveryCodes)
          .set({ usedAt: new Date() })
          .where(eq(userRecoveryCodes.id, rec.id));
        ok = true;
      }
    }

    if (!ok) {
      return c.redirect(
        `/login/2fa?error=Invalid+code&redirect=${encodeURIComponent(redirect)}`
      );
    }

    await db
      .update(sessions)
      .set({ requires2fa: false })
      .where(eq(sessions.token, token));
    await db
      .update(userTotp)
      .set({ lastUsedAt: new Date() })
      .where(eq(userTotp.userId, session.userId));

    return c.redirect(redirect);
  } catch (err) {
    console.error("[auth] 2fa verify:", err);
    return c.redirect(
      `/login/2fa?error=Service+unavailable&redirect=${encodeURIComponent(redirect)}`
    );
  }
});

auth.get("/logout", async (c) => {
  deleteCookie(c, "session", { path: "/" });
  return c.redirect("/");
});

// --- API ---

auth.post("/api/auth/register", async (c) => {
  const body = await c.req.json<{
    username: string;
    email: string;
    password: string;
  }>();

  if (!body.username || !body.email || !body.password) {
    return c.json({ error: "username, email, and password are required" }, 400);
  }

  if (!/^[a-zA-Z0-9_-]+$/.test(body.username)) {
    return c.json({ error: "Invalid username" }, 400);
  }

  if (body.password.length < 8) {
    return c.json({ error: "Password must be at least 8 characters" }, 400);
  }

  const [existing] = await db
    .select()
    .from(users)
    .where(eq(users.username, body.username))
    .limit(1);
  if (existing) {
    return c.json({ error: "Username already taken" }, 409);
  }

  const passwordHash = await hashPassword(body.password);
  const [user] = await db
    .insert(users)
    .values({
      username: body.username,
      email: body.email,
      passwordHash,
    })
    .returning();

  const token = generateSessionToken();
  await db.insert(sessions).values({
    userId: user.id,
    token,
    expiresAt: sessionExpiry(),
  });

  return c.json(
    {
      user: { id: user.id, username: user.username, email: user.email },
      token,
    },
    201
  );
});

auth.post("/api/auth/login", async (c) => {
  const body = await c.req.json<{ username: string; password: string }>();

  if (!body.username || !body.password) {
    return c.json({ error: "username and password are required" }, 400);
  }

  const isEmail = body.username.includes("@");
  const [user] = await db
    .select()
    .from(users)
    .where(
      isEmail
        ? eq(users.email, body.username)
        : eq(users.username, body.username)
    )
    .limit(1);

  if (!user) return c.json({ error: "Invalid credentials" }, 401);

  const valid = await verifyPassword(body.password, user.passwordHash);
  if (!valid) return c.json({ error: "Invalid credentials" }, 401);

  const token = generateSessionToken();
  await db.insert(sessions).values({
    userId: user.id,
    token,
    expiresAt: sessionExpiry(),
  });

  return c.json({
    user: { id: user.id, username: user.username, email: user.email },
    token,
  });
});

// --- SSO domain-hint API (used by the login form JS) ---

auth.get("/api/sso/domain-hint", async (c) => {
  const domain = String(c.req.query("domain") || "").toLowerCase().trim();
  if (!domain || domain.length > 253) {
    return c.json({ sso: false });
  }
  const [row] = await db
    .select({ orgId: orgSsoConfigs.orgId, provider: orgSsoConfigs.provider })
    .from(orgSsoConfigs)
    .where(eq(orgSsoConfigs.domainHint, domain))
    .limit(1);
  return c.json({ sso: !!row, provider: row?.provider ?? null });
});

/**
 * Pick a friendly provider name for the "Sign in with X" button when the
 * admin hasn't set one explicitly. Looks at the configured IdP URLs.
 * Falls back to undefined so the caller can default to a literal "SSO".
 */
function inferSsoProviderName(
  cfg: { issuer?: string | null; authorizationEndpoint?: string | null } | null | undefined
): string | undefined {
  const urls = [cfg?.issuer, cfg?.authorizationEndpoint]
    .filter((s): s is string => !!s)
    .join(" ")
    .toLowerCase();
  if (!urls) return undefined;
  if (urls.includes("google")) return "Google";
  if (urls.includes("okta")) return "Okta";
  if (urls.includes("microsoftonline") || urls.includes("azure")) return "Microsoft";
  if (urls.includes("auth0.com")) return "Auth0";
  if (urls.includes("authentik")) return "Authentik";
  return undefined;
}

export default auth;
