import { Repository, IsNull } from 'typeorm';
import { randomBytes, createHmac } from 'crypto';
import { MfaAuditLog, MfaEventType, User } from '../entities';

export interface ServiceContext {
  tenantId: string;
  userId?: string;
}

export interface MfaSetupResult {
  secret: string;
  qrCodeUrl: string;
  backupCodes: string[];
}

export interface MfaVerifyResult {
  success: boolean;
  error?: string;
  remainingAttempts?: number;
}

export interface MfaFilters {
  userId?: string;
  eventType?: MfaEventType;
  success?: boolean;
  startDate?: Date;
  endDate?: Date;
}

export interface PaginatedResult<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

export class MfaService {
  private readonly issuer = 'ERP-Construccion';
  private readonly backupCodeCount = 10;

  constructor(
    private readonly auditLogRepository: Repository<MfaAuditLog>,
    private readonly userRepository: Repository<User>,
  ) {}

  async setupMfa(ctx: ServiceContext, userId: string): Promise<MfaSetupResult> {
    const user = await this.userRepository.findOne({
      where: { id: userId, tenantId: ctx.tenantId, deletedAt: IsNull() },
    });

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

    const secret = this.generateSecret();
    const backupCodes = this.generateBackupCodes();
    const qrCodeUrl = this.generateQrCodeUrl(user.email, secret);

    // Store secret and backup codes (hashed) on user
    user.mfaSecret = secret;
    user.mfaBackupCodes = backupCodes.map(code => this.hashCode(code));
    user.mfaEnabled = false; // Not enabled until verified
    await this.userRepository.save(user);

    await this.logEvent(ctx, userId, MfaEventType.SETUP_INITIATED, true);

    return {
      secret,
      qrCodeUrl,
      backupCodes,
    };
  }

  async enableMfa(ctx: ServiceContext, userId: string, token: string): Promise<boolean> {
    const user = await this.userRepository.findOne({
      where: { id: userId, tenantId: ctx.tenantId, deletedAt: IsNull() },
    });

    if (!user || !user.mfaSecret) {
      throw new Error('MFA not set up for this user');
    }

    const isValid = this.verifyTotp(user.mfaSecret, token);
    if (!isValid) {
      await this.logEvent(ctx, userId, MfaEventType.SETUP_FAILED, false, 'Invalid token');
      return false;
    }

    user.mfaEnabled = true;
    user.mfaEnabledAt = new Date();
    await this.userRepository.save(user);

    await this.logEvent(ctx, userId, MfaEventType.SETUP_COMPLETED, true);

    return true;
  }

  async disableMfa(ctx: ServiceContext, userId: string): Promise<boolean> {
    const user = await this.userRepository.findOne({
      where: { id: userId, tenantId: ctx.tenantId, deletedAt: IsNull() },
    });

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

    user.mfaEnabled = false;
    user.mfaSecret = null;
    user.mfaBackupCodes = null;
    user.mfaEnabledAt = null;
    await this.userRepository.save(user);

    await this.logEvent(ctx, userId, MfaEventType.DISABLED, true);

    return true;
  }

  async verifyMfa(ctx: ServiceContext, userId: string, token: string): Promise<MfaVerifyResult> {
    const user = await this.userRepository.findOne({
      where: { id: userId, tenantId: ctx.tenantId, deletedAt: IsNull() },
    });

    if (!user || !user.mfaEnabled || !user.mfaSecret) {
      return { success: false, error: 'MFA not enabled' };
    }

    // Check if it's a TOTP code
    if (this.verifyTotp(user.mfaSecret, token)) {
      await this.logEvent(ctx, userId, MfaEventType.VERIFIED, true);
      return { success: true };
    }

    // Check if it's a backup code
    if (await this.verifyBackupCode(ctx, userId, token)) {
      await this.logEvent(ctx, userId, MfaEventType.BACKUP_CODE_USED, true);
      return { success: true };
    }

    await this.logEvent(ctx, userId, MfaEventType.VERIFICATION_FAILED, false, 'Invalid token');
    return { success: false, error: 'Invalid MFA token' };
  }

  async verifyBackupCode(ctx: ServiceContext, userId: string, code: string): Promise<boolean> {
    const user = await this.userRepository.findOne({
      where: { id: userId, tenantId: ctx.tenantId, deletedAt: IsNull() },
    });

    if (!user || !user.mfaBackupCodes) {
      return false;
    }

    const hashedCode = this.hashCode(code);
    const codeIndex = user.mfaBackupCodes.indexOf(hashedCode);

    if (codeIndex === -1) {
      return false;
    }

    // Remove used backup code
    user.mfaBackupCodes.splice(codeIndex, 1);
    await this.userRepository.save(user);

    return true;
  }

  async regenerateBackupCodes(ctx: ServiceContext, userId: string): Promise<string[]> {
    const user = await this.userRepository.findOne({
      where: { id: userId, tenantId: ctx.tenantId, deletedAt: IsNull() },
    });

    if (!user || !user.mfaEnabled) {
      throw new Error('MFA not enabled');
    }

    const newCodes = this.generateBackupCodes();
    user.mfaBackupCodes = newCodes.map(code => this.hashCode(code));
    await this.userRepository.save(user);

    await this.logEvent(ctx, userId, MfaEventType.BACKUP_CODES_REGENERATED, true);

    return newCodes;
  }

  async getRemainingBackupCodes(ctx: ServiceContext, userId: string): Promise<number> {
    const user = await this.userRepository.findOne({
      where: { id: userId, tenantId: ctx.tenantId, deletedAt: IsNull() },
    });

    return user?.mfaBackupCodes?.length ?? 0;
  }

  async isMfaEnabled(ctx: ServiceContext, userId: string): Promise<boolean> {
    const user = await this.userRepository.findOne({
      where: { id: userId, tenantId: ctx.tenantId, deletedAt: IsNull() },
    });

    return user?.mfaEnabled ?? false;
  }

  async getAuditLogs(
    ctx: ServiceContext,
    filters: MfaFilters,
    page = 1,
    limit = 50,
  ): Promise<PaginatedResult<MfaAuditLog>> {
    const qb = this.auditLogRepository
      .createQueryBuilder('log')
      .leftJoinAndSelect('log.user', 'user')
      .where('log.tenant_id = :tenantId', { tenantId: ctx.tenantId });

    if (filters.userId) {
      qb.andWhere('log.user_id = :userId', { userId: filters.userId });
    }

    if (filters.eventType) {
      qb.andWhere('log.event_type = :eventType', { eventType: filters.eventType });
    }

    if (filters.success !== undefined) {
      qb.andWhere('log.success = :success', { success: filters.success });
    }

    if (filters.startDate) {
      qb.andWhere('log.created_at >= :startDate', { startDate: filters.startDate });
    }

    if (filters.endDate) {
      qb.andWhere('log.created_at <= :endDate', { endDate: filters.endDate });
    }

    const [data, total] = await qb
      .orderBy('log.created_at', 'DESC')
      .skip((page - 1) * limit)
      .take(limit)
      .getManyAndCount();

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  async getUserMfaStatus(ctx: ServiceContext, userId: string): Promise<{
    enabled: boolean;
    enabledAt: Date | null;
    backupCodesRemaining: number;
    lastVerification: Date | null;
  }> {
    const user = await this.userRepository.findOne({
      where: { id: userId, tenantId: ctx.tenantId, deletedAt: IsNull() },
    });

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

    const lastVerification = await this.auditLogRepository.findOne({
      where: {
        tenantId: ctx.tenantId,
        userId,
        eventType: MfaEventType.VERIFIED,
        success: true,
      },
      order: { createdAt: 'DESC' },
    });

    return {
      enabled: user.mfaEnabled ?? false,
      enabledAt: user.mfaEnabledAt ?? null,
      backupCodesRemaining: user.mfaBackupCodes?.length ?? 0,
      lastVerification: lastVerification?.createdAt ?? null,
    };
  }

  private async logEvent(
    ctx: ServiceContext,
    userId: string,
    eventType: MfaEventType,
    success: boolean,
    details?: string,
  ): Promise<void> {
    const log = this.auditLogRepository.create({
      tenantId: ctx.tenantId,
      userId,
      eventType,
      success,
      details,
      createdBy: ctx.userId ?? null,
    });

    await this.auditLogRepository.save(log);
  }

  private generateSecret(): string {
    return randomBytes(20).toString('base64').replace(/[^a-zA-Z2-7]/g, '').substring(0, 32);
  }

  private generateBackupCodes(): string[] {
    const codes: string[] = [];
    for (let i = 0; i < this.backupCodeCount; i++) {
      const code = randomBytes(4).toString('hex').toUpperCase();
      codes.push(`${code.substring(0, 4)}-${code.substring(4)}`);
    }
    return codes;
  }

  private hashCode(code: string): string {
    return createHmac('sha256', 'backup-code-salt').update(code).digest('hex');
  }

  private generateQrCodeUrl(email: string, secret: string): string {
    const encodedIssuer = encodeURIComponent(this.issuer);
    const encodedEmail = encodeURIComponent(email);
    return `otpauth://totp/${encodedIssuer}:${encodedEmail}?secret=${secret}&issuer=${encodedIssuer}`;
  }

  private verifyTotp(secret: string, token: string): boolean {
    // Simplified TOTP verification
    // In production, use a library like speakeasy or otplib
    const currentTime = Math.floor(Date.now() / 1000 / 30);

    for (let i = -1; i <= 1; i++) {
      const expectedToken = this.generateTotp(secret, currentTime + i);
      if (expectedToken === token) {
        return true;
      }
    }

    return false;
  }

  private generateTotp(secret: string, counter: number): string {
    const buffer = Buffer.alloc(8);
    buffer.writeBigInt64BE(BigInt(counter));

    const hmac = createHmac('sha1', Buffer.from(secret, 'base64'));
    hmac.update(buffer);
    const hash = hmac.digest();

    const offset = hash[hash.length - 1] & 0xf;
    const binary = ((hash[offset] & 0x7f) << 24) |
                   ((hash[offset + 1] & 0xff) << 16) |
                   ((hash[offset + 2] & 0xff) << 8) |
                   (hash[offset + 3] & 0xff);

    const otp = binary % 1000000;
    return otp.toString().padStart(6, '0');
  }
}
