import NextAuth from 'next-auth'
import EmailProvider from 'next-auth/providers/email'
import CredentialsProvider from 'next-auth/providers/credentials'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { prisma } from './prisma'
import { sendMagicLinkEmail } from './email'
import { verifyPassword } from './password'
import type { UserRole } from '@prisma/client'
import { authConfig } from './auth.config'

// Failed login attempt tracking (in-memory)
const failedAttempts = new Map<string, { count: number; lockedUntil: number }>()
const MAX_LOGIN_ATTEMPTS = 5
const LOCKOUT_DURATION_MS = 15 * 60 * 1000 // 15 minutes

export const { handlers, auth, signIn, signOut } = NextAuth({
  ...authConfig,
  logger: {
    error(error) {
      // CredentialsSignin is expected (wrong password, bots) — already logged to AuditLog with detail
      // Check both name and type: name gets minified in production builds
      if (error?.name === 'CredentialsSignin' || (error as unknown as { type?: string })?.type === 'CredentialsSignin') return
      console.error('[auth][error]', error)
    },
    warn(code) {
      console.warn('[auth][warn]', code)
    },
  },
  adapter: {
    ...PrismaAdapter(prisma),
    // Block auto-creation of users via magic link — only pre-created users can log in
    createUser: () => {
      throw new Error('Self-registration is not allowed. Please contact an administrator.')
    },
    async useVerificationToken({ identifier, token }: { identifier: string; token: string }) {
      try {
        return await prisma.verificationToken.delete({
          where: { identifier_token: { identifier, token } },
        })
      } catch (e) {
        if ((e as { code?: string }).code === 'P2025') return null
        throw e
      }
    },
  },
  providers: [
    // Email provider for magic links (only for existing active users)
    EmailProvider({
      // Server config required by NextAuth validation but not used —
      // sendVerificationRequest below fully overrides email sending via getTransporter()
      server: {
        host: process.env.SMTP_HOST || 'localhost',
        port: Number(process.env.SMTP_PORT || 587),
        auth: {
          user: process.env.SMTP_USER || '',
          pass: process.env.SMTP_PASS || '',
        },
      },
      from: process.env.EMAIL_FROM || 'MOPC Platform <noreply@monaco-opc.com>',
      maxAge: parseInt(process.env.MAGIC_LINK_EXPIRY || '900'), // 15 minutes
      sendVerificationRequest: async ({ identifier: email, url }) => {
        // Send magic links to any existing, non-suspended user.
        // This is the primary first-login path for applicants seeded from CSV
        // (status NONE) who have no password yet.
        const existingUser = await prisma.user.findFirst({
          where: { email: { equals: email.toLowerCase().trim(), mode: 'insensitive' } },
          select: { id: true, status: true },
        })
        if (!existingUser || existingUser.status === 'SUSPENDED') {
          // Silently skip — don't reveal whether the email exists (prevents enumeration)
          console.log(`[auth] Magic link requested for suspended/unknown email: ${email}`)
          return
        }
        await sendMagicLinkEmail(email, url)

        // Audit: magic link sent
        await prisma.auditLog.create({
          data: {
            userId: existingUser.id,
            action: 'MAGIC_LINK_SENT',
            entityType: 'User',
            entityId: existingUser.id,
            detailsJson: { email, timestamp: new Date().toISOString() },
          },
        }).catch(() => {})
      },
    }),
    // Credentials provider for email/password login and invite token auth
    CredentialsProvider({
      name: 'credentials',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' },
        inviteToken: { label: 'Invite Token', type: 'text' },
      },
      async authorize(credentials) {
        // Handle invite token authentication
        if (credentials?.inviteToken) {
          const token = credentials.inviteToken as string
          const user = await prisma.user.findUnique({
            where: { inviteToken: token },
            select: {
              id: true,
              email: true,
              name: true,
              role: true,
              roles: true,
              status: true,
              inviteTokenExpiresAt: true,
              passwordHash: true,
            },
          })

          if (!user || user.status === 'SUSPENDED') {
            return null
          }

          if (user.inviteTokenExpiresAt && user.inviteTokenExpiresAt < new Date()) {
            return null
          }

          // For users without a password, force them through /set-password.
          // For users who already have one (e.g. an admin issued a fresh
          // access link to bypass email delivery), let them keep it — this
          // is a magic-login, not a forced password reset.
          const needsPasswordSetup = !user.passwordHash

          await prisma.user.update({
            where: { id: user.id },
            data: {
              inviteToken: null,
              inviteTokenExpiresAt: null,
              status: 'ACTIVE',
              mustSetPassword: needsPasswordSetup,
              lastLoginAt: new Date(),
            },
          })

          // Log invitation accepted
          await prisma.auditLog.create({
            data: {
              userId: user.id,
              action: 'INVITATION_ACCEPTED',
              entityType: 'User',
              entityId: user.id,
              detailsJson: { email: user.email, role: user.role, magicLogin: !needsPasswordSetup },
            },
          }).catch(() => {})

          return {
            id: user.id,
            email: user.email,
            name: user.name,
            role: user.role,
            roles: user.roles.length ? user.roles : [user.role],
            mustSetPassword: needsPasswordSetup,
          }
        }

        if (!credentials?.email || !credentials?.password) {
          return null
        }

        const email = (credentials.email as string).toLowerCase()
        const password = credentials.password as string

        // Check if account is temporarily locked
        const attempts = failedAttempts.get(email)
        if (attempts && Date.now() < attempts.lockedUntil) {
          throw new Error('Account temporarily locked due to too many failed attempts. Try again later.')
        }

        // Find user by email (case-insensitive — DB may store mixed-case emails)
        const user = await prisma.user.findFirst({
          where: { email: { equals: email, mode: 'insensitive' } },
          select: {
            id: true,
            email: true,
            name: true,
            role: true,
            roles: true,
            status: true,
            passwordHash: true,
            mustSetPassword: true,
          },
        })

        if (!user || user.status === 'SUSPENDED') {
          // Track failed attempt (don't reveal whether user exists)
          const current = failedAttempts.get(email) || { count: 0, lockedUntil: 0 }
          current.count++
          const wasLocked = current.count >= MAX_LOGIN_ATTEMPTS
          if (wasLocked) {
            current.lockedUntil = Date.now() + LOCKOUT_DURATION_MS
            current.count = 0
          }
          failedAttempts.set(email, current)

          // Log failed login — real security event
          await prisma.auditLog.create({
            data: {
              userId: null,
              action: 'LOGIN_FAILED',
              entityType: 'User',
              detailsJson: { email, reason: !user ? 'user_not_found' : 'suspended' },
            },
          }).catch(() => {})

          // Log account lockout as a distinct security event
          if (wasLocked) {
            await prisma.auditLog.create({
              data: {
                userId: null,
                action: 'ACCOUNT_LOCKED',
                entityType: 'User',
                detailsJson: {
                  email,
                  reason: 'max_failed_attempts',
                  lockoutDurationMinutes: LOCKOUT_DURATION_MS / 60000,
                },
              },
            }).catch(() => {})
          }

          return null
        }

        if (!user.passwordHash) {
          // Magic-link user tried credentials form — expected, not a security event
          return null
        }

        // Verify password
        const isValid = await verifyPassword(password, user.passwordHash)
        if (!isValid) {
          // Track failed attempt
          const current = failedAttempts.get(email) || { count: 0, lockedUntil: 0 }
          current.count++
          const wasLocked = current.count >= MAX_LOGIN_ATTEMPTS
          if (wasLocked) {
            current.lockedUntil = Date.now() + LOCKOUT_DURATION_MS
            current.count = 0
          }
          failedAttempts.set(email, current)

          // Log failed login
          await prisma.auditLog.create({
            data: {
              userId: user.id,
              action: 'LOGIN_FAILED',
              entityType: 'User',
              entityId: user.id,
              detailsJson: { email, reason: 'invalid_password' },
            },
          }).catch(() => {})

          // Log account lockout as a distinct security event
          if (wasLocked) {
            await prisma.auditLog.create({
              data: {
                userId: user.id,
                action: 'ACCOUNT_LOCKED',
                entityType: 'User',
                entityId: user.id,
                detailsJson: {
                  email,
                  reason: 'max_failed_attempts',
                  lockoutDurationMinutes: LOCKOUT_DURATION_MS / 60000,
                },
              },
            }).catch(() => {})
          }

          return null
        }

        // Clear failed attempts on successful login
        failedAttempts.delete(email)

        return {
          id: user.id,
          email: user.email,
          name: user.name,
          role: user.role,
          roles: user.roles.length ? user.roles : [user.role],
          mustSetPassword: user.mustSetPassword,
        }
      },
    }),
  ],
  callbacks: {
    ...authConfig.callbacks,
    async jwt({ token, user, trigger, session }) {
      // Initial sign in
      if (user) {
        token.id = user.id as string
        token.role = user.role as UserRole
        token.roles = user.roles?.length ? user.roles : [user.role as UserRole]
        token.mustSetPassword = user.mustSetPassword
      }

      // On session update, handle impersonation or normal refresh
      if (trigger === 'update') {
        // Start impersonation
        if (session?.impersonate && typeof session.impersonate === 'string') {
          // Only SUPER_ADMIN can impersonate (defense-in-depth)
          if (token.role === 'SUPER_ADMIN' && !token.impersonating) {
            const targetUser = await prisma.user.findUnique({
              where: { id: session.impersonate },
              select: { id: true, email: true, name: true, role: true, roles: true, status: true },
            })
            if (targetUser && targetUser.status !== 'SUSPENDED' && targetUser.role !== 'SUPER_ADMIN') {
              // Save original admin identity
              token.impersonating = {
                originalId: token.id as string,
                originalRole: token.role as UserRole,
                originalRoles: (token.roles as UserRole[]) ?? [token.role as UserRole],
                originalEmail: token.email as string,
              }
              // Swap to target user
              token.id = targetUser.id
              token.email = targetUser.email
              token.name = targetUser.name
              token.role = targetUser.role
              token.roles = targetUser.roles.length ? targetUser.roles : [targetUser.role]
              token.mustSetPassword = false
            }
          }
        }
        // End impersonation
        else if (session?.endImpersonation && token.impersonating) {
          const original = token.impersonating as { originalId: string; originalRole: UserRole; originalRoles: UserRole[]; originalEmail: string }
          token.id = original.originalId
          token.role = original.originalRole
          token.roles = original.originalRoles
          token.email = original.originalEmail
          token.impersonating = undefined
          token.mustSetPassword = false
          // Refresh original admin's name
          const adminUser = await prisma.user.findUnique({
            where: { id: original.originalId },
            select: { name: true },
          })
          if (adminUser) {
            token.name = adminUser.name
          }
        }
        // Normal session refresh
        else {
          const dbUser = await prisma.user.findUnique({
            where: { id: token.id as string },
            select: { role: true, roles: true, mustSetPassword: true },
          })
          if (dbUser) {
            token.role = dbUser.role
            token.roles = dbUser.roles.length ? dbUser.roles : [dbUser.role]
            // Don't override mustSetPassword=false during impersonation
            if (!token.impersonating) {
              token.mustSetPassword = dbUser.mustSetPassword
            }
          }
        }
      }

      return token
    },
    async session({ session, token }) {
      if (token && session.user) {
        session.user.id = token.id as string
        session.user.role = token.role as UserRole
        session.user.roles = (token.roles as UserRole[]) ?? [token.role as UserRole]
        session.user.mustSetPassword = token.mustSetPassword as boolean | undefined
        session.user.impersonating = token.impersonating as typeof session.user.impersonating
      }
      return session
    },
    async signIn({ user, account }) {
      // For email provider (magic link), check user status and get password info
      if (account?.provider === 'email') {
        const dbUser = await prisma.user.findFirst({
          where: { email: { equals: user.email!, mode: 'insensitive' } },
          select: {
            id: true,
            status: true,
            passwordHash: true,
            mustSetPassword: true,
            role: true,
            roles: true,
          },
        })

        // Block non-existent users (defense-in-depth against adapter auto-creation)
        if (!dbUser) {
          return false
        }

        if (dbUser.status === 'SUSPENDED') {
          return false // Block suspended users
        }

        // Note: status stays INVITED/NONE until onboarding completes.
        // The completeOnboarding mutation sets status to ACTIVE.

        // Add user data for JWT callback
        user.id = dbUser.id
        user.role = dbUser.role
        user.roles = dbUser.roles.length ? dbUser.roles : [dbUser.role]
        user.mustSetPassword = dbUser.mustSetPassword || !dbUser.passwordHash
      }

      // Update last login time on actual sign-in, and activate INVITED users on login
      if (user.email) {
        const loginUser = await prisma.user.findFirst({
          where: { email: { equals: user.email, mode: 'insensitive' } },
          select: { id: true, status: true },
        })
        if (loginUser) {
          await prisma.user.update({
            where: { id: loginUser.id },
            data: {
              lastLoginAt: new Date(),
              // If user is still INVITED but successfully logged in, activate them
              ...(loginUser.status === 'INVITED'
                ? { status: 'ACTIVE' }
                : {}),
            },
          }).catch(() => {
            // Ignore errors from updating last login
          })
        }

        // Log successful login
        await prisma.auditLog.create({
          data: {
            userId: user.id as string,
            action: 'LOGIN_SUCCESS',
            entityType: 'User',
            entityId: user.id as string,
            detailsJson: { method: account?.provider || 'unknown', email: user.email },
          },
        }).catch(() => {})
      }

      return true
    },
    async redirect({ url, baseUrl }) {
      // Check if user needs to set password and redirect accordingly
      // This is called after successful authentication
      if (url.startsWith(baseUrl)) {
        return url
      }
      // Allow relative redirects
      if (url.startsWith('/')) {
        return `${baseUrl}${url}`
      }
      return baseUrl
    },
  },
})

// Helper to get session in server components
export async function getServerSession() {
  return auth()
}

// Helper to require authentication
export async function requireAuth() {
  const session = await auth()
  if (!session?.user) {
    throw new Error('Unauthorized')
  }
  return session
}

// Helper to require specific role(s)
export async function requireRole(...roles: UserRole[]) {
  const session = await requireAuth()
  // Use roles array, fallback to [role] for stale JWT tokens
  const userRoles = session.user.roles?.length ? session.user.roles : [session.user.role]
  if (!roles.some(r => userRoles.includes(r))) {
    throw new Error('Forbidden')
  }
  return session
}
