import { BadRequestException, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectRepository } from '@nestjs/typeorm';
import * as bcrypt from 'bcryptjs';
import type { Request } from 'express';
import { randomUUID } from 'node:crypto';
import { Repository } from 'typeorm';
import { AuditService } from '../audit/audit.service';
import { Role } from '../users/entities/role.entity';
import { User } from '../users/entities/user.entity';
import { UsersService } from '../users/users.service';
import type { ChangePasswordDto, LoginDto, RegisterDto } from './dto/login.dto';

type LoginResult = 'success' | 'failed';

@Injectable()
export class AuthService {
  constructor(
    private readonly auditService: AuditService,
    private readonly jwtService: JwtService,
    private readonly usersService: UsersService,
    @InjectRepository(Role)
    private readonly rolesRepository: Repository<Role>,
  ) {}

  async login(dto: LoginDto, request: Request) {
    const loginName = (dto.userName || dto.username || '').trim();
    const password = dto.password || '';

    if (!loginName || !password) {
      await this.writeLoginAudit(loginName, 'failed', request, 'empty credentials');
      throw new BadRequestException('用户名或密码不能为空');
    }

    const user = await this.usersService.findByLoginName(loginName);
    if (!user || !user.enabled) {
      await this.writeLoginAudit(loginName, 'failed', request, 'user not found or disabled');
      throw new BadRequestException('账号或密码错误');
    }

    const passwordMatched = await bcrypt.compare(password, user.passwordHash);
    if (!passwordMatched) {
      await this.writeLoginAudit(loginName, 'failed', request, 'bad password', user);
      throw new BadRequestException('账号或密码错误');
    }

    await this.usersService.markLoginSuccess(user.id);
    await this.writeLoginAudit(loginName, 'success', request, null, user);

    return {
      status: 'ok',
      token: await this.signUserToken(user),
      user: this.toUserInfo(user),
    };
  }

  async register(dto: RegisterDto) {
    const name = String(dto.name || '').trim();
    const email = String(dto.email || '').trim();
    const password = String(dto.password || '');

    if (!name || !email || !password) {
      throw new BadRequestException('用户名、邮箱和密码不能为空');
    }
    if (password.length < 6) {
      throw new BadRequestException('密码长度不能少于 6 位');
    }

    const existed = await this.usersService.findByEmail(email);
    if (existed) {
      throw new BadRequestException('邮箱已被注册');
    }

    const normalRole = await this.rolesRepository.findOne({ where: { code: 'user' } });
    const user = new User();
    user.id = randomUUID();
    user.name = name;
    user.email = email;
    user.passwordHash = await bcrypt.hash(password, 10);
    user.enabled = true;
    user.parentId = null;
    user.parentPath = null;
    user.tokenVersion = 0;
    user.mustChangePassword = false;
    user.roles = normalRole ? [normalRole] : [];

    await this.usersService.save(user);

    return {
      status: 'ok',
      token: await this.signUserToken(user),
      user: this.toUserInfo(user),
    };
  }

  async changePassword(user: User, dto: ChangePasswordDto) {
    const oldPassword = String(dto.oldPassword || '');
    const newPassword = String(dto.newPassword || '');
    if (!oldPassword || !newPassword) {
      throw new BadRequestException('原密码和新密码不能为空');
    }
    if (newPassword.length < 6) {
      throw new BadRequestException('新密码长度不能少于 6 位');
    }

    const matched = await bcrypt.compare(oldPassword, user.passwordHash);
    if (!matched) {
      throw new BadRequestException('原密码不正确');
    }

    user.passwordHash = await bcrypt.hash(newPassword, 10);
    user.mustChangePassword = false;
    user.tokenVersion = (user.tokenVersion || 0) + 1;
    await this.usersService.save(user);

    return { status: 'ok', token: await this.signUserToken(user) };
  }

  async refreshToken(user: User) {
    return {
      status: 'ok',
      token: await this.signUserToken(user),
      user: this.toUserInfo(user),
    };
  }

  async logout(user: User) {
    await this.usersService.incrementTokenVersion(user.id);
    return { status: 'ok' };
  }

  toUserInfo(user: User) {
    const primaryRole = user.roles?.[0];

    return {
      id: user.id,
      name: user.name,
      username: user.name,
      email: user.email,
      role: primaryRole?.code,
      roleName: primaryRole?.name,
      mustChangePassword: user.mustChangePassword,
      avatar:
        'https://lf1-xgcdn-tos.pstatp.com/obj/vcloud/vadmin/start.8e0e4855ee346a46ccff8ff3e24db27b.png',
      permissions: user.permissions ?? this.getPermissions(user.roles || []),
    };
  }

  private signUserToken(user: User) {
    return this.jwtService.signAsync({
      sub: user.id,
      username: user.name,
      tokenVersion: user.tokenVersion || 0,
    });
  }

  private getPermissions(roles: Role[]) {
    const permissions: Record<string, string[]> = {};

    for (const role of roles) {
      const rolePermissions = role.permissions;
      if (Array.isArray(rolePermissions)) {
        if (rolePermissions.includes('*')) {
          permissions['*'] = ['*'];
        }
        continue;
      }

      if (rolePermissions) {
        for (const [resource, actions] of Object.entries(rolePermissions)) {
          permissions[resource] = Array.from(
            new Set([...(permissions[resource] || []), ...actions]),
          );
        }
      }
    }

    return permissions;
  }

  private async writeLoginAudit(
    username: string,
    result: LoginResult,
    request: Request,
    failureReason: string | null,
    user?: User,
  ) {
    const loginName = username || '(empty)';
    await this.auditService.write({
      actorId: user?.id ?? null,
      actorUsername: user?.name ?? loginName,
      action: 'auth.login',
      resourceType: 'user',
      resourceId: user?.id ?? null,
      metadata: {
        loginResult: result,
        loginName,
        failureReason,
        userAgent: request.headers['user-agent'] ?? null,
      },
      ipAddress: this.getIp(request),
    });
  }

  private getIp(request: Request) {
    const forwardedFor = request.headers['x-forwarded-for'];
    if (typeof forwardedFor === 'string') {
      return forwardedFor.split(',')[0]?.trim() || null;
    }

    return request.ip || request.socket.remoteAddress || null;
  }
}
