import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import type { StringValue } from 'ms';
import * as bcrypt from 'bcrypt';
import { PrismaService } from '../prisma/prisma.service';
import { LoginDto } from './dto/login.dto';
import { RefreshTokenDto } from './dto/refresh-token.dto';

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

  private async issueTokens(user: {
    id: string;
    username: string;
    role: string;
    displayName: string | null;
    profileImageUrl: string | null;
    workspaceId: string | null;
    createdAt: Date;
  }) {
    const membership = await this.prismaService.projectMembership.findUnique({
      where: { userId: user.id },
      select: { projectId: true },
    });

    const currentProjectId = membership?.projectId ?? null;

    const accessToken = await this.jwtService.signAsync({
      sub: user.id,
      username: user.username,
      role: user.role,
      workspaceId: user.workspaceId,
      projectId: currentProjectId,
    });

    const refreshToken = await this.jwtService.signAsync(
      { sub: user.id, type: 'refresh' },
      {
        secret:
          this.configService.get<string>('REFRESH_TOKEN_SECRET') ??
          'change_me_refresh_secret',
        expiresIn:
          (this.configService.get<string>(
            'REFRESH_TOKEN_EXPIRES_IN',
          ) as StringValue) ?? '30d',
      },
    );

    const refreshTokenHash = await bcrypt.hash(refreshToken, 12);

    await this.prismaService.user.update({
      where: { id: user.id },
      data: { refreshTokenHash },
    });

    return {
      accessToken,
      refreshToken,
      user: {
        id: user.id,
        username: user.username,
        displayName: user.displayName,
        profileImageUrl: user.profileImageUrl,
        workspaceId: user.workspaceId,
        currentProjectId,
        role: user.role,
        createdAt: user.createdAt,
      },
    };
  }

  async login(loginDto: LoginDto) {
    const normalizedUsername = loginDto.username.trim();

    const user = await this.prismaService.user.findUnique({
      where: { username: normalizedUsername },
    });

    const caseInsensitiveUser = user ?? (await this.prismaService.user.findFirst({
      where: {
        username: {
          equals: normalizedUsername,
          mode: 'insensitive',
        },
      },
    }));

    if (!caseInsensitiveUser) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const passwordMatches = await bcrypt.compare(
      loginDto.password,
      caseInsensitiveUser.passwordHash,
    );

    if (!passwordMatches) {
      throw new UnauthorizedException('Invalid credentials');
    }

    return this.issueTokens(caseInsensitiveUser);
  }

  async refresh(refreshTokenDto: RefreshTokenDto) {
    const refreshSecret =
      this.configService.get<string>('REFRESH_TOKEN_SECRET') ??
      'change_me_refresh_secret';

    try {
      const payload = await this.jwtService.verifyAsync(refreshTokenDto.refreshToken, {
        secret: refreshSecret,
      });

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

      const user = await this.prismaService.user.findUnique({
        where: { id: payload.sub as string },
      });

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

      const isRefreshTokenValid = await bcrypt.compare(
        refreshTokenDto.refreshToken,
        user.refreshTokenHash,
      );

      if (!isRefreshTokenValid) {
        throw new UnauthorizedException('Invalid refresh token');
      }

      return this.issueTokens(user);
    } catch {
      throw new UnauthorizedException('Invalid or expired refresh token');
    }
  }

  async logout(userId: string) {
    await this.prismaService.user.updateMany({
      where: { id: userId },
      data: { refreshTokenHash: null },
    });

    return { loggedOut: true };
  }

  async me(userId: string) {
    const user = await this.prismaService.user.findUnique({ where: { id: userId } });

    if (!user) {
      throw new UnauthorizedException('User not found');
    }

    return {
      id: user.id,
      username: user.username,
      displayName: user.displayName,
      profileImageUrl: user.profileImageUrl,
      workspaceId: user.workspaceId,
      currentProjectId: (
        await this.prismaService.projectMembership.findUnique({
          where: { userId: user.id },
          select: { projectId: true },
        })
      )?.projectId ?? null,
      role: user.role,
      createdAt: user.createdAt,
    };
  }
}