
import {
  Injectable,
  Logger,
  BadRequestException,
  NotFoundException,
} from '@nestjs/common';
import { InjectRepository, InjectDataSource } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';

import { NcEntity } from './entities/nc.entity';
import { NcrEntryEntity } from './entities/ncr-entry.entity';
import { NcRemarkEntity } from './entities/nc-remark.entity';
import { CreateNcDto } from './dto/create-nc.dto';
import { createReadStream, existsSync, mkdirSync, writeFileSync, statSync } from 'fs';
import { join, extname, basename } from 'path';
import * as crypto from 'crypto';
import { MailsService } from '../mails/mails.service';
@Injectable()
export class NcService {
  private readonly logger = new Logger(NcService.name);

  constructor(
    @InjectRepository(NcEntity, 'scheme_dbs')
    private readonly ncRepo: Repository<NcEntity>,
    @InjectRepository(NcrEntryEntity, 'scheme_dbs')
    private readonly entryRepo: Repository<NcrEntryEntity>,
    @InjectRepository(NcRemarkEntity, 'scheme_dbs')
    private readonly remarkRepo: Repository<NcRemarkEntity>,
    // @InjectDataSource('scheme_dbs')
    // private readonly schemeDb: DataSource,
    @InjectDataSource('scheme_dbs')
    private readonly schemeDb: DataSource,
    private readonly mailsService: MailsService,   // 👈 NEW
  ) { }
  private readonly NC_STORAGE_ROOT =
    process.env.NC_UPLOAD_ROOT ||
    '/var/www/scheme_certiifcation/backend/storage/nc-uploads';

  private readonly NC_ALLOWED = new Set([
    '.pdf', '.doc', '.docx', '.jpg', '.jpeg', '.png', '.zip',
  ]);

  private parseDocs(raw?: string | null): { path: string; name: string }[] {
    if (!raw) return [];
    const s = String(raw).trim();
    if (!s) return [];
    if (s.startsWith('[')) {
      try {
        const arr = JSON.parse(s);
        return Array.isArray(arr)
          ? arr.map((x: any) => (typeof x === 'string'
            ? { path: x, name: x.split('/').pop() || 'file' }
            : { path: x.path, name: x.name || (x.path || '').split('/').pop() || 'file' }))
          : [];
      } catch { return []; }
    }
    return [{ path: s, name: s.split('/').pop() || 'file' }];
  }

  async clientRespondToFinding(
    ncId: number,
    entryId: number,
    companyIds: number[],
    correctiveAction: string,
    files?: { originalname: string; buffer: Buffer }[],
  ): Promise<{ ok: true; entry_id: number; documents: { path: string; name: string }[] }> {
    const nc = await this.ncRepo.findOne({ where: { id: ncId } });
    if (!nc || !companyIds.includes(Number(nc.company_id))) {
      throw new NotFoundException('NC not found');
    }
    const entry = await this.entryRepo.findOne({ where: { id: entryId, nc_id: ncId } });
    if (!entry) throw new NotFoundException('Finding not found');

    const docs = this.parseDocs(entry.document_path);

    if (files && files.length) {
      const dir = join(this.NC_STORAGE_ROOT, String(ncId));
      mkdirSync(dir, { recursive: true });
      for (const file of files) {
        if (!file?.buffer?.length) continue;
        const ext = extname(file.originalname).toLowerCase();
        if (!this.NC_ALLOWED.has(ext)) {
          throw new BadRequestException(`File type ${ext || '(none)'} not allowed`);
        }
        const safe = basename(file.originalname).replace(/[^\w.\-]+/g, '_');
        const fname = `${Date.now()}_${Math.random().toString(36).slice(2, 7)}_${safe}`;
        writeFileSync(join(dir, fname), file.buffer, { mode: 0o664 });
        docs.push({ path: `new:${ncId}/${fname}`, name: safe });
      }
    }

    entry.corrective_action = (correctiveAction || '').trim() || entry.corrective_action;
    entry.document_path = docs.length ? JSON.stringify(docs) : entry.document_path;
    entry.status = 'pending';
    await this.entryRepo.save(entry);

    // 👇 NEW: notify auditor + coordinators about the client upload.
    //    Wrapped in try/catch so email failures NEVER break the upload itself.
    try {
      await this.notifyStaffOnClientUpload(nc, entry, files || []);
    } catch (err: any) {
      this.logger.warn(`[client-upload-email] failed (upload itself succeeded): ${err.message}`);
    }

    return { ok: true, entry_id: entry.id, documents: docs };
  }

  // 👇 NEW HELPER — add anywhere in the class (I'd put it right after clientRespondToFinding)
  private async notifyStaffOnClientUpload(
    nc: NcEntity,
    entry: NcrEntryEntity,
    files: { originalname: string; buffer: Buffer }[],
  ): Promise<void> {
    // 1. Resolve the auditor — try followed_up_by, then closed_by, then created_by
    const ncRow = await this.schemeDb.query(
      `SELECT followed_up_by, closed_by, created_by FROM nc__ncs WHERE id = ? LIMIT 1`,
      [nc.id],
    );
    const auditorUserId: number | null =
      ncRow?.[0]?.followed_up_by || ncRow?.[0]?.closed_by || ncRow?.[0]?.created_by || null;

    let auditorEmail: string | null = null;
    let auditorName = 'Auditor';
    if (auditorUserId) {
      const uRows = await this.schemeDb.query(
        `SELECT email, firstName, lastName FROM users WHERE id = ? LIMIT 1`,
        [auditorUserId],
      );
      auditorEmail = (uRows?.[0]?.email || '').trim().toLowerCase() || null;
      const fn = (uRows?.[0]?.firstName || '').trim();
      const ln = (uRows?.[0]?.lastName || '').trim();
      auditorName = [fn, ln].filter(Boolean).join(' ') || 'Auditor';
    }

    // 2. Auditor is the only recipient — bail out if none
    if (!auditorEmail) {
      this.logger.warn(`[client-upload-email] no auditor email resolved for NC ${nc.id} — skipping`);
      return;
    }

    // 3. Company name (best-effort)
    let companyName = 'Client';
    try {
      const cRows = await this.schemeDb.query(
        `SELECT name FROM companies WHERE id = ? LIMIT 1`,
        [nc.company_id],
      );
      companyName = (cRows?.[0]?.name || '').trim() || 'Client';
    } catch { /* company name is optional */ }

    // 4. Attachments — the files the client just uploaded
    const attachments = (files || [])
      .filter((f) => f?.buffer?.length)
      .map((f) => ({
        filename: f.originalname,
        content: f.buffer,
        contentType: this.guessMime(f.originalname),
      }));

    // 5. Formatted timestamp
    const uploadedAt = new Date().toLocaleDateString('en-GB', {
      day: '2-digit', month: 'long', year: 'numeric',
      hour: '2-digit', minute: '2-digit',
    });

    // 6. Numbered file list (professional, no emoji)
    const fileRows = attachments.length
      ? attachments.map((a, i) => `
        <tr>
          <td style="padding:10px 14px;border-bottom:1px solid #e2e8f0;font-size:13px;color:#475569;width:32px;vertical-align:top;">${i + 1}.</td>
          <td style="padding:10px 14px;border-bottom:1px solid #e2e8f0;font-size:13px;color:#1e293b;vertical-align:top;word-break:break-all;">${this.esc(a.filename)}</td>
        </tr>`).join('')
      : `<tr><td colspan="2" style="padding:10px 14px;font-size:13px;color:#94a3b8;font-style:italic;">No files attached</td></tr>`;

    // 7. Build the professional email
    const subject = `New Evidence Submitted by Client — NC #${nc.id}`;
    const html = `<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>${this.esc(subject)}</title>
</head>
<body style="margin:0;padding:0;background:#f1f5f9;font-family:-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#1e293b;-webkit-font-smoothing:antialiased;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f1f5f9;">
<tr><td align="center" style="padding:40px 16px;">

  <table role="presentation" width="620" cellpadding="0" cellspacing="0" style="max-width:620px;width:100%;background:#ffffff;border-radius:14px;overflow:hidden;box-shadow:0 1px 3px rgba(15,23,42,0.08),0 4px 20px rgba(15,23,42,0.06);">

    <!-- Logo header (white background so logo stays crisp) -->
    <tr>
      <td style="background:#ffffff;padding:28px 40px 20px;text-align:center;border-bottom:1px solid #f1f5f9;">
        <img src="https://crm.qrs.ae/qrslogo.jpg" alt="Quality Registrar Systems" width="180" style="display:block;margin:0 auto;max-width:180px;height:auto;"/>
        <div style="margin-top:10px;font-size:10px;color:#94a3b8;font-weight:700;letter-spacing:0.15em;text-transform:uppercase;">Audit &amp; Certification Platform</div>
      </td>
    </tr>

    <!-- Title band -->
    <tr>
      <td style="background:#1a1054;padding:24px 40px;text-align:left;">
        <div style="color:#ffffff;font-size:20px;font-weight:600;line-height:1.3;letter-spacing:-0.01em;">New Evidence Received</div>
        <div style="color:rgba(255,255,255,0.75);font-size:13px;margin-top:6px;">Client response to Non-Conformity Report #${nc.id}</div>
      </td>
    </tr>

    <!-- Gold accent -->
    <tr><td style="height:4px;background:linear-gradient(90deg,#d4a843 0%,#7c3aed 100%);"></td></tr>

    <!-- Body -->
    <tr>
      <td style="padding:32px 40px 24px;">
        <p style="margin:0 0 18px;font-size:15px;line-height:1.6;color:#1e293b;">
          Dear <strong>${this.esc(auditorName)}</strong>,
        </p>
        <p style="margin:0 0 14px;font-size:14px;line-height:1.7;color:#475569;">
          We would like to inform you that <strong>${this.esc(companyName)}</strong> has submitted supporting evidence for a finding raised on <strong>Non-Conformity Report #${nc.id}</strong>.
        </p>
        <p style="margin:0 0 24px;font-size:14px;line-height:1.7;color:#475569;">
          The submission is now available for your review. Kindly examine the attached document(s) and update the finding status accordingly in the auditor dashboard at your earliest convenience.
        </p>

        <!-- Summary card -->
        <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e2e8f0;border-radius:8px;margin-bottom:24px;background:#f8fafc;">
          <tr>
            <td style="padding:14px 18px;border-bottom:1px solid #e2e8f0;font-size:11px;font-weight:700;color:#64748b;letter-spacing:0.08em;text-transform:uppercase;">Submission Summary</td>
          </tr>
          <tr>
            <td style="padding:16px 18px;">
              <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="font-size:13px;line-height:1.8;">
                <tr>
                  <td style="color:#64748b;width:140px;padding:4px 0;">Client</td>
                  <td style="color:#1e293b;font-weight:500;padding:4px 0;">${this.esc(companyName)}</td>
                </tr>
                <tr>
                  <td style="color:#64748b;padding:4px 0;">NC Report</td>
                  <td style="color:#1e293b;font-weight:500;padding:4px 0;">#${nc.id}</td>
                </tr>
                <tr>
                  <td style="color:#64748b;padding:4px 0;">Finding ID</td>
                  <td style="color:#1e293b;font-weight:500;padding:4px 0;">#${entry.id}</td>
                </tr>
                <tr>
                  <td style="color:#64748b;padding:4px 0;">Submitted on</td>
                  <td style="color:#1e293b;font-weight:500;padding:4px 0;">${this.esc(uploadedAt)}</td>
                </tr>
                <tr>
                  <td style="color:#64748b;padding:4px 0;">Files received</td>
                  <td style="color:#1e293b;font-weight:500;padding:4px 0;">${attachments.length}</td>
                </tr>
              </table>
            </td>
          </tr>
        </table>

        <!-- Files -->
        <div style="font-size:11px;font-weight:700;color:#64748b;letter-spacing:0.08em;text-transform:uppercase;margin-bottom:10px;">Attached Documents</div>
        <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border:1px solid #e2e8f0;border-radius:8px;overflow:hidden;margin-bottom:28px;">
          ${fileRows}
        </table>

        <p style="margin:0 0 6px;font-size:14px;line-height:1.6;color:#475569;">
          Should you require any assistance or clarification regarding this submission, please do not hesitate to reach out to our support team.
        </p>
        <p style="margin:24px 0 4px;font-size:14px;line-height:1.6;color:#475569;">Best regards,</p>
        <p style="margin:0;font-size:14px;line-height:1.6;color:#1e293b;font-weight:600;">Quality Registrar Systems</p>
        <p style="margin:2px 0 0;font-size:13px;line-height:1.6;color:#94a3b8;">Audit &amp; Certification Team</p>
      </td>
    </tr>

    <!-- Footer with mini logo -->
    <tr>
      <td style="padding:22px 40px 26px;background:#f8fafc;border-top:1px solid #e2e8f0;text-align:center;">
        <img src="https://crm.qrs.ae/qrslogo.jpg" alt="QRS" width="80" style="display:block;margin:0 auto 10px;max-width:80px;height:auto;opacity:0.55;"/>
        <div style="font-size:11px;color:#94a3b8;line-height:1.7;">
          This is an automated notification from the QRS Audit &amp; Certification Platform.<br/>
          &copy; ${new Date().getFullYear()} Quality Registrar Systems. All rights reserved.<br/>
          <a href="mailto:info@qrsyst.com" style="color:#4a0080;text-decoration:none;">info@qrsyst.com</a> &nbsp;&middot;&nbsp;
          <a href="https://qrsyst.com" style="color:#4a0080;text-decoration:none;">qrsyst.com</a>
        </div>
      </td>
    </tr>

  </table>

</td></tr></table>
</body></html>`;

    // 8. Send it — auditor only, no CC
    try {
      await this.mailsService.sendAsUser(auditorUserId, {
        to: auditorEmail,
        subject,
        html,
        ...(attachments.length ? { attachments } : {}),
      } as any);
      this.logger.log(
        `[client-upload-email] sent → to=${auditorEmail} attachments=${attachments.length}`,
      );
    } catch (err: any) {
      this.logger.error(`[client-upload-email] mailsService.sendAsUser failed: ${err.message}`);
      throw err;
    }
  }

  // 👇 NEW small helpers — put them near the other private helpers (e.g. after parseDocs)
  private esc(s: string): string {
    return String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  }
  private guessMime(name: string): string {
    const ext = (name.split('.').pop() || '').toLowerCase();
    return ext === 'pdf' ? 'application/pdf'
      : ext === 'png' ? 'image/png'
        : ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg'
          : ext === 'doc' ? 'application/msword'
            : ext === 'docx' ? 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
              : ext === 'zip' ? 'application/zip'
                : 'application/octet-stream';
  }

  async openFindingEvidence(
    ncId: number, entryId: number, companyIds: number[], index = 0,
  ): Promise<{ stream: any; size: number; mime: string; filename: string }> {
    const nc = await this.ncRepo.findOne({ where: { id: ncId } });
    if (!nc || !companyIds.includes(Number(nc.company_id))) {
      throw new NotFoundException('NC not found');
    }
    const entry = await this.entryRepo.findOne({ where: { id: entryId, nc_id: ncId } });
    const docs = this.parseDocs(entry?.document_path);
    const doc = docs[index];
    if (!doc) throw new NotFoundException('Evidence not found');

    const rel = doc.path.replace(/^new:/i, '').replace(/^\/+/, '');
    const abs = join(this.NC_STORAGE_ROOT, rel);
    if (!existsSync(abs)) throw new NotFoundException('Evidence file missing');

    const ext = extname(abs).toLowerCase();
    const mime =
      ext === '.pdf' ? 'application/pdf'
        : ext === '.png' ? 'image/png'
          : ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg'
            : ext === '.zip' ? 'application/zip'
              : ext === '.doc' ? 'application/msword'
                : ext === '.docx' ? 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
                  : 'application/octet-stream';
    return { stream: createReadStream(abs), size: statSync(abs).size, mime, filename: doc.name };
  }
  async create(
    dto: CreateNcDto,
    currentUserId: number,
  ): Promise<{
    ok: true;
    nc_id: number;
    audit_id: number;
    findings_created: number;
  }> {
    const validFindings = (dto.findings || []).filter((f) =>
      f.ncr_statement?.trim(),
    );
    if (validFindings.length === 0) {
      throw new BadRequestException(
        'At least one finding with a statement is required.',
      );
    }

    // Verify the audit row exists (FK would catch it, but a clear error is nicer).
    const auditExists = await this.schemeDb.query(
      `SELECT id FROM audit_schedule_rows WHERE id = ? LIMIT 1`,
      [dto.audit_id],
    );
    if (!auditExists.length) {
      throw new NotFoundException(`Audit ${dto.audit_id} not found`);
    }

    const qr = this.schemeDb.createQueryRunner();
    await qr.connect();
    await qr.startTransaction();

    try {
      // Build the NC + its findings via cascade, save in one call.
      const nc = qr.manager.create(NcEntity, {
        audit_id: dto.audit_id,
        company_id: dto.company_id ?? null,
        auditee_name: dto.auditee_name ?? null,
        audit_type: dto.audit_type ?? null,
        nc_type: dto.nc_type,
        status: dto.status ?? 'open',
        follow_up_date: dto.follow_up_date || null,
        due_date: dto.due_date || null,
        follow_up_notes: dto.follow_up_notes ?? null,
        remark: dto.remark ?? null,
        created_by: currentUserId,
        entries: validFindings.map((f) =>
          qr.manager.create(NcrEntryEntity, {
            nc_type: f.nc_type ?? dto.nc_type,
            ncr_statement: f.ncr_statement.trim(),
            criteria_clause: f.criteria_clause ?? null,
            corrective_action: f.corrective_action ?? null,
            status: f.status ?? 'open',
          }),
        ),
      });

      const savedNc = await qr.manager.save(NcEntity, nc);

      // Optional opening remark.
      if (dto.remark && dto.remark.trim()) {
        await qr.manager.save(
          NcRemarkEntity,
          qr.manager.create(NcRemarkEntity, {
            nc_id: savedNc.id,
            user_id: currentUserId,
            remark: dto.remark.trim(),
          }),
        );
      }

      await qr.commitTransaction();

      const findingsCreated = savedNc.entries?.length ?? validFindings.length;
      this.logger.log(
        `[NC-CREATE] User ${currentUserId} raised NC ${savedNc.id} for audit ` +
        `${dto.audit_id} with ${findingsCreated} finding(s)`,
      );

      return {
        ok: true,
        nc_id: savedNc.id,
        audit_id: dto.audit_id,
        findings_created: findingsCreated,
      };
    } catch (err) {
      await qr.rollbackTransaction();
      this.logger.error(
        `[NC-CREATE] Failed for audit ${dto.audit_id}: ${(err as Error).message}`,
        (err as Error).stack,
      );
      throw err;
    } finally {
      await qr.release();
    }
  }
  async getClientNcs(companyIds: number[]): Promise<any[]> {
    if (!companyIds || companyIds.length === 0) return [];

    const ncs = await this.ncRepo.find({
      where: companyIds.map((cid) => ({ company_id: cid })),
      order: { created_at: 'DESC' },
    });
    if (ncs.length === 0) return [];

    const ncIds = ncs.map((n) => n.id);
    // pull findings for all these NCs in one query
    const entries = await this.entryRepo
      .createQueryBuilder('e')
      .where('e.nc_id IN (:...ids)', { ids: ncIds })
      .getMany();

    const byNc = new Map<number, NcrEntryEntity[]>();
    for (const e of entries) {
      const arr = byNc.get(Number(e.nc_id)) || [];
      arr.push(e);
      byNc.set(Number(e.nc_id), arr);
    }

    return ncs.map((nc) => {
      const items = byNc.get(Number(nc.id)) || [];
      const open = items.filter((i) => i.status === 'open').length;
      const pending = items.filter((i) => i.status === 'pending').length;
      const closed = items.filter((i) => i.status === 'closed').length;
      return {
        id: nc.id,
        created_by: nc.created_by,
        audit_id: nc.audit_id,
        company_id: nc.company_id,
        auditee_name: nc.auditee_name,
        audit_type: nc.audit_type,
        nc_type: nc.nc_type,
        status: nc.status,
        due_date: nc.due_date,
        created_at: nc.created_at,
        findings_total: items.length,
        findings_open: open,
        findings_pending: pending,
        findings_closed: closed,
      };
    });
  }

  /**
   * One NC with all its findings — company-scoped so a client can only
   * open their own company's NC.
   */
  async getClientNcDetail(ncId: number, companyIds: number[]): Promise<any> {
    if (!companyIds || companyIds.length === 0) {
      throw new NotFoundException('NC not found');
    }
    const nc = await this.ncRepo.findOne({ where: { id: ncId } });
    if (!nc || !companyIds.includes(Number(nc.company_id))) {
      throw new NotFoundException('NC not found');
    }
    const entries = await this.entryRepo.find({
      where: { nc_id: ncId },
      order: { id: 'ASC' },
    });
    return {
      id: nc.id,
      created_by: nc.created_by,
      audit_id: nc.audit_id,
      company_id: nc.company_id,
      auditee_name: nc.auditee_name,
      audit_type: nc.audit_type,
      nc_type: nc.nc_type,
      status: nc.status,
      due_date: nc.due_date,
      follow_up_date: nc.follow_up_date,
      remark: nc.remark,
      created_at: nc.created_at,
      findings: entries.map((e) => ({
        id: e.id,
        nc_type: e.nc_type,
        ncr_statement: e.ncr_statement,
        criteria_clause: e.criteria_clause,
        corrective_action: e.corrective_action,
        document_path: e.document_path,
        documents: this.parseDocs(e.document_path),
        status: e.status,
      })),
    };
  }
  /** Save the client's signature + optional stamp for an NC. */
  async saveClientSignature(
    ncId: number,
    companyId: number,
    payload: { signer_name: string; signer_email?: string | null; signature_img: string; stamp_img?: string },
  ): Promise<{ ok: true; document_hash: string; signed_at: string }> {
    const ds = this.schemeDb; // your @InjectDataSource('scheme_dbs')

    // stamp: if provided, save to company (reused later). If not, load company's saved stamp.
    let stamp = payload.stamp_img || null;
    if (stamp) {
      await ds.query(`UPDATE companies SET nc_stamp_img = ? WHERE id = ?`, [stamp, companyId]);
    } else {
      const rows = await ds.query(`SELECT nc_stamp_img FROM companies WHERE id = ? LIMIT 1`, [companyId]);
      stamp = rows?.[0]?.nc_stamp_img || null;
    }

    const signedAt = new Date();
    const hash = crypto
      .createHash('sha256')
      .update(`${ncId}|${payload.signer_name}|${signedAt.toISOString()}`)
      .digest('hex')
      .slice(0, 12)
      .toUpperCase();

    // upsert (unique on nc_id)
    await ds.query(
      `INSERT INTO nc_signatures
         (nc_id, company_id, signer_name, signer_email, signature_img, stamp_img, signed_at, document_hash)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?)
       ON DUPLICATE KEY UPDATE
         signer_name = VALUES(signer_name),
         signer_email = VALUES(signer_email),
         signature_img = VALUES(signature_img),
         stamp_img = VALUES(stamp_img),
         signed_at = VALUES(signed_at),
         document_hash = VALUES(document_hash)`,
      [ncId, companyId, payload.signer_name, payload.signer_email || null, payload.signature_img, stamp, signedAt, hash],
    );

    return { ok: true, document_hash: hash, signed_at: signedAt.toISOString() };
  }

  /** Fetch a saved signature for an NC (used by the PDF builder). */
  async getNcSignature(ncId: number): Promise<any | null> {
    const ds = this.schemeDb;
    const rows = await ds.query(
      `SELECT signer_name, signer_email, signature_img, stamp_img, signed_at, document_hash
         FROM nc_signatures WHERE nc_id = ? LIMIT 1`,
      [ncId],
    );
    return rows?.[0] || null;
  }
}
