import {
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { Prisma, UserStatus } from '@prisma/client';
import { compare } from 'bcryptjs';
import { createHash, randomUUID } from 'crypto';
import { AuditAction, AuditEntityType, type AppPermission } from '@aechr/shared';
import type { JwtAuthUser } from '../../common/interfaces/jwt-auth-user.interface';
import { AuditService } from '../audit/audit.service';
import { PrismaService } from '../prisma/prisma.service';
import { LoginDto } from './dto/login.dto';

type UserWithRolePermissions = Awaited<ReturnType<AuthService['findActiveUserById']>>;

interface JwtTokenPayload {
  sub: string;
  email: string;
  employeeCode: string;
  type: 'access' | 'refresh';
  jti?: string;
}

@Injectable()
export class AuthService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly jwtService: JwtService,
    private readonly configService: ConfigService,
    private readonly auditService: AuditService,
  ) {}

  async login(input: LoginDto) {
    const user = await this.prisma.user.findUnique({
      where: { email: input.email },
      include: {
        role: {
          include: {
            rolePermissions: {
              where: { deletedAt: null, permission: { deletedAt: null } },
              include: { permission: true },
            },
          },
        },
      },
    });

    if (!user || user.deletedAt || user.status !== UserStatus.ACTIVE) {
      throw new UnauthorizedException('Invalid email or password');
    }

    const passwordMatches = await compare(input.password, user.passwordHash);
    if (!passwordMatches) {
      throw new UnauthorizedException('Invalid email or password');
    }

    await this.prisma.user.update({
      where: { id: user.id },
      data: { lastLoginAt: new Date() },
    });

    const authUser = this.mapAuthUser(user);
    const tokens = await this.issueTokens(authUser);

    await this.auditService.log({
      module: 'auth',
      action: AuditAction.LOGIN,
      entityType: AuditEntityType.USER,
      entityId: user.id,
      newValues: {
        email: user.email,
        lastLoginAt: new Date().toISOString(),
      },
      remarks: 'User logged in',
      createdBy: user.id,
      updatedBy: user.id,
    });

    return {
      ...tokens,
      user: this.serializeUser(authUser),
    };
  }

  async refresh(refreshToken: string) {
    const payload = await this.verifyRefreshToken(refreshToken);

    if (!payload.jti) {
      throw new UnauthorizedException('Invalid refresh token');
    }

    const storedToken = await this.prisma.refreshToken.findUnique({
      where: { jti: payload.jti },
    });

    if (
      !storedToken ||
      storedToken.deletedAt ||
      storedToken.revokedAt ||
      storedToken.expiresAt <= new Date()
    ) {
      throw new UnauthorizedException('Refresh token has been revoked');
    }

    if (storedToken.tokenHash !== this.hashToken(refreshToken)) {
      await this.revokeAllUserRefreshTokens(storedToken.userId, 'Refresh token reuse detected');
      throw new UnauthorizedException('Refresh token reuse detected');
    }

    const user = await this.findActiveUserById(payload.sub);
    if (!user) {
      throw new UnauthorizedException('User is inactive');
    }

    const authUser = this.mapAuthUser(user);
    const tokens = await this.issueTokens(authUser, storedToken.id);

    return {
      ...tokens,
      user: this.serializeUser(authUser),
    };
  }

  async logout(refreshToken: string) {
    const payload = await this.verifyRefreshToken(refreshToken);

    if (!payload.jti) {
      throw new UnauthorizedException('Invalid refresh token');
    }

    const storedToken = await this.prisma.refreshToken.findUnique({
      where: { jti: payload.jti },
    });

    if (!storedToken || storedToken.deletedAt) {
      throw new UnauthorizedException('Invalid refresh token');
    }

    if (!storedToken.revokedAt) {
      await this.prisma.refreshToken.update({
        where: { id: storedToken.id },
        data: {
          revokedAt: new Date(),
          updatedBy: storedToken.userId,
        },
      });

      await this.auditService.log({
        module: 'auth',
        action: AuditAction.UPDATE,
        entityType: AuditEntityType.USER,
        entityId: storedToken.userId,
        remarks: 'User logged out',
        newValues: {
          revokedRefreshTokenJti: storedToken.jti,
        } as Prisma.InputJsonValue,
        createdBy: storedToken.userId,
        updatedBy: storedToken.userId,
      });
    }

    return { success: true };
  }

  async getProfile(userId: string) {
    const user = await this.findActiveUserById(userId);
    if (!user) {
      throw new UnauthorizedException('User is inactive');
    }

    return this.serializeUser(this.mapAuthUser(user));
  }

  async validateAccessTokenUser(userId: string): Promise<JwtAuthUser> {
    const user = await this.findActiveUserById(userId);
    if (!user) {
      throw new UnauthorizedException('User is inactive');
    }

    return this.mapAuthUser(user);
  }

  private async verifyRefreshToken(refreshToken: string) {
    const secret =
      this.configService.get<string>('JWT_REFRESH_SECRET') || 'replace-me';

    try {
      const payload = await this.jwtService.verifyAsync<JwtTokenPayload>(
        refreshToken,
        {
          secret,
        },
      );

      if (payload.type !== 'refresh') {
        throw new UnauthorizedException('Invalid refresh token');
      }

      return payload;
    } catch {
      throw new UnauthorizedException('Invalid refresh token');
    }
  }

  private async findActiveUserById(userId: string) {
    return this.prisma.user.findFirst({
      where: {
        id: userId,
        deletedAt: null,
        status: UserStatus.ACTIVE,
      },
      include: {
        role: {
          include: {
            rolePermissions: {
              where: { deletedAt: null, permission: { deletedAt: null } },
              include: { permission: true },
            },
          },
        },
      },
    });
  }

  private async issueTokens(user: JwtAuthUser, rotateFromTokenId?: string) {
    const payload: Omit<JwtTokenPayload, 'type'> = {
      sub: user.sub,
      email: user.email,
      employeeCode: user.employeeCode,
    };
    const refreshTokenJti = randomUUID();

    const [accessToken, refreshToken] = await Promise.all([
      this.jwtService.signAsync(
        { ...payload, type: 'access' },
        {
          secret:
            this.configService.get<string>('JWT_ACCESS_SECRET') || 'replace-me',
          expiresIn:
            (this.configService.get<string>('JWT_ACCESS_TTL') || '15m') as never,
        },
      ),
      this.jwtService.signAsync(
        { ...payload, jti: refreshTokenJti, type: 'refresh' },
        {
          secret:
            this.configService.get<string>('JWT_REFRESH_SECRET') || 'replace-me',
          expiresIn:
            (this.configService.get<string>('JWT_REFRESH_TTL') || '30d') as never,
        },
      ),
    ]);

    const refreshTokenExpiresAt = this.resolveRefreshTokenExpiry();

    await this.prisma.$transaction(async (tx) => {
      const newToken = await tx.refreshToken.create({
        data: {
          userId: user.sub,
          jti: refreshTokenJti,
          tokenHash: this.hashToken(refreshToken),
          expiresAt: refreshTokenExpiresAt,
          createdBy: user.sub,
          updatedBy: user.sub,
        },
      });

      if (rotateFromTokenId) {
        await tx.refreshToken.update({
          where: { id: rotateFromTokenId },
          data: {
            revokedAt: new Date(),
            replacedByTokenId: newToken.id,
            updatedBy: user.sub,
          },
        });
      }
    });

    return { accessToken, refreshToken };
  }

  private resolveRefreshTokenExpiry() {
    const configuredTtl =
      this.configService.get<string>('JWT_REFRESH_TTL') || '30d';
    const match = configuredTtl.match(/^(\d+)([smhd])$/);

    if (!match) {
      return new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
    }

    const amount = Number(match[1]);
    const unit = match[2];
    const multipliers: Record<string, number> = {
      s: 1000,
      m: 60 * 1000,
      h: 60 * 60 * 1000,
      d: 24 * 60 * 60 * 1000,
    };

    return new Date(Date.now() + amount * multipliers[unit]);
  }

  private hashToken(token: string) {
    return createHash('sha256').update(token).digest('hex');
  }

  private async revokeAllUserRefreshTokens(userId: string, reason: string) {
    await this.prisma.refreshToken.updateMany({
      where: {
        userId,
        revokedAt: null,
        deletedAt: null,
      },
      data: {
        revokedAt: new Date(),
        updatedBy: userId,
      },
    });

    await this.auditService.log({
      module: 'auth',
      action: AuditAction.UPDATE,
      entityType: AuditEntityType.USER,
      entityId: userId,
      remarks: reason,
      newValues: { revokedRefreshTokens: true } as Prisma.InputJsonValue,
      createdBy: userId,
      updatedBy: userId,
    });
  }

  private mapAuthUser(user: NonNullable<UserWithRolePermissions>): JwtAuthUser {
    const permissions = user.role.rolePermissions.map(
      (mapping) => mapping.permission.key as AppPermission,
    );

    return {
      sub: user.id,
      email: user.email,
      employeeCode: user.employeeCode,
      name: user.name,
      roleId: user.roleId,
      departmentId: user.departmentId,
      designation: user.designation,
      permissions,
    };
  }

  private serializeUser(user: JwtAuthUser) {
    return {
      id: user.sub,
      employeeCode: user.employeeCode,
      name: user.name,
      email: user.email,
      roleId: user.roleId,
      departmentId: user.departmentId,
      designation: user.designation,
      permissions: user.permissions,
    };
  }
}
