import "server-only";
import NextAuth from "next-auth";
import type { Provider } from "next-auth/providers";
import Credentials from "next-auth/providers/credentials";
import Google from "next-auth/providers/google";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { db } from "@/lib/db/client";
import { users, accounts, sessions, verificationTokens } from "@/lib/db/schema";
import { loadEnv } from "@/lib/env";
import { verifyPassword } from "./password";
import { authConfig } from "./auth.config";

const env = loadEnv();

const credentialsSchema = z.object({
  email: z.string().email(),
  password: z.string().min(1),
});

// Build the providers array dynamically — Google only appears if both
// GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are set in the environment.
// The credentials provider is always enabled.
const providers: Provider[] = [
  Credentials({
    credentials: { email: {}, password: {} },
    async authorize(raw) {
      const parsed = credentialsSchema.safeParse(raw);
      if (!parsed.success) return null;
      const { email, password } = parsed.data;
      const [user] = await db.select().from(users).where(eq(users.email, email)).limit(1);
      if (!user || !user.passwordHash) return null;
      if (user.status !== "active") return null;
      const ok = await verifyPassword(password, user.passwordHash);
      if (!ok) return null;
      return {
        id: user.id,
        email: user.email,
        name: user.name,
        role: user.role,
        // Included so the jwt() callback can stamp it into the token and
        // revoke all prior sessions when the counter increments.
        tokenVersion: user.tokenVersion,
      } as never;
    },
  }),
];

if (env.googleAuthEnabled) {
  providers.push(
    Google({
      clientId: env.googleClientId,
      clientSecret: env.googleClientSecret,
      // Safe here: Google always verifies email ownership, and we still gate
      // in the signIn callback below against an existing `active` user row.
      allowDangerousEmailAccountLinking: true,
    }),
  );
}

// NOTE: Credentials providers always use JWT session tokens in Auth.js v5 regardless
// of the session.strategy config — the callback action calls jwt() and encodes a
// JWT cookie directly. We therefore use strategy:"jwt" explicitly (in authConfig)
// and carry id/role through the jwt callback into the session callback.

// When a session token is older than this, the jwt() callback re-reads the
// user row so role/status/name changes propagate without requiring re-login.
const SESSION_REFRESH_MS = 15 * 60 * 1000;

export const { handlers, auth, signIn, signOut } = NextAuth({
  ...authConfig,
  adapter: DrizzleAdapter(db, {
    usersTable: users,
    accountsTable: accounts,
    sessionsTable: sessions,
    verificationTokensTable: verificationTokens,
  }),
  providers,
  callbacks: {
    ...authConfig.callbacks,
    /**
     * Gate Google sign-in on the vetted-membership model: the Google email
     * must match an existing `active` user row. New Google identities get
     * redirected to /login with an error — they must apply at /join first.
     * Credentials sign-ins are already gated in the authorize() above.
     */
    async signIn({ user, account }) {
      if (account?.provider === "credentials") return true;
      if (!account) return true;

      const email = user.email?.toLowerCase();
      if (!email) return "/login?error=no_email";

      const [existing] = await db
        .select({ id: users.id, status: users.status, tokenVersion: users.tokenVersion })
        .from(users)
        .where(eq(users.email, email))
        .limit(1);

      if (!existing) return "/login?error=not_registered";
      if (existing.status === "pending") return "/login?error=pending_approval";
      if (existing.status !== "active") return "/login?error=not_active";

      return true;
    },
    async jwt({ token, user, trigger }) {
      type MutableToken = typeof token & {
        role?: string;
        status?: string;
        name?: string | null;
        refreshedAt?: number;
        tokenVersion?: number;
      };
      const t = token as MutableToken;
      if (user) {
        const u = user as {
          id?: string;
          role?: string;
          name?: string | null;
          tokenVersion?: number;
        };
        if (u.id) t.sub = u.id;
        if (u.role) t.role = u.role;
        if (u.name) t.name = u.name;
        t.status = "active";
        t.tokenVersion = u.tokenVersion ?? 0;
        t.refreshedAt = Date.now();
        return t;
      }
      const stale =
        !t.refreshedAt || Date.now() - (t.refreshedAt as number) > SESSION_REFRESH_MS;
      if (trigger === "update" || stale) {
        if (t.sub) {
          const [row] = await db
            .select({
              id: users.id,
              status: users.status,
              role: users.role,
              name: users.name,
              tokenVersion: users.tokenVersion,
            })
            .from(users)
            .where(eq(users.id, t.sub as string))
            .limit(1);
          if (!row || row.status !== "active") {
            t.status = row?.status ?? "deleted";
          } else if (
            typeof t.tokenVersion === "number" &&
            t.tokenVersion < row.tokenVersion
          ) {
            // An admin or the user themselves bumped the version — every
            // token issued before this point is now invalid.
            t.status = "revoked";
          } else {
            t.role = row.role;
            t.status = row.status;
            t.name = row.name;
            t.tokenVersion = row.tokenVersion;
          }
          t.refreshedAt = Date.now();
        }
      }
      return t;
    },
    async session({ session, token }) {
      const t = token as { sub?: string; role?: string; status?: string };
      if (t.status && t.status !== "active") {
        return { ...session, user: undefined as never };
      }
      if (session.user) {
        (session.user as { id?: string }).id = t.sub;
        (session.user as { role?: string }).role = t.role ?? "user";
      }
      return session;
    },
  },
});
