import { Repository, IsNull, LessThan } from 'typeorm';
import { randomBytes, createHash } from 'crypto';
import { PasswordReset, User } from '../entities';

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

export interface CreatePasswordResetDto {
  userId: string;
  email: string;
  expiresInMinutes?: number;
}

export interface PasswordResetFilters {
  userId?: string;
  email?: string;
  isUsed?: boolean;
  isExpired?: boolean;
}

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

export interface PasswordResetResult {
  passwordReset: PasswordReset;
  token: string;
}

export class PasswordResetService {
  constructor(
    private readonly passwordResetRepository: Repository<PasswordReset>,
    private readonly userRepository: Repository<User>,
  ) {}

  async create(ctx: ServiceContext, data: CreatePasswordResetDto): Promise<PasswordResetResult> {
    // Invalidate any existing reset tokens for this user
    await this.invalidateUserTokens(ctx, data.userId);

    const token = this.generateToken();
    const tokenHash = this.hashToken(token);
    const expiresInMinutes = data.expiresInMinutes ?? 60; // 1 hour default
    const expiresAt = new Date(Date.now() + expiresInMinutes * 60 * 1000);

    const passwordReset = this.passwordResetRepository.create({
      tenantId: ctx.tenantId,
      userId: data.userId,
      email: data.email,
      tokenHash,
      expiresAt,
      createdBy: ctx.userId ?? null,
    });

    const saved = await this.passwordResetRepository.save(passwordReset);

    return {
      passwordReset: saved,
      token, // Only returned once
    };
  }

  async createForEmail(ctx: ServiceContext, email: string): Promise<PasswordResetResult | null> {
    const user = await this.userRepository.findOne({
      where: { tenantId: ctx.tenantId, email, deletedAt: IsNull() },
    });

    if (!user) return null;

    return this.create(ctx, {
      userId: user.id,
      email: user.email,
    });
  }

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

  async findByUserId(ctx: ServiceContext, userId: string): Promise<PasswordReset[]> {
    return this.passwordResetRepository.find({
      where: { tenantId: ctx.tenantId, userId },
      order: { createdAt: 'DESC' },
    });
  }

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

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

    if (filters.email) {
      qb.andWhere('pr.email = :email', { email: filters.email });
    }

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

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

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

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

  async validateToken(ctx: ServiceContext, token: string): Promise<PasswordReset | null> {
    const tokenHash = this.hashToken(token);

    const passwordReset = await this.passwordResetRepository.findOne({
      where: {
        tenantId: ctx.tenantId,
        tokenHash,
        isUsed: false,
      },
      relations: ['user'],
    });

    if (!passwordReset) return null;
    if (passwordReset.expiresAt < new Date()) return null;

    return passwordReset;
  }

  async consumeToken(ctx: ServiceContext, token: string): Promise<PasswordReset | null> {
    const passwordReset = await this.validateToken(ctx, token);
    if (!passwordReset) return null;

    passwordReset.isUsed = true;
    passwordReset.usedAt = new Date();
    await this.passwordResetRepository.save(passwordReset);

    return passwordReset;
  }

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

    return result.affected ?? 0;
  }

  async isTokenValid(ctx: ServiceContext, token: string): Promise<boolean> {
    const passwordReset = await this.validateToken(ctx, token);
    return passwordReset !== null;
  }

  async getActiveResetForUser(ctx: ServiceContext, userId: string): Promise<PasswordReset | null> {
    return this.passwordResetRepository.findOne({
      where: {
        tenantId: ctx.tenantId,
        userId,
        isUsed: false,
      },
      order: { createdAt: 'DESC' },
    });
  }

  async hasActiveReset(ctx: ServiceContext, userId: string): Promise<boolean> {
    const reset = await this.getActiveResetForUser(ctx, userId);
    if (!reset) return false;
    return reset.expiresAt > new Date();
  }

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

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

    return result.affected ?? 0;
  }

  async getRateLimitInfo(ctx: ServiceContext, email: string, windowMinutes = 60): Promise<{
    count: number;
    firstRequestAt: Date | null;
    canRequest: boolean;
    maxRequests: number;
  }> {
    const windowStart = new Date(Date.now() - windowMinutes * 60 * 1000);
    const maxRequests = 3;

    const resets = await this.passwordResetRepository.find({
      where: {
        tenantId: ctx.tenantId,
        email,
      },
      order: { createdAt: 'ASC' },
    });

    const recentResets = resets.filter(r => r.createdAt > windowStart);

    return {
      count: recentResets.length,
      firstRequestAt: recentResets[0]?.createdAt ?? null,
      canRequest: recentResets.length < maxRequests,
      maxRequests,
    };
  }

  private generateToken(): string {
    return randomBytes(32).toString('hex');
  }

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