import { NextResponse } from 'next/server';
import bcrypt from 'bcryptjs';
import { eq, and } from 'drizzle-orm';
import { db, users, restaurants, branches } from '@server/db/drizzle';
import { createSessionToken, COOKIE_NAME } from '@server/auth';
import { initDatabase } from '@server/db/init';
import { withErrorHandler } from '@server/middleware/withErrorHandler';
import { withValidation, ParsedRequest } from '@server/middleware/withValidation';
import { signInSchema, SignInInput } from '@server/validators/auth.validator';
import { AuthError } from '@server/errors';
import {
  withRateLimit,
  getClientIp,
  isLockedOut,
  recordFailure,
  clearFailures,
  checkRateLimit,
} from '@server/middleware/withRateLimit';
import { dispatchWebhook } from '@server/services/webhooks.service';

const LOCKOUT_SCOPE = 'auth:sign-in:fail';

const signIn = withErrorHandler(
  withRateLimit(
    [
      { scope: 'auth:sign-in:ip-min', limit: 10, windowMs: 60_000, keyOf: (req) => getClientIp(req) },
      { scope: 'auth:sign-in:ip-hour', limit: 50, windowMs: 60 * 60_000, keyOf: (req) => getClientIp(req) },
    ],
    withValidation(signInSchema, async (req: ParsedRequest<SignInInput>) => {
      await initDatabase();
      const { email, password } = req.parsedBody;
      const normalizedEmail = email.toLowerCase().trim();

      // Per-email rate limit (post-validation since we need parsed email)
      const emailMin = checkRateLimit('auth:sign-in:email-min', normalizedEmail, 5, 60_000);
      if (!emailMin.allowed) {
        return NextResponse.json(
          { error: 'Too many sign-in attempts. Please slow down and try again later.', code: 'RATE_LIMITED', retryAfter: emailMin.retryAfterSec },
          { status: 429, headers: { 'Retry-After': String(emailMin.retryAfterSec ?? 60) } }
        );
      }

      // Account lockout check (preserves enumeration safety: same generic AuthError message)
      const lockState = isLockedOut(LOCKOUT_SCOPE, normalizedEmail);
      if (lockState.locked) {
        throw new AuthError('Invalid email or password');
      }

      const rows = await db
        .select({
          id: users.id,
          email: users.email,
          name: users.name,
          role: users.role,
          passwordHash: users.passwordHash,
          restaurantId: users.restaurantId,
          branchId: users.branchId,
          preferredLanguage: users.preferredLanguage,
          restId: restaurants.id,
          restName: restaurants.name,
          restLogo: restaurants.logoUrl,
          brId: branches.id,
          brName: branches.name,
        })
        .from(users)
        .leftJoin(restaurants, eq(users.restaurantId, restaurants.id))
        .leftJoin(branches, eq(users.branchId, branches.id))
        .where(and(eq(users.email, normalizedEmail), eq(users.isActive, true)));

      const user = rows[0];
      if (!user) {
        recordFailure(LOCKOUT_SCOPE, normalizedEmail);
        throw new AuthError('Invalid email or password');
      }

      const valid = await bcrypt.compare(password, user.passwordHash);
      if (!valid) {
        recordFailure(LOCKOUT_SCOPE, normalizedEmail);
        throw new AuthError('Invalid email or password');
      }

      clearFailures(LOCKOUT_SCOPE, normalizedEmail);

      const token = await createSessionToken({
        userId: user.id,
        email: user.email,
        role: user.role!,
        restaurantId: user.restaurantId!,
        branchId: user.branchId!,
        // Hard branch pin — copy from users.branch_id for non-owner roles
        // so withAuth-protected routes can refuse cross-branch requests even
        // after a tampered switch-branch attempt. Owners stay un-pinned even
        // when their user row carries a default branch_id (used only as the
        // initial active branch on sign-in) and may switch freely.
        pinnedBranchId: user.role === 'owner' ? null : (user.branchId ?? null),
      });

      const response = NextResponse.json({
        user: {
          id: user.id,
          email: user.email,
          name: user.name,
          role: user.role,
          restaurant_id: user.restaurantId,
          branch_id: user.branchId,
          // At sign-in the JWT's session.branchId is set from user.branchId,
          // so session_branch_id mirrors it here. After the user calls
          // /api/auth/switch-branch, /api/auth/me will reflect the updated
          // session value (including null for an owner's All branches view).
          session_branch_id: user.branchId,
          preferred_language: user.preferredLanguage || 'en',
          restaurants: user.restId ? { id: user.restId, name: user.restName, logo_url: user.restLogo } : null,
          branches: user.brId ? { id: user.brId, name: user.brName } : null,
        },
      });

      response.cookies.set(COOKIE_NAME, token, {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'lax',
        maxAge: 7 * 24 * 60 * 60,
        path: '/',
      });

      // Fire-and-forget webhook so external systems (audit pipelines, IT
      // monitoring, etc.) can react to staff sign-ins. Skipped for the
      // platform superadmin which has no restaurant scope.
      if (user.restaurantId) {
        dispatchWebhook(user.restaurantId, 'staff.login', {
          user_id: user.id,
          email: user.email,
          name: user.name,
          role: user.role,
          branch_id: user.branchId,
          ip: getClientIp(req),
          at: new Date().toISOString(),
        });
      }

      return response;
    })
  )
);

export const POST = signIn;
