import { prisma } from "@capakraken/db";
import { authRateLimiter } from "@capakraken/api/middleware/rate-limit";
import { createAuditEntry } from "@capakraken/api/lib/audit";
import { logger } from "@capakraken/api/lib/logger";
import NextAuth, { type NextAuthConfig } from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { CredentialsSignin } from "next-auth";
import { verify } from "@node-rs/argon2";
import { z } from "zod";
import { assertSecureRuntimeEnv } from "./runtime-env";
import { authConfig } from "./auth.config.js";

assertSecureRuntimeEnv();

// Auth.js v5: throw CredentialsSignin subclasses so the `code` is forwarded
// to the client via SignInResponse.code — plain Error throws become
// CallbackRouteError and the message is never visible to the client.
export class MfaRequiredError extends CredentialsSignin {
  code = "MFA_REQUIRED" as const;
}
export class MfaRequiredSetupError extends CredentialsSignin {
  code = "MFA_REQUIRED_SETUP" as const;
}
export class InvalidTotpError extends CredentialsSignin {
  code = "INVALID_TOTP" as const;
}

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

const config = {
  ...authConfig,
  trustHost: true,
  providers: [
    Credentials({
      name: "credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
        totp: { label: "TOTP", type: "text" },
      },
      async authorize(credentials) {
        const parsed = LoginSchema.safeParse(credentials);
        if (!parsed.success) return null;

        const { email, password, totp } = parsed.data;
        const isE2eTestMode = process.env["E2E_TEST_MODE"] === "true";

        // Rate limit: 5 login attempts per 15 minutes per email
        const rateLimitResult = isE2eTestMode
          ? { allowed: true }
          : await authRateLimiter(email.toLowerCase());
        if (!rateLimitResult.allowed) {
          // Audit failed login (rate limited)
          void createAuditEntry({
            db: prisma,
            entityType: "Auth",
            entityId: email.toLowerCase(),
            entityName: email,
            action: "CREATE",
            summary: "Login blocked — rate limit exceeded",
            source: "ui",
          });
          throw new Error("Too many login attempts. Please try again later.");
        }

        const user = await prisma.user.findUnique({ where: { email } });
        if (!user?.passwordHash) {
          logger.warn({ email, reason: "user_not_found" }, "Failed login attempt");
          void createAuditEntry({
            db: prisma,
            entityType: "Auth",
            entityId: email.toLowerCase(),
            entityName: email,
            action: "CREATE",
            summary: "Login failed — user not found",
            source: "ui",
          });
          return null;
        }

        if (!user.isActive) {
          logger.warn({ email, userId: user.id, reason: "account_deactivated" }, "Login blocked — account deactivated");
          void createAuditEntry({
            db: prisma,
            entityType: "Auth",
            entityId: user.id,
            entityName: user.email,
            action: "CREATE",
            userId: user.id,
            summary: "Login blocked — account deactivated",
            source: "ui",
          });
          return null;
        }

        const isValid = await verify(user.passwordHash, password);
        if (!isValid) {
          logger.warn({ email, reason: "invalid_password" }, "Failed login attempt");
          // Audit failed login (bad password)
          void createAuditEntry({
            db: prisma,
            entityType: "Auth",
            entityId: user.id,
            entityName: user.email,
            action: "CREATE",
            userId: user.id,
            summary: "Login failed — invalid password",
            source: "ui",
          });
          return null;
        }

        // MFA check: if TOTP is enabled, require the token
        if (user.totpEnabled && user.totpSecret) {
          if (!totp) {
            // Signal to the client that MFA is required (include userId for re-submission)
            throw new MfaRequiredError();
          }

          const { TOTP, Secret } = await import("otpauth");
          const totpInstance = new TOTP({
            issuer: "CapaKraken",
            label: user.email,
            algorithm: "SHA1",
            digits: 6,
            period: 30,
            secret: Secret.fromBase32(user.totpSecret),
          });

          const delta = totpInstance.validate({ token: totp, window: 1 });
          if (delta === null) {
            logger.warn({ email, reason: "invalid_totp" }, "Failed MFA verification");
            void createAuditEntry({
              db: prisma,
              entityType: "Auth",
              entityId: user.id,
              entityName: user.email,
              action: "CREATE",
              userId: user.id,
              summary: "Login failed — invalid TOTP token",
              source: "ui",
            });
            throw new InvalidTotpError();
          }
        }

        // MFA enforcement: if the user's role is in requireMfaForRoles but they
        // haven't set up TOTP yet, block login and signal setup requirement.
        if (!user.totpEnabled) {
          const settings = await prisma.systemSettings.findUnique({
            where: { id: "singleton" },
            select: { requireMfaForRoles: true },
          });
          const requireMfaForRoles = (settings?.requireMfaForRoles as string[] | null) ?? [];
          if (requireMfaForRoles.includes(user.systemRole)) {
            throw new MfaRequiredSetupError();
          }
        }

        // Track last login time
        await prisma.user.update({
          where: { id: user.id },
          data: { lastLoginAt: new Date() },
        });

        logger.info({ email, userId: user.id }, "Successful login");
        // Audit successful login
        void createAuditEntry({
          db: prisma,
          entityType: "Auth",
          entityId: user.id,
          entityName: user.email,
          action: "CREATE",
          userId: user.id,
          summary: "User logged in",
          source: "ui",
        });

        return {
          id: user.id,
          email: user.email,
          name: user.name,
          role: user.systemRole,
        };
      },
    }),
  ],
  callbacks: {
    async session({ session, token }) {
      if (token.sub) {
        session.user.id = token.sub;
      }
      if (token.role) {
        (session.user as typeof session.user & { role: string }).role = token.role as string;
      }
      // Use token.sid (not token.jti) to avoid conflict with Auth.js's internal JWT ID claim
      if (token.sid) {
        (session.user as typeof session.user & { jti: string }).jti = token.sid as string;
      }
      return session;
    },
    async jwt({ token, user }) {
      if (user) {
        token.role = (user as typeof user & { role: string }).role;

        // Generate a unique session ID for tracking.
        // We use token.sid (not token.jti) because Auth.js manages token.jti
        // internally and may overwrite it after the jwt callback returns.
        const jti = crypto.randomUUID();
        token.sid = jti;

        // Skip active-session registration in E2E test mode.
        // Test logins must not pollute the active_sessions table — doing so
        // kicks real user sessions when the concurrent-session limit is reached.
        const isE2eTestMode = process.env["E2E_TEST_MODE"] === "true";
        if (isE2eTestMode) return token;

        // Enforce concurrent session limit (kick-oldest strategy)
        try {
          const settings = await prisma.systemSettings.findUnique({
            where: { id: "singleton" },
            select: { maxConcurrentSessions: true },
          });
          const maxSessions = settings?.maxConcurrentSessions ?? 3;

          // Register this new session
          await prisma.activeSession.create({
            data: { userId: user.id!, jti },
          });

          // Count active sessions and delete the oldest if over the limit
          const activeSessions = await prisma.activeSession.findMany({
            where: { userId: user.id! },
            orderBy: { createdAt: "asc" },
            select: { id: true },
          });

          if (activeSessions.length > maxSessions) {
            const toDelete = activeSessions.slice(0, activeSessions.length - maxSessions);
            await prisma.activeSession.deleteMany({
              where: { id: { in: toDelete.map((s) => s.id) } },
            });
            logger.info({ userId: user.id, kicked: toDelete.length, maxSessions }, "Kicked oldest sessions");
          }
        } catch (err) {
          // Non-blocking: don't prevent login if session tracking fails
          logger.error({ err }, "Failed to enforce concurrent session limit");
        }
      }
      return token;
    },
  },
  events: {
    async signOut(message) {
      // Auth.js fires this event on sign-out; extract userId from the JWT token
      const token = "token" in message ? message.token : null;
      const userId = token?.sub ?? null;
      const email = token?.email ?? "unknown";
      const jti = (token?.sid ?? token?.jti) as string | undefined;

      // Remove from active session registry
      if (jti) {
        void prisma.activeSession.delete({ where: { jti } }).catch(() => { /* already gone */ });
      }

      void createAuditEntry({
        db: prisma,
        entityType: "Auth",
        entityId: userId ?? email,
        entityName: email,
        action: "DELETE",
        ...(userId ? { userId } : {}),
        summary: "User logged out",
        source: "ui",
      });
    },
  },
} satisfies NextAuthConfig;

export const { handlers, auth } = NextAuth(config);
