import { Router, Request, Response, NextFunction } from 'express';
import { z } from 'zod';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { UserRole, UserStatus } from '@prisma/client';
import { authService } from './auth.service';
import { loginSchema, registerSchema, refreshSchema } from './auth.schemas';
import { validate } from '../../middleware/validate';
import { authenticate } from '../../middleware/auth.middleware';
import { authRateLimit, loginRateLimit } from '../../middleware/rate-limit';
import { prisma } from '../../config/database';
import { verificationTokenService } from '../../services/verification-token.service';
import { passwordResetTokenService } from '../../services/password-reset-token.service';
import { emailService } from '../../services/email.service';
import { siteSettingsService } from '../settings/settings.service';
import { env } from '../../config/env';
import { logger } from '../../utils/logger';
import { createVerificationRateLimit, createResetRateLimit } from './auth.rate-limits';
import { profileService } from '../people/profile.service';
import { computeDeviceFingerprint } from '../../utils/device-fingerprint';

const router = Router();

const REFRESH_COOKIE_NAME = 'cml_refresh';
const REFRESH_COOKIE_MAX_AGE = 24 * 60 * 60 * 1000; // 24 hours in ms (matches JWT_REFRESH_EXPIRY default)

const SESSION_COOKIE_NAME = 'cml_session';
const SESSION_COOKIE_MAX_AGE = 30 * 60 * 1000; // 30 min buffer (JWT inside enforces 15min expiry)

/** Set the refresh token as an httpOnly cookie.
 *  Uses req.secure (respects trust proxy + X-Forwarded-Proto) to determine
 *  the Secure flag, so it works correctly over both HTTP (dev) and HTTPS (tunnel). */
function setRefreshCookie(req: Request, res: Response, token: string) {
  res.cookie(REFRESH_COOKIE_NAME, token, {
    httpOnly: true,
    secure: req.secure,
    sameSite: req.secure ? 'strict' : 'lax',
    maxAge: REFRESH_COOKIE_MAX_AGE,
    path: '/api/auth',
  });
}

/** Clear the refresh token cookie */
function clearRefreshCookie(req: Request, res: Response) {
  res.clearCookie(REFRESH_COOKIE_NAME, {
    httpOnly: true,
    secure: req.secure,
    sameSite: req.secure ? 'strict' : 'lax',
    path: '/api/auth',
  });
}

/** Cookie options for the SSO session cookie (domain-wide for Gitea reverse proxy auth) */
function sessionCookieOptions(req: Request) {
  const isSecure = req.secure;
  const domain = env.DOMAIN;
  // Use domain-wide cookie for production (subdomains); omit for localhost dev
  const hasDomain = domain && !domain.includes('localhost') && !domain.match(/^\d/);
  return {
    httpOnly: true,
    secure: isSecure,
    sameSite: 'lax' as const,
    path: '/',
    maxAge: SESSION_COOKIE_MAX_AGE,
    ...(hasDomain ? { domain: `.${domain}` } : {}),
  };
}

/** Set the SSO session cookie for Gitea reverse proxy auth (fire-and-forget).
 *  Only sets the cookie if the user has a provisioned Gitea account. */
async function setSessionCookie(req: Request, res: Response, userId: string) {
  try {
    const user = await prisma.user.findUnique({
      where: { id: userId },
      select: { permissions: true },
    });
    const permissions = (user?.permissions as Record<string, unknown>) || {};
    const giteaUser = permissions._giteaUsername as string | undefined;
    if (!giteaUser) return; // Not provisioned — skip

    const ssoSecret = env.GITEA_SSO_SECRET;
    const token = jwt.sign(
      { sub: userId, giteaUser },
      ssoSecret,
      { algorithm: 'HS256', expiresIn: '15m' },
    );
    res.cookie(SESSION_COOKIE_NAME, token, sessionCookieOptions(req));
  } catch (err) {
    logger.debug('Failed to set SSO session cookie:', err);
  }
}

/** Clear the SSO session cookie */
function clearSessionCookie(req: Request, res: Response) {
  const opts = sessionCookieOptions(req);
  delete (opts as Record<string, unknown>).maxAge;
  res.clearCookie(SESSION_COOKIE_NAME, opts);
}

// POST /api/auth/login
router.post(
  '/login',
  loginRateLimit,
  validate(loginSchema),
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      const result = await authService.login(req.body.email, req.body.password, computeDeviceFingerprint(req));
      // Set refresh token as httpOnly cookie (not in response body)
      setRefreshCookie(req, res, result.refreshToken);
      // Set SSO session cookie for Gitea reverse proxy auth
      await setSessionCookie(req, res, result.user.id);
      const { refreshToken: _, ...responseWithoutRefresh } = result;
      res.json(responseWithoutRefresh);
    } catch (err) {
      next(err);
    }
  }
);

// POST /api/auth/register
router.post(
  '/register',
  authRateLimit,
  validate(registerSchema),
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      const result = await authService.register(req.body, computeDeviceFingerprint(req));
      // Set refresh token as httpOnly cookie if tokens were issued (non-verification path)
      if ('refreshToken' in result && result.refreshToken) {
        setRefreshCookie(req, res, result.refreshToken);
        const { refreshToken: _, ...responseWithoutRefresh } = result;
        res.status(201).json(responseWithoutRefresh);
      } else {
        res.status(201).json(result);
      }
    } catch (err) {
      next(err);
    }
  }
);

// POST /api/auth/verify-email
const verifyEmailSchema = z.object({ token: z.string().min(1) });

router.post(
  '/verify-email',
  authRateLimit,
  validate(verifyEmailSchema),
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      const { token } = req.body;
      const result = await verificationTokenService.verifyToken(token);

      if (!result.valid || !result.userId) {
        res.status(400).json({
          error: { message: result.error || 'Invalid token', code: 'INVALID_TOKEN' },
        });
        return;
      }

      const settings = await siteSettingsService.get();
      const autoApprove = settings.autoApproveVerifiedUsers;

      const newStatus = autoApprove ? UserStatus.ACTIVE : UserStatus.PENDING_APPROVAL;

      await prisma.user.update({
        where: { id: result.userId },
        data: { emailVerified: true, status: newStatus },
      });

      // If not auto-approved, notify admins
      if (!autoApprove) {
        const user = await prisma.user.findUnique({ where: { id: result.userId } });
        if (user) {
          const admins = await prisma.user.findMany({
            where: { role: UserRole.SUPER_ADMIN, status: UserStatus.ACTIVE },
            select: { email: true },
          });
          if (admins.length > 0) {
            await emailService.sendPendingApprovalNotification({
              adminEmails: admins.map(a => a.email),
              newUserEmail: user.email,
              newUserName: user.name || '',
            }).catch(err => logger.error('Failed to send approval notification:', err));
          }
        }
      }

      res.json({
        verified: true,
        approved: autoApprove,
        message: autoApprove
          ? 'Email verified. You can now log in.'
          : 'Email verified. Your account is pending admin approval.',
      });
    } catch (err) {
      next(err);
    }
  }
);

// POST /api/auth/resend-verification
const resendVerificationSchema = z.object({ email: z.string().email() });

router.post(
  '/resend-verification',
  createVerificationRateLimit(),
  validate(resendVerificationSchema),
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      // Always return success to prevent user enumeration
      res.json({ message: 'If your email is registered and pending verification, a new verification link has been sent.' });

      // Send asynchronously (don't block response)
      const user = await prisma.user.findUnique({ where: { email: req.body.email } });
      if (user && user.status === UserStatus.PENDING_VERIFICATION) {
        const token = await verificationTokenService.createToken(user.id);
        const adminUrl = env.ADMIN_URL || 'http://localhost:3000';
        const verificationUrl = `${adminUrl}/verify-email?token=${token}`;
        await emailService.sendVerificationEmail({
          recipientEmail: user.email,
          recipientName: user.name || 'there',
          verificationUrl,
        }).catch(err => logger.error('Failed to resend verification email:', err));
      }
    } catch (err) {
      next(err);
    }
  }
);

// POST /api/auth/forgot-password
const forgotPasswordSchema = z.object({ email: z.string().email() });

router.post(
  '/forgot-password',
  createResetRateLimit(),
  validate(forgotPasswordSchema),
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      // Always return success to prevent user enumeration
      res.json({ message: 'If your email is registered, a password reset link has been sent.' });

      // Send asynchronously
      const user = await prisma.user.findUnique({ where: { email: req.body.email } });
      if (user && user.status === UserStatus.ACTIVE) {
        const token = await passwordResetTokenService.createToken(user.id);
        const adminUrl = env.ADMIN_URL || 'http://localhost:3000';
        const resetUrl = `${adminUrl}/reset-password?token=${token}`;
        await emailService.sendPasswordResetEmail({
          recipientEmail: user.email,
          recipientName: user.name || 'there',
          resetUrl,
        }).catch(err => logger.error('Failed to send password reset email:', err));
      }
    } catch (err) {
      next(err);
    }
  }
);

// POST /api/auth/reset-password
const resetPasswordSchema = z.object({
  token: z.string().min(1),
  password: z.string()
    .min(12, 'Password must be at least 12 characters')
    .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
    .regex(/[a-z]/, 'Password must contain at least one lowercase letter')
    .regex(/[0-9]/, 'Password must contain at least one digit'),
});

router.post(
  '/reset-password',
  authRateLimit,
  validate(resetPasswordSchema),
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      const { token, password } = req.body;

      // Hash password BEFORE token validation to minimize race window
      const hashedPassword = await bcrypt.hash(password, 12);

      // Atomic: validate + consume token + reset password in one transaction
      await prisma.$transaction(async (tx) => {
        // Atomically mark token as used (only if valid and not already used)
        const updated = await tx.passwordResetToken.updateMany({
          where: {
            token,
            usedAt: null,
            expiresAt: { gt: new Date() },
          },
          data: { usedAt: new Date() },
        });

        if (updated.count === 0) {
          throw new Error('INVALID_TOKEN');
        }

        // Get the userId from the consumed token
        const record = await tx.passwordResetToken.findUnique({ where: { token } });
        if (!record) throw new Error('INVALID_TOKEN');

        await tx.user.update({
          where: { id: record.userId },
          data: { password: hashedPassword },
        });
        await tx.refreshToken.deleteMany({ where: { userId: record.userId } });
      });

      res.json({ message: 'Password has been reset. You can now log in with your new password.' });
    } catch (err) {
      if (err instanceof Error && err.message === 'INVALID_TOKEN') {
        res.status(400).json({
          error: { message: 'Invalid or expired reset token', code: 'INVALID_TOKEN' },
        });
        return;
      }
      next(err);
    }
  }
);

// POST /api/auth/refresh
// Accepts refresh token from httpOnly cookie ONLY (2026-04-12: the legacy
// request-body fallback was removed — cookies are HttpOnly+SameSite and
// cannot be read by XSS, while body tokens were reachable via any XSS).
router.post(
  '/refresh',
  authRateLimit,
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      const refreshToken = req.cookies?.[REFRESH_COOKIE_NAME];
      if (!refreshToken) {
        res.status(401).json({ error: { message: 'No refresh token', code: 'INVALID_REFRESH_TOKEN' } });
        return;
      }
      const result = await authService.refreshTokens(refreshToken, computeDeviceFingerprint(req));
      // Set new refresh token as httpOnly cookie
      setRefreshCookie(req, res, result.refreshToken);
      // Renew SSO session cookie for Gitea reverse proxy auth
      if (result.user?.id) {
        await setSessionCookie(req, res, result.user.id);
      }
      const { refreshToken: _, ...responseWithoutRefresh } = result;
      res.json(responseWithoutRefresh);
    } catch (err) {
      clearRefreshCookie(req, res);
      next(err);
    }
  }
);

// POST /api/auth/logout — cookie only (2026-04-12).
router.post(
  '/logout',
  authRateLimit,
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      const refreshToken = req.cookies?.[REFRESH_COOKIE_NAME];
      if (refreshToken) {
        await authService.logout(refreshToken);
      }
      clearRefreshCookie(req, res);
      clearSessionCookie(req, res);
      res.json({ message: 'Logged out' });
    } catch (err) {
      clearRefreshCookie(req, res);
      clearSessionCookie(req, res);
      next(err);
    }
  }
);

// GET /api/auth/me/profile-token
router.get(
  '/me/profile-token',
  authenticate,
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      const userId = req.user!.id;

      // Look up existing Contact linked to this user
      let contact = await prisma.contact.findUnique({ where: { userId } });

      // Auto-create Contact if none exists
      if (!contact) {
        const user = await prisma.user.findUnique({
          where: { id: userId },
          select: { name: true, email: true },
        });
        if (!user) {
          res.status(401).json({ error: { message: 'Invalid token', code: 'INVALID_TOKEN' } });
          return;
        }

        try {
          contact = await prisma.contact.create({
            data: {
              displayName: user.name || user.email,
              firstName: user.name?.split(' ')[0] || null,
              lastName: user.name?.split(' ').slice(1).join(' ') || null,
              email: user.email,
              userId,
              primarySource: 'USER',
            },
          });
        } catch (err: any) {
          // Race condition: another request created the Contact — retry lookup
          if (err.code === 'P2002') {
            contact = await prisma.contact.findUnique({ where: { userId } });
          }
          if (!contact) throw err;
        }
      }

      // Generate profile token if Contact doesn't have one
      if (!contact.profileToken) {
        const result = await profileService.generateProfileToken(contact.id);
        res.json({ token: result.token });
        return;
      }

      res.json({ token: contact.profileToken });
    } catch (err) {
      next(err);
    }
  }
);

// GET /api/auth/me
router.get(
  '/me',
  authenticate,
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      const user = await prisma.user.findUnique({
        where: { id: req.user!.id },
        select: {
          id: true,
          email: true,
          name: true,
          phone: true,
          role: true,
          roles: true,
          status: true,
          permissions: true,
          createdVia: true,
          emailVerified: true,
          lastLoginAt: true,
          createdAt: true,
          updatedAt: true,
        },
      });

      if (!user) {
        res.status(401).json({ error: { message: 'Invalid token', code: 'INVALID_TOKEN' } });
        return;
      }

      res.json(user);
    } catch (err) {
      next(err);
    }
  }
);

export { router as authRouter };
