import { Repository, IsNull } from 'typeorm';
import { randomInt } from 'crypto';
import { VerificationCode, CodeType, User } from '../entities';

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

export interface CreateVerificationCodeDto {
  userId: string;
  type: CodeType;
  destination: string; // email or phone
  expiresInMinutes?: number;
}

export interface VerificationFilters {
  userId?: string;
  type?: CodeType;
  isUsed?: boolean;
  isExpired?: boolean;
}

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

export interface VerificationResult {
  verification: VerificationCode;
  code: string;
}

export class VerificationService {
  constructor(
    private readonly verificationRepository: Repository<VerificationCode>,
    private readonly userRepository: Repository<User>,
  ) {}

  async create(ctx: ServiceContext, data: CreateVerificationCodeDto): Promise<VerificationResult> {
    // Invalidate existing codes of same type
    await this.invalidateExisting(ctx, data.userId, data.type);

    const code = this.generateCode(data.type);
    const expiresInMinutes = data.expiresInMinutes ?? this.getDefaultExpiry(data.type);
    const expiresAt = new Date(Date.now() + expiresInMinutes * 60 * 1000);

    const verification = this.verificationRepository.create({
      tenantId: ctx.tenantId,
      userId: data.userId,
      type: data.type,
      code,
      destination: data.destination,
      expiresAt,
      attempts: 0,
      maxAttempts: 5,
      createdBy: ctx.userId ?? null,
    });

    const saved = await this.verificationRepository.save(verification);

    return {
      verification: saved,
      code,
    };
  }

  async createEmailVerification(ctx: ServiceContext, userId: string, email: string): Promise<VerificationResult> {
    return this.create(ctx, {
      userId,
      type: CodeType.EMAIL,
      destination: email,
    });
  }

  async createSmsVerification(ctx: ServiceContext, userId: string, phone: string): Promise<VerificationResult> {
    return this.create(ctx, {
      userId,
      type: CodeType.SMS,
      destination: phone,
      expiresInMinutes: 5, // Shorter for SMS
    });
  }

  async createMfaVerification(ctx: ServiceContext, userId: string, destination: string): Promise<VerificationResult> {
    return this.create(ctx, {
      userId,
      type: CodeType.MFA,
      destination,
      expiresInMinutes: 5,
    });
  }

  async findById(ctx: ServiceContext, id: string): Promise<VerificationCode | null> {
    return this.verificationRepository.findOne({
      where: { id, tenantId: ctx.tenantId },
      relations: ['user'],
    });
  }

  async findActiveByUserId(ctx: ServiceContext, userId: string, type?: CodeType): Promise<VerificationCode | null> {
    const where: Record<string, unknown> = {
      tenantId: ctx.tenantId,
      userId,
      isUsed: false,
    };

    if (type) {
      where.type = type;
    }

    return this.verificationRepository.findOne({
      where,
      order: { createdAt: 'DESC' },
    });
  }

  async findWithFilters(
    ctx: ServiceContext,
    filters: VerificationFilters,
    page = 1,
    limit = 50,
  ): Promise<PaginatedResult<VerificationCode>> {
    const qb = this.verificationRepository
      .createQueryBuilder('vc')
      .leftJoinAndSelect('vc.user', 'user')
      .where('vc.tenant_id = :tenantId', { tenantId: ctx.tenantId });

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

    if (filters.type) {
      qb.andWhere('vc.type = :type', { type: filters.type });
    }

    if (filters.isUsed !== undefined) {
      qb.andWhere('vc.is_used = :isUsed', { isUsed: filters.isUsed });
    }

    if (filters.isExpired !== undefined) {
      if (filters.isExpired) {
        qb.andWhere('vc.expires_at < :now', { now: new Date() });
      } else {
        qb.andWhere('vc.expires_at >= :now', { now: new Date() });
      }
    }

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

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

  async verify(ctx: ServiceContext, userId: string, code: string, type?: CodeType): Promise<{
    success: boolean;
    verification?: VerificationCode;
    error?: string;
  }> {
    const verification = await this.findActiveByUserId(ctx, userId, type);

    if (!verification) {
      return { success: false, error: 'No active verification code found' };
    }

    if (verification.expiresAt < new Date()) {
      return { success: false, error: 'Verification code has expired' };
    }

    if (verification.attempts >= verification.maxAttempts) {
      return { success: false, error: 'Maximum verification attempts exceeded' };
    }

    verification.attempts++;
    await this.verificationRepository.save(verification);

    if (verification.code !== code) {
      return {
        success: false,
        error: `Invalid code. ${verification.maxAttempts - verification.attempts} attempts remaining`,
      };
    }

    verification.isUsed = true;
    verification.usedAt = new Date();
    await this.verificationRepository.save(verification);

    return { success: true, verification };
  }

  async invalidateExisting(ctx: ServiceContext, userId: string, type: CodeType): Promise<number> {
    const result = await this.verificationRepository.update(
      {
        tenantId: ctx.tenantId,
        userId,
        type,
        isUsed: false,
      },
      {
        isUsed: true,
        usedAt: new Date(),
      },
    );

    return result.affected ?? 0;
  }

  async resend(ctx: ServiceContext, userId: string, type: CodeType): Promise<VerificationResult | null> {
    const existing = await this.findActiveByUserId(ctx, userId, type);
    if (!existing) return null;

    // Check rate limit
    const rateLimit = await this.getRateLimitInfo(ctx, userId, type);
    if (!rateLimit.canResend) {
      throw new Error(`Please wait ${rateLimit.secondsUntilResend} seconds before requesting a new code`);
    }

    return this.create(ctx, {
      userId,
      type,
      destination: existing.destination,
    });
  }

  async getRateLimitInfo(ctx: ServiceContext, userId: string, type: CodeType): Promise<{
    count: number;
    canResend: boolean;
    secondsUntilResend: number;
    maxPerHour: number;
  }> {
    const hourAgo = new Date(Date.now() - 60 * 60 * 1000);
    const maxPerHour = 5;
    const minIntervalSeconds = 60;

    const recent = await this.verificationRepository.find({
      where: {
        tenantId: ctx.tenantId,
        userId,
        type,
      },
      order: { createdAt: 'DESC' },
      take: maxPerHour,
    });

    const recentInHour = recent.filter(v => v.createdAt > hourAgo);
    const mostRecent = recent[0];

    let secondsUntilResend = 0;
    if (mostRecent) {
      const timeSinceLastRequest = (Date.now() - mostRecent.createdAt.getTime()) / 1000;
      if (timeSinceLastRequest < minIntervalSeconds) {
        secondsUntilResend = Math.ceil(minIntervalSeconds - timeSinceLastRequest);
      }
    }

    return {
      count: recentInHour.length,
      canResend: recentInHour.length < maxPerHour && secondsUntilResend === 0,
      secondsUntilResend,
      maxPerHour,
    };
  }

  async cleanupExpired(ctx: ServiceContext): Promise<number> {
    const cleanupDate = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours old

    const result = await this.verificationRepository
      .createQueryBuilder()
      .delete()
      .where('tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('(expires_at < :now OR created_at < :cleanupDate)', {
        now: new Date(),
        cleanupDate,
      })
      .execute();

    return result.affected ?? 0;
  }

  private generateCode(type: CodeType): string {
    switch (type) {
      case CodeType.EMAIL:
        return String(randomInt(100000, 999999)); // 6 digits
      case CodeType.SMS:
        return String(randomInt(100000, 999999)); // 6 digits
      case CodeType.MFA:
        return String(randomInt(100000, 999999)); // 6 digits
      default:
        return String(randomInt(100000, 999999));
    }
  }

  private getDefaultExpiry(type: CodeType): number {
    switch (type) {
      case CodeType.EMAIL:
        return 60; // 1 hour
      case CodeType.SMS:
        return 5; // 5 minutes
      case CodeType.MFA:
        return 5; // 5 minutes
      default:
        return 15;
    }
  }
}
