import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
import { createHash, randomInt } from 'crypto';
import * as QRCode from 'qrcode';
import { TrainingCertificate } from './entities/training-certificate.entity';
import { Standard } from '../standards/entities/standard.entity';
import { CreateTrainingCertificateDto } from './dto/create-training-certificate.dto';
import { UpdateTrainingCertificateDto } from './dto/update-training-certificate.dto';
import { BatchCreateTrainingCertificatesDto } from './dto/batch-create-training-certificates.dto';
import { CertificatesService } from '../certificates/certificates.service';

@Injectable()
export class TrainingCertificatesService {
  constructor(
    @InjectRepository(TrainingCertificate, 'scheme_dbs')
    private readonly trainingRepo: Repository<TrainingCertificate>,

    @InjectRepository(Standard, 'scheme_dbs')
    private readonly standardRepo: Repository<Standard>,

    // Reused ONLY for the unified /verify fall-through — company module untouched
    private readonly companyCertsService: CertificatesService,
  ) {}

  /* ═══════════════════════════════════════════════════
     VERIFICATION URL + QR  (same env vars, same verify
     page as the company certificates → one QR system)
  ═══════════════════════════════════════════════════ */

  /** Build verification URL based on domain */
  public buildVerificationUrl(certificate: TrainingCertificate): string {
    const localBase =
      process.env.TRAINING_VERIFY_URL_LOCAL ||
      'https://qrsyst.com/verify-training';
    const internationalBase =
      process.env.TRAINING_VERIFY_URL_INTERNATIONAL ||
      'https://cert-verification.qrs-intl.com/verify-training';

    const base =
      certificate.verification_domain === 'international'
        ? internationalBase
        : localBase;

    return `${base}?token=${certificate.qrcode_token}&fp=${certificate.fingerprint}`;
  }

  async generateQrCode(
    token: string,
    fingerprint?: string,
    domain: 'local' | 'international' = 'local',
  ): Promise<string> {
    const verificationUrls = {
      local:
        process.env.TRAINING_VERIFY_URL_LOCAL ||
        'https://qrsyst.com/verify-training',
      international:
        process.env.TRAINING_VERIFY_URL_INTERNATIONAL ||
        'https://cert-verification.qrs-intl.com/verify-training',
    };

    const baseUrl = verificationUrls[domain] || verificationUrls.local;

    let qrUrl = `${baseUrl}?token=${token}`;
    if (fingerprint) qrUrl += `&fp=${fingerprint}`;

    const qrCodeDataUrl = await QRCode.toDataURL(qrUrl, {
      errorCorrectionLevel: 'M',
      type: 'image/png',
      margin: 1,
      width: 250,
      scale: 6,
    });

    return qrCodeDataUrl;
  }

  private async generateUniqueToken(): Promise<string> {
    const token = uuidv4();

    const exists = await this.trainingRepo.findOne({
      where: { qrcode_token: token },
    });
    if (!exists) return token;

    return this.generateUniqueToken();
  }

  /** Generate a unique fingerprint */
  private async generateFingerprint(): Promise<string> {
    const raw = uuidv4();
    const fp = createHash('sha256')
      .update(raw)
      .digest('base64url')
      .slice(0, 12);

    const exists = await this.trainingRepo.findOne({
      where: { fingerprint: fp },
    });
    if (!exists) return fp;

    return this.generateFingerprint();
  }

  /* ═══════════════════════════════════════════════════
     CERTIFICATE NUMBER — QRS-TRG-{YY}-{XXXXXX}
     Random unique suffix (NOT sequential): 6 characters,
     mixed uppercase letters + digits, always at least
     2 digits. Confusable chars (0,O,1,I,L) are excluded
     so the printed number is unambiguous.
     e.g. QRS-TRG-26-A7K4XD

     Uniqueness is guaranteed by:
       1) crypto-secure random generation (~887M combos/yr)
       2) DB existence check before use
       3) the UNIQUE KEY on certificate_no as final guard
  ═══════════════════════════════════════════════════ */
  private static readonly CERT_LETTERS = 'ABCDEFGHJKMNPQRSTUVWXYZ'; // no O, I, L
  private static readonly CERT_DIGITS = '23456789'; // no 0, 1
  private static readonly CERT_SUFFIX_LENGTH = 6;

  /** Build one random suffix with at least 2 digits, rest letters/digits, shuffled */
  private buildRandomSuffix(): string {
    const letters = TrainingCertificatesService.CERT_LETTERS;
    const digits = TrainingCertificatesService.CERT_DIGITS;
    const all = letters + digits;
    const len = TrainingCertificatesService.CERT_SUFFIX_LENGTH;

    const chars: string[] = [];

    // Guarantee at least 2 digits
    chars.push(digits[randomInt(digits.length)]);
    chars.push(digits[randomInt(digits.length)]);

    // Fill the rest from the full alphanumeric set
    while (chars.length < len) {
      chars.push(all[randomInt(all.length)]);
    }

    // Fisher–Yates shuffle so digit positions are random
    for (let i = chars.length - 1; i > 0; i--) {
      const j = randomInt(i + 1);
      [chars[i], chars[j]] = [chars[j], chars[i]];
    }

    return chars.join('');
  }

  private async generateCertificateNo(): Promise<string> {
    const yy = new Date().getFullYear().toString().slice(-2);
    const prefix = `QRS-TRG-${yy}-`;

    // Retry loop: collisions are astronomically unlikely, but we
    // still verify against the DB, and the unique index on
    // certificate_no protects against any race condition.
    for (let attempt = 0; attempt < 20; attempt++) {
      const candidate = `${prefix}${this.buildRandomSuffix()}`;
      const exists = await this.trainingRepo.findOne({
        where: { certificate_no: candidate },
      });
      if (!exists) return candidate;
    }

    throw new Error(
      'Unable to generate unique training certificate number after multiple attempts.',
    );
  }

  /* ═══════════════════════════════════════════════════
     CREATE — single certificate
  ═══════════════════════════════════════════════════ */
  async create(dto: CreateTrainingCertificateDto): Promise<{
    certificate: TrainingCertificate;
    qrCode: string;
    verification_url: string;
  }> {
    // ── Certificate number: manual override or auto ──
    let certNo: string;
    if (dto.certificate_no && dto.certificate_no.trim().length > 0) {
      certNo = dto.certificate_no.trim().toUpperCase();
      const clash = await this.trainingRepo.findOne({
        where: { certificate_no: certNo },
      });
      if (clash) {
        throw new BadRequestException(
          `Certificate number ${certNo} already exists.`,
        );
      }
    } else {
      certNo = await this.generateCertificateNo();
    }

    // ── Optional standard link ──
    let standard: Standard | null = null;
    if (dto.standard_id) {
      standard = await this.standardRepo.findOne({
        where: { id: dto.standard_id },
      });
      if (!standard) {
        throw new NotFoundException(`Standard ${dto.standard_id} not found`);
      }
    }

    const qrToken = await this.generateUniqueToken();
    const fingerprint = await this.generateFingerprint();

    const certificate = this.trainingRepo.create({
      participant_name: dto.participant_name.trim(),
      participant_company: dto.participant_company
        ? dto.participant_company.trim()
        : null,
      course_title: dto.course_title.trim(),
      standard,
      training_location: dto.training_location.trim(),
      training_date: dto.training_date,
      training_date_end: dto.training_date_end ?? null,
      valid_until: dto.valid_until ?? null,
      certificate_no: certNo,
      qrcode_token: qrToken,
      fingerprint,
      verification_domain: dto.verification_domain || 'local',
      status: 'pending_scan',
      created_by: dto.created_by,
    });

    const saved = await this.trainingRepo.save(certificate);

    const qrCodeDataUrl = await this.generateQrCode(
      saved.qrcode_token,
      saved.fingerprint,
      saved.verification_domain as 'local' | 'international',
    );

    return {
      certificate: saved,
      qrCode: qrCodeDataUrl,
      verification_url: this.buildVerificationUrl(saved),
    };
  }

  /* ═══════════════════════════════════════════════════
     BATCH CREATE — one training session, many people
  ═══════════════════════════════════════════════════ */
  async batchCreate(dto: BatchCreateTrainingCertificatesDto): Promise<{
    batch_ref: string;
    count: number;
    certificates: Array<{
      certificate: TrainingCertificate;
      qrCode: string;
      verification_url: string;
    }>;
  }> {
    const batchRef = `TRG-${uuidv4().split('-')[0].toUpperCase()}`;
    const results: Array<{
      certificate: TrainingCertificate;
      qrCode: string;
      verification_url: string;
    }> = [];

    for (const p of dto.participants) {
      const single = await this.create({
        participant_name: p.participant_name,
        participant_company: p.participant_company,
        course_title: dto.course_title,
        standard_id: dto.standard_id,
        training_location: dto.training_location,
        training_date: dto.training_date,
        training_date_end: dto.training_date_end,
        valid_until: dto.valid_until,
        verification_domain: dto.verification_domain,
        created_by: dto.created_by,
      });

      // Tag every certificate of this session with the batch reference
      single.certificate.batch_ref = batchRef;
      single.certificate = await this.trainingRepo.save(single.certificate);

      results.push(single);
    }

    return {
      batch_ref: batchRef,
      count: results.length,
      certificates: results,
    };
  }

  /* ═══════════════════════════════════════════════════
     FIND
  ═══════════════════════════════════════════════════ */
  async findAll(page = 1, search?: string) {
    const take = 10;
    const skip = (page - 1) * take;

    const qb = this.trainingRepo
      .createQueryBuilder('cert')
      .leftJoinAndSelect('cert.standard', 'standard')
      .orderBy('cert.id', 'DESC')
      .skip(skip)
      .take(take);

    if (search && search.trim().length > 0) {
      const exactSearch = search.trim();
      const likeSearch = `%${search.trim()}%`;
      qb.where(
        '(cert.certificate_no = :exactSearch OR cert.batch_ref = :exactSearch OR cert.participant_name LIKE :likeSearch OR cert.participant_company LIKE :likeSearch OR cert.course_title LIKE :likeSearch)',
        { exactSearch, likeSearch },
      );
    }

    const [data, total] = await qb.getManyAndCount();

    return {
      data,
      total,
      page,
      lastPage: Math.ceil(total / take) || 1,
    };
  }

  async findOne(id: number): Promise<TrainingCertificate> {
    const certificate = await this.trainingRepo.findOne({
      where: { id },
      relations: ['standard'],
    });
    if (!certificate) {
      throw new NotFoundException(`Training certificate ${id} not found`);
    }
    return certificate;
  }

  /** Find by certificate_no (manual search on the verify page) */
  async findByCertificateNo(certificateNo: string) {
    return this.trainingRepo.findOne({
      where: { certificate_no: certificateNo.trim().toUpperCase() },
      relations: ['standard'],
    });
  }

  /* ═══════════════════════════════════════════════════
     UPDATE / DELETE
  ═══════════════════════════════════════════════════ */
  async updatePartial(
    id: number,
    dto: Partial<UpdateTrainingCertificateDto>,
  ): Promise<TrainingCertificate> {
    const cert = await this.findOne(id);

    // Certificate number change — guard the unique index
    if (dto.certificate_no !== undefined && dto.certificate_no !== null) {
      const newNo = dto.certificate_no.trim().toUpperCase();
      if (newNo !== cert.certificate_no) {
        const clash = await this.trainingRepo.findOne({
          where: { certificate_no: newNo },
        });
        if (clash && clash.id !== id) {
          throw new BadRequestException(
            `Certificate number ${newNo} already exists.`,
          );
        }
        cert.certificate_no = newNo;
      }
    }

    // Standard change
    if (dto.standard_id !== undefined) {
      const standard = await this.standardRepo.findOne({
        where: { id: dto.standard_id },
      });
      if (!standard) {
        throw new NotFoundException(`Standard ${dto.standard_id} not found`);
      }
      cert.standard = standard;
    }

    if (dto.participant_name !== undefined) {
      cert.participant_name = dto.participant_name.trim();
    }
    if (dto.participant_company !== undefined) {
      cert.participant_company = dto.participant_company
        ? dto.participant_company.trim()
        : null;
    }
    if (dto.course_title !== undefined) {
      cert.course_title = dto.course_title.trim();
    }
    if (dto.training_location !== undefined) {
      cert.training_location = dto.training_location.trim();
    }
    if (dto.training_date !== undefined) {
      cert.training_date = dto.training_date;
    }
    if (dto.training_date_end !== undefined) {
      cert.training_date_end = dto.training_date_end ?? null;
    }
    if (dto.valid_until !== undefined) {
      cert.valid_until = dto.valid_until ?? null;
    }
    if (dto.verification_domain !== undefined) {
      cert.verification_domain = dto.verification_domain;
    }
    if (dto.status !== undefined) {
      cert.status = dto.status;
    }
    if (dto.updated_by !== undefined) {
      cert.updated_by = dto.updated_by;
    }

    return this.trainingRepo.save(cert);
  }

  async update(
    id: number,
    dto: UpdateTrainingCertificateDto,
  ): Promise<TrainingCertificate> {
    return this.updatePartial(id, dto);
  }

  async remove(id: number): Promise<{ message: string }> {
    const cert = await this.findOne(id);
    await this.trainingRepo.remove(cert);
    return {
      message: `Training certificate ${cert.certificate_no} deleted`,
    };
  }

  /* ═══════════════════════════════════════════════════
     SCAN UPLOAD — the uploaded signed scan IS the
     digital certificate; first upload activates it
  ═══════════════════════════════════════════════════ */
  async attachScan(id: number, fileUrl: string, uploadedBy?: number) {
    const cert = await this.trainingRepo.findOne({ where: { id } });
    if (!cert) {
      throw new NotFoundException(`Training certificate ${id} not found`);
    }

    cert.scan_pdf_url = fileUrl;
    cert.scan_uploaded_at = new Date();
    if (uploadedBy) cert.scan_uploaded_by = uploadedBy;

    if (cert.status === 'pending_scan') {
      cert.status = 'active';
    }

    const saved = await this.trainingRepo.save(cert);
    return {
      message: 'Scan uploaded — certificate is now active',
      certificate: saved,
    };
  }

  /* ═══════════════════════════════════════════════════
     UNIFIED PUBLIC VERIFY
     One endpoint for the verify page + all QR codes:
     1) checks training certificates
     2) falls through to company certification certs
        (existing CertificatesService — untouched)
  ═══════════════════════════════════════════════════ */
  async verify(token: string, fingerprint?: string): Promise<any> {
    if (!token || token.trim().length === 0) {
      throw new BadRequestException('Token is required for verification');
    }

    const trainingCert = await this.trainingRepo
      .createQueryBuilder('cert')
      .leftJoinAndSelect('cert.standard', 'standard')
      .where('cert.qrcode_token = :token', { token: token.trim() })
      .getOne();

    if (trainingCert) {
      return this.buildTrainingVerifyResponse(trainingCert, fingerprint);
    }

    // Not a training certificate → check company certification certificates
    const companyResult = await this.companyCertsService.verifyCertificate(
      token,
      fingerprint,
    );
    return { record_type: 'certification', ...companyResult };
  }

  /* ═══════════════════════════════════════════════════
     PUBLIC MANUAL VERIFY — certificate number + name
     Powers the website's "Verify Manually" form.
     Security notes:
       • needs the EXACT certificate number (random, unguessable)
       • AND the participant name must match (fuzzy, word-based)
       • returns only the same safe fields as verify()
       • never reveals whether a cert number exists on its own
  ═══════════════════════════════════════════════════ */
  async verifyManual(certNo: string, participantName: string): Promise<any> {
    const notFound = {
      record_type: 'training',
      valid: false,
      message:
        'No matching training certificate found. Please check your details and try again.',
    };

    const cleanCertNo = (certNo || '').trim();
    const cleanName = (participantName || '').trim();

    // Always answer 200 + JSON so the public page shows a friendly
    // message instead of a gateway error.
    if (!cleanCertNo || !cleanName) {
      return {
        record_type: 'training',
        valid: false,
        message: 'Both certificate number and participant name are required.',
      };
    }
    if (cleanCertNo.length > 80 || cleanName.length > 250) {
      return notFound;
    }

    const cert = await this.trainingRepo
      .createQueryBuilder('cert')
      .leftJoinAndSelect('cert.standard', 'standard')
      .where('UPPER(cert.certificate_no) = UPPER(:certNo)', {
        certNo: cleanCertNo,
      })
      .getOne();

    // Unknown cert no OR name mismatch → identical answer (no leaking)
    if (!cert || !this.nameMatches(cleanName, cert.participant_name || '')) {
      return notFound;
    }

    // Same safe response as the QR flow (handles expiry, fake, etc.).
    // pending_scan certs are shown normally — the scan upload only
    // controls whether the digital-certificate link appears.
    return this.buildTrainingVerifyResponse(cert);
  }

  /** Normalise a name for comparison: trim, UPPERCASE, collapse spaces */
  private normName(s: string): string {
    return (s || '').trim().toUpperCase().replace(/\s+/g, ' ');
  }

  /**
   * Fuzzy name match (same rule as the website):
   * every word (≥2 chars) the visitor typed must appear in the
   * stored participant name. "Wael Betar" matches
   * "WAEL MESEEF AL BETAR", but "Ahmed" does not.
   */
  private nameMatches(userInput: string, recordName: string): boolean {
    const input = this.normName(userInput);
    const target = this.normName(recordName);
    if (!input || !target) return false;
    if (target.includes(input)) return true;
    const words = input.split(' ').filter((w) => w.length >= 2);
    if (words.length === 0) return target.includes(input);
    return words.every((w) => target.includes(w));
  }

  private isExpired(cert: TrainingCertificate): boolean {
    if (!cert.valid_until) return false; // no expiry = valid forever
    return new Date(cert.valid_until).getTime() < Date.now();
  }

  private buildTrainingVerifyResponse(
    certificate: TrainingCertificate,
    fingerprint?: string,
  ) {
    // Fingerprint mismatch = tampered QR
    if (fingerprint && certificate.fingerprint !== fingerprint) {
      return {
        record_type: 'training',
        valid: false,
        message: 'Certificate has been tampered with or is fake',
      };
    }

    // Compute expiry dynamically (valid_until is optional).
    // NOTE: 'pending_scan' no longer blocks verification — the record in
    // our database is genuine from the moment it is issued (same behaviour
    // as company certification certificates). The uploaded scan only adds
    // the digital-certificate link below; until then the link is null.
    let status = certificate.status;
    if (status === 'pending_scan') {
      status = 'active';
    }
    if (status === 'active' && this.isExpired(certificate)) {
      status = 'expired';
    }

    return {
      record_type: 'training',
      valid: true,
      status,
      certificate_no: certificate.certificate_no,
      participant_name: certificate.participant_name,
      participant_company: certificate.participant_company,
      course_title: certificate.course_title,
      standard: certificate.standard,
      training_location: certificate.training_location,
      training_date: certificate.training_date,
      training_date_end: certificate.training_date_end,
      valid_until: certificate.valid_until,
      verification_domain: certificate.verification_domain,
      // The uploaded scan IS the digital certificate (null until uploaded)
      digital_certificate_url: certificate.scan_pdf_url || null,
      scan_uploaded: !!certificate.scan_pdf_url,
    };
  }
}