import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import * as crypto from 'crypto';
import { PrismaService } from '../prisma/prisma.service';

const STALE_PROCESSING_MINUTES = 5;
const CODE_CHARSET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'; // excludes 0/O, 1/I/L — matches legacy generator

@Injectable()
export class ActivationService {
  constructor(private readonly prisma: PrismaService) {}

  generateCodeString(prefix = 'raseedna'): string {
    const groups: string[] = [];
    for (let g = 0; g < 3; g++) {
      let chunk = '';
      for (let i = 0; i < 4; i++) {
        chunk += CODE_CHARSET[crypto.randomInt(0, CODE_CHARSET.length)];
      }
      groups.push(chunk);
    }
    return `${prefix}-${groups.join('-')}`;
  }

  async createCode(codeType = 'standard', source = '', createdBy?: string) {
    for (let attempt = 0; attempt < 5; attempt++) {
      const code = this.generateCodeString();
      try {
        return await this.prisma.activationCode.create({
          data: { code, codeType, source, createdBy: createdBy ?? null },
        });
      } catch {
        // unique collision — retry with a freshly generated code
      }
    }
    throw new ConflictException('تعذّر إنشاء كود فريد، حاول مرة أخرى.');
  }

  async createBatch(count: number, codeType = 'standard', source = '', createdBy?: string) {
    const safeCount = Math.max(1, Math.min(5000, Math.floor(count)));
    const codes = [];
    for (let i = 0; i < safeCount; i++) {
      codes.push(await this.createCode(codeType, source, createdBy));
    }
    return codes;
  }

  /** Reclaims codes stuck in 'processing' for longer than the timeout (a crashed request). */
  private async reclaimStaleProcessing() {
    const cutoff = new Date(Date.now() - STALE_PROCESSING_MINUTES * 60 * 1000);
    await this.prisma.activationCode.updateMany({
      where: { status: 'processing', processingStartedAt: { lt: cutoff } },
      data: { status: 'unused', processingToken: null, processingStartedAt: null },
    });
  }

  /**
   * Atomically claims a code: UPDATE ... WHERE status = 'unused' is a single
   * row operation, so a race between two requests can only ever let one
   * succeed. Mirrors inc/codes-system.php exactly.
   */
  async claimCode(rawCode: string): Promise<{ id: string; processingToken: string }> {
    await this.reclaimStaleProcessing();

    const processingToken = crypto.randomUUID();
    const result = await this.prisma.activationCode.updateMany({
      where: { code: rawCode, status: 'unused' },
      data: { status: 'processing', processingToken, processingStartedAt: new Date() },
    });

    if (result.count === 0) {
      const existing = await this.prisma.activationCode.findUnique({ where: { code: rawCode } });
      if (!existing) throw new NotFoundException('الكود غير صحيح.');
      throw new ConflictException('هذا الكود مستخدم بالفعل أو غير صالح.');
    }

    const claimed = await this.prisma.activationCode.findUnique({ where: { code: rawCode } });
    return { id: claimed!.id, processingToken };
  }

  /** Called after account creation succeeds. */
  async finalizeCode(codeId: string, userId: string) {
    await this.prisma.activationCode.update({
      where: { id: codeId },
      data: { status: 'used', userId, usedAt: new Date(), processingToken: null },
    });
  }

  /** Called if account creation fails after the code was claimed — code must never be permanently lost. */
  async releaseCode(codeId: string) {
    await this.prisma.activationCode.update({
      where: { id: codeId },
      data: { status: 'unused', processingToken: null, processingStartedAt: null },
    });
  }

  /**
   * Redeem a code against an ALREADY-LOGGED-IN account (e.g. a gift code
   * for extra/renewed access), as opposed to the registration-time flow
   * above. Kept as a thin, separate extension point per the architecture
   * requirement to keep Activation Codes decoupled from Payments — a future
   * "code grants N months of premium" rule plugs in here without touching
   * registration.
   */
  async redeemForExistingUser(userId: string, rawCode: string) {
    const claimed = await this.claimCode(rawCode);
    await this.finalizeCode(claimed.id, userId);
    return { redeemed: true };
  }
}
