import {
  Controller,
  Post,
  Body,
  UseGuards,
  UseInterceptors,
  Get,
  Req,
  HttpCode,
  Res,
  UnauthorizedException,
} from "@nestjs/common";
import type { Request, Response } from "express";
import { RateLimitGuard, RateLimit } from "@bff/core/rate-limiting/index.js";
import { AuthOrchestrator } from "@bff/modules/auth/application/auth-orchestrator.service.js";
import { AuthHealthService } from "@bff/modules/auth/application/auth-health.service.js";
import { LocalAuthGuard } from "./guards/local-auth.guard.js";
import {
  FailedLoginThrottleGuard,
  type RequestWithRateLimit,
} from "./guards/failed-login-throttle.guard.js";
import { LoginResultInterceptor } from "./interceptors/login-result.interceptor.js";
import { Public, OptionalAuth } from "../../decorators/public.decorator.js";
import { createZodDto, ZodResponse } from "nestjs-zod";
import type { RequestWithUser, RequestWithCookies } from "@bff/modules/auth/auth.types.js";
import { JoseJwtService } from "../../infra/token/jose-jwt.service.js";
import { LoginOtpWorkflowService } from "../../infra/workflows/login-otp-workflow.service.js";
import { PasswordWorkflowService } from "../../infra/workflows/password-workflow.service.js";
import type { UserAuth } from "@customer-portal/domain/customer";
import { extractAccessTokenFromRequest } from "../../utils/token-from-request.util.js";
import { getRequestFingerprint } from "@bff/core/http/request-context.util.js";
import {
  setAuthCookies,
  clearAuthCookies,
  buildSessionInfo,
  ACCESS_COOKIE_PATH,
  REFRESH_COOKIE_PATH,
  TOKEN_TYPE,
} from "./utils/auth-cookie.util.js";
import {
  setTrustedDeviceCookie,
  clearTrustedDeviceCookie,
  getTrustedDeviceToken,
} from "./utils/trusted-device-cookie.util.js";
import { getDevAuthConfig } from "@bff/core/config/auth-dev.config.js";
import { TrustedDeviceService } from "../../infra/trusted-device/trusted-device.service.js";

// Import Zod schemas from domain
import {
  passwordResetRequestSchema,
  passwordResetSchema,
  setPasswordRequestSchema,
  changePasswordRequestSchema,
  accountStatusRequestSchema,
  ssoLinkRequestSchema,
  checkPasswordNeededRequestSchema,
  refreshTokenRequestSchema,
  checkPasswordNeededResponseSchema,
  loginVerifyOtpRequestSchema,
} from "@customer-portal/domain/auth";

// Re-export for backward compatibility with tests
export { ACCESS_COOKIE_PATH, REFRESH_COOKIE_PATH, TOKEN_TYPE };

class AccountStatusRequestDto extends createZodDto(accountStatusRequestSchema) {}
class RefreshTokenRequestDto extends createZodDto(refreshTokenRequestSchema) {}
class SetPasswordRequestDto extends createZodDto(setPasswordRequestSchema) {}
class CheckPasswordNeededRequestDto extends createZodDto(checkPasswordNeededRequestSchema) {}
class PasswordResetRequestDto extends createZodDto(passwordResetRequestSchema) {}
class ResetPasswordRequestDto extends createZodDto(passwordResetSchema) {}
class ChangePasswordRequestDto extends createZodDto(changePasswordRequestSchema) {}
class SsoLinkRequestDto extends createZodDto(ssoLinkRequestSchema) {}
class CheckPasswordNeededResponseDto extends createZodDto(checkPasswordNeededResponseSchema) {}
class LoginVerifyOtpRequestDto extends createZodDto(loginVerifyOtpRequestSchema) {}

@Controller("auth")
export class AuthController {
  constructor(
    private authOrchestrator: AuthOrchestrator,
    private readonly jwtService: JoseJwtService,
    private readonly loginOtpWorkflow: LoginOtpWorkflowService,
    private readonly trustedDeviceService: TrustedDeviceService,
    private readonly passwordWorkflow: PasswordWorkflowService,
    private readonly healthService: AuthHealthService
  ) {}

  private applyAuthRateLimitHeaders(req: RequestWithRateLimit, res: Response): void {
    FailedLoginThrottleGuard.applyRateLimitHeaders(req, res);
  }

  @Public()
  @Get("health-check")
  async healthCheck() {
    return this.healthService.check();
  }

  @Public()
  @Post("account-status")
  async accountStatus(@Body() body: AccountStatusRequestDto) {
    return this.authOrchestrator.getAccountStatus(body.email);
  }

  /**
   * POST /auth/login - Initiate login with credentials
   *
   * After valid credential check:
   * 1. Check if device is trusted (has valid trusted device cookie for this user)
   * 2. If trusted, skip OTP and complete login directly
   * 3. Otherwise, generate OTP and send to user's email
   * 4. Return session token for OTP verification
   * 5. User must call /auth/login/verify-otp to complete login
   */
  @Public()
  @UseGuards(LocalAuthGuard, FailedLoginThrottleGuard)
  @UseInterceptors(LoginResultInterceptor)
  @Post("login")
  async login(
    @Req() req: RequestWithUser & RequestWithRateLimit & RequestWithCookies,
    @Res({ passthrough: true }) res: Response
  ) {
    this.applyAuthRateLimitHeaders(req, res);

    // In dev mode with SKIP_OTP=true, skip OTP and complete login directly
    if (getDevAuthConfig().skipOtp) {
      const loginResult = await this.authOrchestrator.completeLogin(
        { id: req.user.id, email: req.user.email, role: req.user.role ?? "USER" },
        req
      );
      setAuthCookies(res, loginResult.tokens);
      return {
        user: loginResult.user,
        session: buildSessionInfo(loginResult.tokens),
      };
    }

    // Check if this is a trusted device for the authenticated user
    const trustedDeviceToken = getTrustedDeviceToken(req);
    const trustedDeviceResult = await this.trustedDeviceService.validateTrustedDevice(
      trustedDeviceToken,
      req.user.id
    );

    // If device is trusted for this user, skip OTP and complete login
    if (trustedDeviceResult.valid) {
      const loginResult = await this.authOrchestrator.completeLogin(
        { id: req.user.id, email: req.user.email, role: req.user.role ?? "USER" },
        req
      );
      setAuthCookies(res, loginResult.tokens);
      return {
        user: loginResult.user,
        session: buildSessionInfo(loginResult.tokens),
      };
    }

    // Credentials validated by LocalAuthGuard - now initiate OTP
    const fingerprint = getRequestFingerprint(req);
    const otpResult = await this.loginOtpWorkflow.initiateOtp(
      {
        id: req.user.id,
        email: req.user.email,
        role: req.user.role ?? "USER",
      },
      fingerprint
    );

    // Return OTP required response - no tokens issued yet
    return {
      requiresOtp: true,
      sessionToken: otpResult.sessionToken,
      maskedEmail: otpResult.maskedEmail,
      expiresAt: otpResult.expiresAt,
    };
  }

  /**
   * POST /auth/login/verify-otp - Complete login with OTP verification
   *
   * Verifies the OTP code and issues auth tokens on success.
   * If rememberDevice is true, sets a trusted device cookie to skip OTP on future logins.
   */
  @Public()
  @Post("login/verify-otp")
  @UseGuards(RateLimitGuard)
  @RateLimit({ limit: 10, ttl: 300 }) // 10 attempts per 5 minutes per IP
  @HttpCode(200)
  async verifyLoginOtp(
    @Body() body: LoginVerifyOtpRequestDto,
    @Req() req: Request,
    @Res({ passthrough: true }) res: Response
  ) {
    const fingerprint = getRequestFingerprint(req);
    const result = await this.loginOtpWorkflow.verifyOtp(body.sessionToken, body.code, fingerprint);

    if (!result.success) {
      throw new UnauthorizedException(result.error);
    }

    // OTP verified - complete login with token generation
    const loginResult = await this.authOrchestrator.completeLogin(
      { id: result.userId, email: result.email, role: result.role },
      req
    );

    setAuthCookies(res, loginResult.tokens);

    // If user wants to remember this device, create and set trusted device cookie
    if (body.rememberDevice) {
      const rawUserAgent = req.headers["user-agent"];
      const userAgent = typeof rawUserAgent === "string" ? rawUserAgent : undefined;
      const trustedDeviceToken = await this.trustedDeviceService.createTrustedDevice(
        result.userId,
        userAgent
      );
      setTrustedDeviceCookie(res, trustedDeviceToken, this.trustedDeviceService.getTtlMs());
    }

    return {
      user: loginResult.user,
      session: buildSessionInfo(loginResult.tokens),
    };
  }

  @Public()
  @Post("logout")
  async logout(
    @Req() req: RequestWithCookies & { user?: { id: string } },
    @Res({ passthrough: true }) res: Response
  ) {
    const token = extractAccessTokenFromRequest(req);
    let userId = req.user?.id;

    if (!userId && token) {
      const payload = await this.jwtService.verifyAllowExpired<{ sub?: string }>(token);
      if (payload?.sub) {
        userId = payload.sub;
      }
    }

    // Revoke trusted device for this user if they have one
    const trustedDeviceToken = getTrustedDeviceToken(req);
    if (trustedDeviceToken && userId) {
      // Validate to get device ID, then revoke
      const validation = await this.trustedDeviceService.validateTrustedDevice(
        trustedDeviceToken,
        userId
      );
      if (validation.deviceId) {
        await this.trustedDeviceService.revokeDevice(validation.deviceId);
      }
    }

    await this.authOrchestrator.logout(userId, token, req as Request);

    // Always clear cookies, even if session expired
    clearAuthCookies(res);
    clearTrustedDeviceCookie(res);
    return { message: "Logout successful" };
  }

  @Public()
  @Post("refresh")
  @UseGuards(RateLimitGuard)
  @RateLimit({ limit: 10, ttl: 300 }) // 10 attempts per 5 minutes per IP
  async refreshToken(
    @Body() body: RefreshTokenRequestDto,
    @Req() req: RequestWithCookies,
    @Res({ passthrough: true }) res: Response
  ) {
    const refreshToken = body.refreshToken ?? req.cookies?.["refresh_token"];
    const rawUserAgent = req.headers["user-agent"];
    const userAgent = typeof rawUserAgent === "string" ? rawUserAgent : undefined;
    const result = await this.authOrchestrator.refreshTokens(refreshToken, {
      deviceId: body.deviceId ?? undefined,
      userAgent: userAgent ?? undefined,
    });
    setAuthCookies(res, result.tokens);
    return { user: result.user, session: buildSessionInfo(result.tokens) };
  }

  @Public()
  @Post("set-password")
  @UseGuards(RateLimitGuard)
  @RateLimit({ limit: 5, ttl: 600 }) // 5 attempts per 10 minutes per IP+UA (industry standard)
  async setPassword(
    @Body() setPasswordData: SetPasswordRequestDto,
    @Req() _req: Request,
    @Res({ passthrough: true }) res: Response
  ) {
    const result = await this.passwordWorkflow.setPassword(
      setPasswordData.email,
      setPasswordData.password
    );
    setAuthCookies(res, result.tokens);
    return { user: result.user, session: buildSessionInfo(result.tokens) };
  }

  @Public()
  @Post("check-password-needed")
  @HttpCode(200)
  @ZodResponse({
    status: 200,
    description: "Check if password is needed",
    type: CheckPasswordNeededResponseDto,
  })
  async checkPasswordNeeded(@Body() data: CheckPasswordNeededRequestDto) {
    return this.passwordWorkflow.checkPasswordNeeded(data.email);
  }

  @Public()
  @Post("request-password-reset")
  @UseGuards(RateLimitGuard)
  @RateLimit({ limit: 5, ttl: 900 }) // 5 attempts per 15 minutes (standard for password operations)
  async requestPasswordReset(@Body() body: PasswordResetRequestDto, @Req() req: Request) {
    await this.passwordWorkflow.requestPasswordReset(body.email, req);
    return { message: "If an account exists, a reset email has been sent" };
  }

  @Public()
  @Post("reset-password")
  @HttpCode(200)
  @UseGuards(RateLimitGuard)
  @RateLimit({ limit: 5, ttl: 900 }) // 5 attempts per 15 minutes (standard for password operations)
  async resetPassword(
    @Body() body: ResetPasswordRequestDto,
    @Res({ passthrough: true }) res: Response
  ) {
    await this.passwordWorkflow.resetPassword(body.token, body.password);

    // Clear auth cookies after password reset to force re-login
    clearAuthCookies(res);
    return { message: "Password reset successful" };
  }

  @Post("change-password")
  @UseGuards(RateLimitGuard)
  @RateLimit({ limit: 5, ttl: 300 }) // 5 attempts per 5 minutes
  async changePassword(
    @Req() req: Request & { user: { id: string } },
    @Body() body: ChangePasswordRequestDto,
    @Res({ passthrough: true }) res: Response
  ) {
    const result = await this.passwordWorkflow.changePassword(req.user.id, body, req);
    setAuthCookies(res, result.tokens);
    return { user: result.user, session: buildSessionInfo(result.tokens) };
  }

  /**
   * GET /auth/me - Check authentication status
   *
   * Uses @OptionalAuth: returns isAuthenticated: false if not logged in,
   * 401 only if session cookie is present but expired/invalid
   */
  @OptionalAuth()
  @Get("me")
  getAuthStatus(@Req() req: Request & { user?: UserAuth }) {
    if (!req.user) {
      return { isAuthenticated: false };
    }
    return { isAuthenticated: true, user: req.user };
  }

  @Post("sso-link")
  async createSsoLink(
    @Req() req: Request & { user: { id: string } },
    @Body() body: SsoLinkRequestDto
  ) {
    const destination = body?.destination;
    return this.authOrchestrator.createSsoLink(req.user.id, destination);
  }
}
