import { betterAuth } from "better-auth";
import { genericOAuth, twoFactor } from "better-auth/plugins";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db, users, sessions, accounts, verifications, twoFactors, eq, count } from "@epicure/db";
import { sendEmail, verifyEmailHtml, resetPasswordHtml, welcomeHtml } from "@/lib/email";
import { isSignupsDisabled } from "@/lib/site-settings";
import { findValidInvite, consumeInvite, INVITE_COOKIE } from "@/lib/invites";
import { generateUniqueUsername } from "@/lib/username";
import { getRedis } from "@/lib/redis";
import { dispatchAdminWebhook } from "@/lib/admin-webhooks";

// Without this, better-auth's own rate limiter (sign-in/sign-up/2FA/password
// reset throttles below) defaults to an in-process Map — fine for a single
// instance, but each replica gets its own independent counter the moment
// this app is ever scaled horizontally, multiplying the effective limit by
// the instance count. Backing it with the same Redis used elsewhere
// (lib/rate-limit.ts) makes the limit actually shared and enforceable.
const redisSecondaryStorage = {
  async get(key: string) {
    return getRedis().get(key);
  },
  async set(key: string, value: string, ttl?: number) {
    if (ttl) await getRedis().set(key, value, "EX", ttl);
    else await getRedis().set(key, value);
  },
  async delete(key: string) {
    await getRedis().del(key);
  },
};

export const auth = betterAuth({
  trustedOrigins: [process.env["BETTER_AUTH_URL"] ?? "http://localhost:3000"],

  secondaryStorage: redisSecondaryStorage,

  // Explicit rather than relying on the isProduction default so dev/staging
  // are protected too. Sign-in/sign-up/change-password/change-email get a
  // strict built-in 3-req/10s-per-IP rule (better-auth's default special
  // rules) — everything else on /api/auth falls back to 100/10s.
  rateLimit: {
    enabled: true,
    // A 6-digit TOTP/backup code has far fewer combinations than a password —
    // the generic 100-req/10s default is too loose to meaningfully slow down
    // guessing it.
    customRules: {
      "/two-factor/verify-totp": { window: 60, max: 5 },
      "/two-factor/verify-otp": { window: 60, max: 5 },
      "/two-factor/verify-backup-code": { window: 60, max: 5 },
    },
  },

  database: drizzleAdapter(db, {
    provider: "pg",
    schema: { user: users, session: sessions, account: accounts, verification: verifications, twoFactor: twoFactors },
  }),

  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
    // A stolen session should not survive the account owner noticing and
    // resetting their password.
    revokeSessionsOnPasswordReset: true,
    sendResetPassword: async ({ user, url }) => {
      await sendEmail({
        to: user.email,
        subject: "Reset your Epicure password",
        html: resetPasswordHtml(url),
      });
    },
  },

  emailVerification: {
    sendOnSignUp: true,
    autoSignInAfterVerification: true,
    sendVerificationEmail: async ({ user, url }) => {
      await sendEmail({
        to: user.email,
        subject: "Verify your Epicure email",
        html: verifyEmailHtml(url),
      });
    },
  },

  socialProviders: {
    google: {
      clientId: process.env["GOOGLE_CLIENT_ID"] ?? "",
      clientSecret: process.env["GOOGLE_CLIENT_SECRET"] ?? "",
    },
    ...(process.env["GITHUB_CLIENT_ID"] && {
      github: {
        clientId: process.env["GITHUB_CLIENT_ID"],
        clientSecret: process.env["GITHUB_CLIENT_SECRET"] ?? "",
      },
    }),
    ...(process.env["DISCORD_CLIENT_ID"] && {
      discord: {
        clientId: process.env["DISCORD_CLIENT_ID"],
        clientSecret: process.env["DISCORD_CLIENT_SECRET"] ?? "",
      },
    }),
  },

  plugins: [
    // allowPasswordless: OAuth-only accounts (no credential/password account)
    // would otherwise never be able to enable or manage 2FA, since the
    // endpoint requires a password to confirm identity by default.
    twoFactor({ issuer: "Epicure", allowPasswordless: true }),
    ...(process.env["AUTHENTIK_CLIENT_ID"] && process.env["AUTHENTIK_BASE_URL"] ? [
      genericOAuth({
        config: [
          {
            providerId: "authentik",
            clientId: process.env["AUTHENTIK_CLIENT_ID"],
            clientSecret: process.env["AUTHENTIK_CLIENT_SECRET"] ?? "",
            // Authentik OIDC discovery URL: https://<your-authentik-domain>/application/o/<slug>/
            discoveryUrl: `${process.env["AUTHENTIK_BASE_URL"]}/.well-known/openid-configuration`,
            scopes: ["openid", "email", "profile"],
          },
        ],
      }),
    ] : []),
  ],

  session: {
    cookieCache: {
      enabled: true,
      maxAge: 60 * 5,
    },
    // secondaryStorage above is only meant to back the rate limiter — without
    // this, better-auth would also move session storage itself into Redis
    // (see internal-adapter.mjs's `storeInDb` checks), which is an unrelated,
    // much bigger behavior change (session data no longer queryable from the
    // `sessions` table at all) that nothing else in this app expects.
    storeSessionInDatabase: true,
  },

  databaseHooks: {
    user: {
      create: {
        before: async (user, context) => {
          // No signup form or settings page ever lets someone set a username,
          // yet profiles, follows, and people-search all key on it — so every
          // account needs one generated here, or those features silently see
          // nobody. OAuth signups may already have one (mapped from the
          // provider profile); email/password never does.
          const existingUsername = (user as { username?: string | null }).username;
          const username = existingUsername || (await generateUniqueUsername(user.name || user.email));

          if (!(await isSignupsDisabled())) return { data: { ...user, username } };

          const token = context?.getCookie(INVITE_COOKIE);
          const invite = token ? await findValidInvite(token, user.email) : null;
          if (!invite) return false;

          return { data: { ...user, username, role: invite.role, tier: invite.tier } };
        },
        after: async (user, context) => {
          // First registered user becomes admin
          const result = await db.select({ total: count() }).from(users);
          if ((result[0]?.total ?? 0) === 1) {
            await db.update(users).set({ role: "admin" }).where(eq(users.id, user.id));
          }

          // Gravatar is opt-in (Settings → Profile), off by default — no
          // automatic Gravatar lookup at signup. OAuth providers still set
          // `image` (mapped to avatarUrl) before this hook runs; email/
          // password signups just get the initials fallback until the user
          // uploads a photo or opts into Gravatar.

          // Consume the invite that gated this signup, if any (regardless of
          // whether signups have since been re-enabled/disabled).
          const token = context?.getCookie(INVITE_COOKIE);
          const invite = token ? await findValidInvite(token, user.email) : null;
          if (invite) await consumeInvite(invite.id, user.id);

          // Welcome email (fire and forget)
          sendEmail({
            to: user.email,
            subject: "Welcome to Epicure",
            html: welcomeHtml(user.name),
          }).catch(() => {});

          void dispatchAdminWebhook("user.signed_up", { id: user.id, email: user.email, name: user.name });
        },
      },
    },
  },

  user: {
    fields: {
      image: "avatarUrl",
    },
    additionalFields: {
      role: {
        type: "string",
        defaultValue: "user",
        input: false,
      },
      tier: {
        type: "string",
        defaultValue: "free",
        input: false,
      },
      username: {
        type: "string",
        required: false,
      },
      bio: {
        type: "string",
        required: false,
      },
      unitPref: {
        type: "string",
        defaultValue: "metric",
      },
      locale: {
        type: "string",
        defaultValue: "en",
      },
    },
    changeEmail: {
      enabled: true,
      sendChangeEmailVerification: async ({ newEmail, url }: { newEmail: string; url: string }) => {
        await sendEmail({
          to: newEmail,
          subject: "Verify your new Epicure email",
          html: verifyEmailHtml(url),
        });
      },
    },
  },
});

export type Session = typeof auth.$Infer.Session;
export type User = typeof auth.$Infer.Session.user;
