import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcryptjs';

import { ActivityLogService } from '../activity-logs/activity-log.service';
import { ServiceLogger } from '../../common/logging/service-logger';
import { UsersService } from '../users/users.service';
import { LoginDto } from './dto/login.dto';
import type { AuthResponse, JwtPayload, SafeUser } from './auth.types';
import type {
  ActivityAction,
  ActivityEntityType,
  Prisma,
} from '../../generated/prisma/client';

type AuthResult = AuthResponse & {
  accessToken: string;
  refreshToken: string;
};

@Injectable()
export class AuthService {
  private readonly logger = new ServiceLogger(AuthService.name);
  constructor(
    private readonly usersService: UsersService,
    private readonly jwtService: JwtService,
    private readonly configService: ConfigService,
    private readonly activityLogService: ActivityLogService,
  ) {}

  async login(loginDto: LoginDto): Promise<AuthResult> {
    const user = await this.usersService.findByEmail(loginDto.email.trim());

    if (!user || !user.password || user.status !== 'active') {
      this.logger.warn('auth.login.rejected', {
        reason: 'invalid_credentials_or_inactive',
        emailDomain: this.emailDomain(loginDto.email),
      });
      throw new UnauthorizedException('Invalid credentials.');
    }

    const passwordMatches = await bcrypt.compare(
      loginDto.password.trim(),
      user.password,
    );
    if (!passwordMatches) {
      this.logger.warn('auth.login.rejected', {
        reason: 'invalid_credentials', emailDomain: this.emailDomain(loginDto.email),
      });
      throw new UnauthorizedException('Invalid credentials.');
    }

    const payload = this.buildPayload(user.id, user.email, user.role);
    const accessToken = await this.signAccessToken(payload);
    const refreshToken = await this.signRefreshToken(payload);

    const refreshTokenHash = await bcrypt.hash(refreshToken, 10);
    await this.usersService.updateRefreshTokenHash(user.id, refreshTokenHash);
    await this.recordActivity({
      action: 'auth_login',
      entityType: 'auth',
      entityId: user.id,
      actorUserId: user.id,
      title: 'User signed in',
      description: `${user.name} signed in successfully.`,
      metadata: {
        email: user.email,
        role: user.role,
      },
    });
    this.logger.log('auth.login.succeeded', { userId: user.id, role: user.role });

    return {
      user: this.toSafeUser(user),
      accessToken,
      refreshToken,
      redirectTo: this.getRedirectPath(user.role),
    };
  }

  validatePasswordSetup(token: string) {
    return this.usersService.validatePasswordSetupToken(token);
  }

  setupPassword(token: string, password: string) {
    return this.usersService.completePasswordSetup(token, password);
  }

  async refresh(refreshToken?: string): Promise<AuthResult> {
    if (!refreshToken) {
      throw new UnauthorizedException('Refresh token is missing.');
    }

    const payload = await this.verifyRefreshToken(refreshToken);
    const user = await this.usersService.findById(payload.sub);

    if (!user || user.status !== 'active' || !user.refreshTokenHash) {
      this.logger.warn('auth.refresh.rejected', { reason: 'invalid_session' });
      throw new UnauthorizedException('Refresh token is not valid.');
    }

    const refreshTokenMatches = await bcrypt.compare(
      refreshToken,
      user.refreshTokenHash,
    );
    if (!refreshTokenMatches) {
      this.logger.warn('auth.refresh.rejected', { reason: 'token_mismatch', userId: user.id });
      throw new UnauthorizedException('Refresh token is not valid.');
    }

    const nextPayload = this.buildPayload(user.id, user.email, user.role);
    const accessToken = await this.signAccessToken(nextPayload);
    const nextRefreshToken = await this.signRefreshToken(nextPayload);
    const nextRefreshTokenHash = await bcrypt.hash(nextRefreshToken, 10);

    await this.usersService.updateRefreshTokenHash(
      user.id,
      nextRefreshTokenHash,
    );
    return {
      user: this.toSafeUser(user),
      accessToken,
      refreshToken: nextRefreshToken,
      redirectTo: this.getRedirectPath(user.role),
    };
  }

  async logout(refreshToken?: string): Promise<void> {
    if (!refreshToken) {
      return;
    }

    try {
      const payload = await this.verifyRefreshToken(refreshToken);
      const user = await this.usersService.findById(payload.sub);

      if (user?.refreshTokenHash) {
        const refreshTokenMatches = await bcrypt.compare(
          refreshToken,
          user.refreshTokenHash,
        );
        if (refreshTokenMatches) {
          await this.usersService.updateRefreshTokenHash(user.id, null);
          await this.recordActivity({
            action: 'auth_logout',
            entityType: 'auth',
            entityId: user.id,
            actorUserId: user.id,
            title: 'User signed out',
            description: `${user.name} signed out successfully.`,
            metadata: {
              email: user.email,
              role: user.role,
            },
          });
          this.logger.log('auth.logout.succeeded', { userId: user.id });
        }
      }
    } catch (error) {
      this.logger.warn('auth.logout.skipped', {
        reason: error instanceof Error ? error.message : 'invalid_session',
      });
      return;
    }
  }

  async getCurrentUser(userId: string): Promise<SafeUser> {
    const user = await this.usersService.findById(userId);

    if (!user || user.status !== 'active') {
      throw new UnauthorizedException(
        'User is not active or no longer exists.',
      );
    }

    return this.toSafeUser(user);
  }

  getRedirectPath(role: JwtPayload['role']) {
    return role === 'super_admin' ? '/app/admin' : '/app/seo';
  }

  private buildPayload(
    userId: string,
    email: string,
    role: JwtPayload['role'],
  ): JwtPayload {
    return {
      sub: userId,
      email,
      role,
    };
  }

  private async signAccessToken(payload: JwtPayload) {
    return this.jwtService.signAsync(payload, {
      secret: this.configService.getOrThrow<string>('JWT_ACCESS_SECRET'),
      expiresIn: this.configService.getOrThrow<string>(
        'JWT_ACCESS_EXPIRES_IN',
      ) as any,
    });
  }

  private async signRefreshToken(payload: JwtPayload) {
    return this.jwtService.signAsync(payload, {
      secret: this.configService.getOrThrow<string>('JWT_REFRESH_SECRET'),
      expiresIn: this.configService.getOrThrow<string>(
        'JWT_REFRESH_EXPIRES_IN',
      ) as any,
    });
  }

  private async verifyRefreshToken(refreshToken: string) {
    return this.jwtService.verifyAsync<JwtPayload>(refreshToken, {
      secret: this.configService.getOrThrow<string>('JWT_REFRESH_SECRET'),
    });
  }

  private async recordActivity(input: {
    action: ActivityAction;
    entityType: ActivityEntityType;
    entityId?: string | null;
    title: string;
    description: string;
    metadata?: Prisma.InputJsonValue | null;
    actorUserId?: string | null;
    clientId?: string | null;
    reportId?: string | null;
  }) {
    try {
      await this.activityLogService.record({
        action: input.action,
        entityType: input.entityType,
        entityId: input.entityId,
        title: input.title,
        description: input.description,
        metadata: input.metadata,
        actorUserId: input.actorUserId,
        clientId: input.clientId,
        reportId: input.reportId,
      });
    } catch (error) {
      // Activity logging should never block auth flow.
      this.logger.error('auth.activity_log.failed', error, {
        action: input.action, entityId: input.entityId, actorUserId: input.actorUserId,
      });
    }
  }

  private toSafeUser(user: {
    id: string;
    name: string;
    email: string;
    role: JwtPayload['role'];
    status: 'active' | 'inactive';
  }): SafeUser {
    return {
      id: user.id,
      name: user.name,
      email: user.email,
      role: user.role,
      status: user.status,
    };
  }

  private emailDomain(email: string) {
    return email.trim().toLowerCase().split('@')[1] ?? 'unknown';
  }
}
