import {
  Controller,
  Post,
  Body,
  HttpCode,
  HttpStatus,
  UseGuards,
  Get,
  Inject,
  NotFoundException,
  UnauthorizedException,
  InternalServerErrorException,
  Logger,
  Req,
  Res,
} from '@nestjs/common';
import type { Request, Response } from 'express';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import { AuthService } from '../auth/auth.service';
import {
  LoginDto,
  RegisterDto,
  AuthResponseDto,
  RefreshTokenDto,
  ForgotPasswordDto,
  ResetPasswordDto,
  ContactFormDto,
} from '../dto/auth-login.dto';
import { EmailPort, EMAIL_PORT } from '@domain/ports/out/email.port';
import { Public } from '../decorators/public.decorator';
import { CurrentUser, UserPayload } from '../decorators/current-user.decorator';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { UserRepository, USER_REPOSITORY } from '@domain/ports/out/user.repository';
import { UserMapper } from '../mappers/user.mapper';
import { InvitationService } from '../services/invitation.service';
import { AuditService } from '../services/audit.service';
import { AuditAction } from '@domain/entities/audit-log.entity';
import {
  AUTH_COOKIE_NAMES,
  authCookieOptions,
} from '../../infrastructure/security/security.config';

const REFRESH_COOKIE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days

/**
 * Escape user-provided text before interpolating it into HTML emails
 */
function escapeHtml(value: string): string {
  return value
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

/**
 * Authentication Controller
 *
 * Handles user authentication endpoints:
 * - POST /auth/register - User registration
 * - POST /auth/login - User login
 * - POST /auth/refresh - Token refresh
 * - POST /auth/logout - User logout (placeholder)
 * - GET /auth/me - Get current user profile
 */
@ApiTags('Authentication')
@Controller('auth')
export class AuthController {
  private readonly logger = new Logger(AuthController.name);

  constructor(
    private readonly authService: AuthService,
    @Inject(USER_REPOSITORY) private readonly userRepository: UserRepository,
    private readonly invitationService: InvitationService,
    private readonly auditService: AuditService,
    @Inject(EMAIL_PORT) private readonly emailService: EmailPort
  ) {}

  /**
   * Extract the client IP and user agent from the request so we can record
   * *who* connected and *from where* in the audit trail.
   */
  private getClientInfo(req: Request): { ipAddress?: string; userAgent?: string } {
    const forwardedFor = req.headers['x-forwarded-for'];
    const ipAddress =
      (Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor?.split(',')[0]?.trim()) ||
      req.ip ||
      req.socket?.remoteAddress;
    return {
      ipAddress,
      userAgent: req.headers['user-agent'],
    };
  }

  /**
   * Deliver tokens as httpOnly cookies so they are out of reach of XSS.
   * When rememberMe is false the cookies are session-scoped (cleared when
   * the browser closes); otherwise they persist for the refresh window.
   */
  private setAuthCookies(
    res: Response,
    tokens: { accessToken: string; refreshToken: string },
    rememberMe: boolean
  ): void {
    const maxAgeMs = rememberMe ? REFRESH_COOKIE_MAX_AGE_MS : undefined;
    res.cookie(AUTH_COOKIE_NAMES.accessToken, tokens.accessToken, authCookieOptions({ maxAgeMs }));
    res.cookie(
      AUTH_COOKIE_NAMES.refreshToken,
      tokens.refreshToken,
      authCookieOptions({ maxAgeMs })
    );
    // Readable flag (no token inside) so the frontend knows a session exists
    res.cookie(AUTH_COOKIE_NAMES.session, '1', authCookieOptions({ maxAgeMs, httpOnly: false }));
  }

  private clearAuthCookies(res: Response): void {
    res.clearCookie(AUTH_COOKIE_NAMES.accessToken, authCookieOptions());
    res.clearCookie(AUTH_COOKIE_NAMES.refreshToken, authCookieOptions());
    res.clearCookie(AUTH_COOKIE_NAMES.session, authCookieOptions({ httpOnly: false }));
  }

  /**
   * Register a new user
   *
   * Creates a new user account and returns access + refresh tokens.
   *
   * @param dto - Registration data (email, password, firstName, lastName, organizationId)
   * @returns Access token, refresh token, and user info
   */
  @Public()
  @Throttle({ default: { limit: 5, ttl: 60000 } })
  @Post('register')
  @HttpCode(HttpStatus.CREATED)
  @ApiOperation({
    summary: 'Register new user',
    description: 'Create a new user account with email and password. Returns JWT tokens.',
  })
  @ApiResponse({
    status: 201,
    description: 'User successfully registered',
    type: AuthResponseDto,
  })
  @ApiResponse({
    status: 409,
    description: 'User with this email already exists',
  })
  @ApiResponse({
    status: 400,
    description: 'Validation error (invalid email, weak password, etc.)',
  })
  async register(
    @Body() dto: RegisterDto,
    @Res({ passthrough: true }) res: Response
  ): Promise<AuthResponseDto> {
    // If invitation token is provided, verify and use it
    let invitationOrganizationId: string | undefined;
    let invitationRole: string | undefined;

    if (dto.invitationToken) {
      const invitation = await this.invitationService.verifyInvitation(dto.invitationToken);

      // Verify email matches invitation
      if (invitation.email.toLowerCase() !== dto.email.toLowerCase()) {
        throw new NotFoundException('Invitation email does not match registration email');
      }

      invitationOrganizationId = invitation.organizationId;
      invitationRole = invitation.role;

      // Override firstName/lastName from invitation if not provided
      dto.firstName = dto.firstName || invitation.firstName;
      dto.lastName = dto.lastName || invitation.lastName;
    }

    // Joining an existing organization is only allowed through a verified
    // invitation token — never from a caller-supplied organization ID.
    const result = await this.authService.register(
      dto.email,
      dto.password,
      dto.firstName,
      dto.lastName,
      invitationOrganizationId,
      dto.organization,
      invitationRole
    );

    // Mark invitation as used if provided
    if (dto.invitationToken) {
      await this.invitationService.markInvitationAsUsed(dto.invitationToken);
    }

    this.setAuthCookies(res, result, false);

    return {
      accessToken: result.accessToken,
      refreshToken: result.refreshToken,
      user: result.user,
    };
  }

  /**
   * Login with email and password
   *
   * Authenticates a user and returns access + refresh tokens.
   *
   * @param dto - Login credentials (email, password)
   * @returns Access token, refresh token, and user info
   */
  @Public()
  @Throttle({ default: { limit: 5, ttl: 60000 } })
  @Post('login')
  @HttpCode(HttpStatus.OK)
  @ApiOperation({
    summary: 'User login',
    description: 'Authenticate with email and password. Returns JWT tokens.',
  })
  @ApiResponse({
    status: 200,
    description: 'Login successful',
    type: AuthResponseDto,
  })
  @ApiResponse({
    status: 401,
    description: 'Invalid credentials or inactive account',
  })
  async login(
    @Body() dto: LoginDto,
    @Req() req: Request,
    @Res({ passthrough: true }) res: Response
  ): Promise<AuthResponseDto> {
    const rememberMe = dto.rememberMe === true;
    const { ipAddress, userAgent } = this.getClientInfo(req);

    try {
      const result = await this.authService.login(dto.email, dto.password, rememberMe);

      this.setAuthCookies(res, result, rememberMe);

      // Audit log: record who logged in, when and from where
      await this.auditService.logSuccess(
        AuditAction.USER_LOGIN,
        result.user.id,
        result.user.email,
        result.user.organizationId,
        {
          resourceType: 'user',
          resourceId: result.user.id,
          ipAddress,
          userAgent,
          metadata: { rememberMe },
        }
      );

      this.logger.log(`Login success: ${result.user.email} from ${ipAddress ?? 'unknown IP'}`);

      return {
        accessToken: result.accessToken,
        refreshToken: result.refreshToken,
        user: result.user,
      };
    } catch (error: any) {
      // Audit log: record failed login attempts (the attempted email is the
      // only identity we have — the credentials did not match a valid user)
      await this.auditService.logFailure(
        AuditAction.USER_LOGIN,
        'unknown',
        dto.email,
        'unknown',
        error?.message || 'Invalid credentials',
        {
          resourceType: 'user',
          ipAddress,
          userAgent,
        }
      );

      this.logger.warn(`Login failed for ${dto.email} from ${ipAddress ?? 'unknown IP'}`);

      throw error;
    }
  }

  /**
   * Refresh access token
   *
   * Obtains a new access token using a valid refresh token.
   *
   * @param dto - Refresh token
   * @returns New access token
   */
  @Public()
  @Throttle({ default: { limit: 20, ttl: 60000 } })
  @Post('refresh')
  @HttpCode(HttpStatus.OK)
  @ApiOperation({
    summary: 'Refresh access token',
    description:
      'Get a new access token using a valid refresh token. Refresh tokens are long-lived (7 days).',
  })
  @ApiResponse({
    status: 200,
    description: 'Token refreshed successfully — new tokens are set as httpOnly cookies',
    schema: {
      properties: {
        success: { type: 'boolean', example: true },
      },
    },
  })
  @ApiResponse({
    status: 401,
    description: 'Invalid or expired refresh token',
  })
  async refresh(
    @Body() dto: RefreshTokenDto,
    @Req() req: Request,
    @Res({ passthrough: true }) res: Response
  ): Promise<{ success: boolean }> {
    // Prefer the httpOnly cookie; fall back to the body for legacy clients
    const refreshToken = req.cookies?.[AUTH_COOKIE_NAMES.refreshToken] || dto.refreshToken;

    if (!refreshToken) {
      this.clearAuthCookies(res);
      throw new UnauthorizedException('No refresh token provided');
    }

    const result = await this.authService.refreshAccessToken(refreshToken);

    this.setAuthCookies(res, result, result.rememberMe);

    // Tokens are intentionally NOT returned in the body: an XSS payload could
    // otherwise call this endpoint and exfiltrate a fresh access token.
    return { success: true };
  }

  /**
   * Logout
   *
   * Revokes the refresh token (Redis blacklist) and clears the auth cookies.
   * The access token naturally expires within 15 minutes.
   */
  @Public()
  @Post('logout')
  @HttpCode(HttpStatus.OK)
  @ApiOperation({
    summary: 'Logout',
    description: 'Revoke the refresh token and clear authentication cookies.',
  })
  @ApiResponse({
    status: 200,
    description: 'Logout successful',
    schema: {
      properties: {
        message: { type: 'string', example: 'Logout successful' },
      },
    },
  })
  async logout(
    @Req() req: Request,
    @Res({ passthrough: true }) res: Response
  ): Promise<{ message: string }> {
    const refreshToken = req.cookies?.[AUTH_COOKIE_NAMES.refreshToken];

    const loggedOutUser = await this.authService.logout(refreshToken);
    this.clearAuthCookies(res);

    // Audit log: record who logged out and when
    if (loggedOutUser) {
      const { ipAddress, userAgent } = this.getClientInfo(req);
      await this.auditService.logSuccess(
        AuditAction.USER_LOGOUT,
        loggedOutUser.userId,
        loggedOutUser.email,
        loggedOutUser.organizationId,
        { resourceType: 'user', resourceId: loggedOutUser.userId, ipAddress, userAgent }
      );
      this.logger.log(`Logout: ${loggedOutUser.email} from ${ipAddress ?? 'unknown IP'}`);
    }

    return { message: 'Logout successful' };
  }

  /**
   * Contact form — forwards message to contact@xpeditis.com
   */
  @Public()
  @Throttle({ default: { limit: 3, ttl: 60000 } })
  @Post('contact')
  @HttpCode(HttpStatus.OK)
  @ApiOperation({
    summary: 'Contact form',
    description: 'Send a contact message to the Xpeditis team.',
  })
  @ApiResponse({ status: 200, description: 'Message sent successfully' })
  async contact(@Body() dto: ContactFormDto): Promise<{ message: string }> {
    const subjectLabels: Record<string, string> = {
      demo: 'Demande de démonstration',
      pricing: 'Questions sur les tarifs',
      partnership: 'Partenariat',
      support: 'Support technique',
      press: 'Relations presse',
      careers: 'Recrutement',
      other: 'Autre',
    };

    const subjectLabel = escapeHtml(subjectLabels[dto.subject] || dto.subject);
    const firstName = escapeHtml(dto.firstName);
    const lastName = escapeHtml(dto.lastName);
    const email = escapeHtml(dto.email);
    const company = dto.company ? escapeHtml(dto.company) : undefined;
    const phone = dto.phone ? escapeHtml(dto.phone) : undefined;
    const message = escapeHtml(dto.message);

    const html = `
      <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
        <div style="background: #10183A; padding: 24px; border-radius: 8px 8px 0 0;">
          <h1 style="color: #34CCCD; margin: 0; font-size: 20px;">Nouveau message de contact</h1>
        </div>
        <div style="background: #f9f9f9; padding: 24px; border: 1px solid #e0e0e0;">
          <table style="width: 100%; border-collapse: collapse;">
            <tr>
              <td style="padding: 8px 0; color: #666; width: 130px; font-size: 14px;">Nom</td>
              <td style="padding: 8px 0; color: #222; font-weight: bold; font-size: 14px;">${firstName} ${lastName}</td>
            </tr>
            <tr>
              <td style="padding: 8px 0; color: #666; font-size: 14px;">Email</td>
              <td style="padding: 8px 0; font-size: 14px;"><a href="mailto:${email}" style="color: #34CCCD;">${email}</a></td>
            </tr>
            ${company ? `<tr><td style="padding: 8px 0; color: #666; font-size: 14px;">Entreprise</td><td style="padding: 8px 0; color: #222; font-size: 14px;">${company}</td></tr>` : ''}
            ${phone ? `<tr><td style="padding: 8px 0; color: #666; font-size: 14px;">Téléphone</td><td style="padding: 8px 0; color: #222; font-size: 14px;">${phone}</td></tr>` : ''}
            <tr>
              <td style="padding: 8px 0; color: #666; font-size: 14px;">Sujet</td>
              <td style="padding: 8px 0; color: #222; font-size: 14px;">${subjectLabel}</td>
            </tr>
          </table>
          <div style="margin-top: 16px; padding-top: 16px; border-top: 1px solid #ddd;">
            <p style="color: #666; font-size: 14px; margin: 0 0 8px 0;">Message :</p>
            <p style="color: #222; font-size: 14px; white-space: pre-wrap; margin: 0;">${message}</p>
          </div>
        </div>
        <div style="background: #f0f0f0; padding: 12px 24px; border-radius: 0 0 8px 8px; text-align: center;">
          <p style="color: #999; font-size: 12px; margin: 0;">Xpeditis — Formulaire de contact</p>
        </div>
      </div>
    `;

    try {
      await this.emailService.send({
        to: 'contact@xpeditis.com',
        replyTo: dto.email,
        subject: `[Contact] ${subjectLabels[dto.subject] || dto.subject} — ${dto.firstName} ${dto.lastName}`,
        html,
      });
    } catch (error) {
      this.logger.error(`Failed to send contact email: ${error}`);
      throw new InternalServerErrorException(
        "Erreur lors de l'envoi du message. Veuillez réessayer."
      );
    }

    return { message: 'Message envoyé avec succès.' };
  }

  /**
   * Forgot password — sends reset email
   */
  @Public()
  @Throttle({ default: { limit: 3, ttl: 60000 } })
  @Post('forgot-password')
  @HttpCode(HttpStatus.OK)
  @ApiOperation({
    summary: 'Forgot password',
    description: 'Send a password reset email. Always returns 200 to avoid user enumeration.',
  })
  @ApiResponse({ status: 200, description: 'Reset email sent (if account exists)' })
  async forgotPassword(@Body() dto: ForgotPasswordDto): Promise<{ message: string }> {
    await this.authService.forgotPassword(dto.email);
    return {
      message: 'Si un compte existe avec cet email, vous recevrez un lien de réinitialisation.',
    };
  }

  /**
   * Reset password using token from email
   */
  @Public()
  @Throttle({ default: { limit: 5, ttl: 60000 } })
  @Post('reset-password')
  @HttpCode(HttpStatus.OK)
  @ApiOperation({
    summary: 'Reset password',
    description: 'Reset user password using the token received by email.',
  })
  @ApiResponse({ status: 200, description: 'Password reset successfully' })
  @ApiResponse({ status: 400, description: 'Invalid or expired token' })
  async resetPassword(@Body() dto: ResetPasswordDto): Promise<{ message: string }> {
    await this.authService.resetPassword(dto.token, dto.newPassword);
    return { message: 'Mot de passe réinitialisé avec succès.' };
  }

  /**
   * Get current user profile
   *
   * Returns the profile of the currently authenticated user with complete details.
   *
   * @param user - Current user from JWT token
   * @returns User profile with firstName, lastName, etc.
   */
  @UseGuards(JwtAuthGuard)
  @Get('me')
  @ApiBearerAuth()
  @ApiOperation({
    summary: 'Get current user profile',
    description: 'Returns the complete profile of the authenticated user.',
  })
  @ApiResponse({
    status: 200,
    description: 'User profile retrieved successfully',
    schema: {
      properties: {
        id: { type: 'string', format: 'uuid' },
        email: { type: 'string', format: 'email' },
        firstName: { type: 'string' },
        lastName: { type: 'string' },
        role: { type: 'string', enum: ['ADMIN', 'MANAGER', 'USER', 'VIEWER'] },
        organizationId: { type: 'string', format: 'uuid' },
        isActive: { type: 'boolean' },
        createdAt: { type: 'string', format: 'date-time' },
        updatedAt: { type: 'string', format: 'date-time' },
      },
    },
  })
  @ApiResponse({
    status: 401,
    description: 'Unauthorized - invalid or missing token',
  })
  async getProfile(@CurrentUser() user: UserPayload) {
    // Fetch complete user details from database
    const fullUser = await this.userRepository.findById(user.id);

    if (!fullUser) {
      throw new NotFoundException('User not found');
    }

    // Return complete user data with firstName and lastName
    return UserMapper.toDto(fullUser);
  }
}
