import {
  Injectable,
  Logger,
  OnModuleDestroy,
  OnModuleInit,
  Inject,
  forwardRef,
} from '@nestjs/common';
import * as puppeteer from 'puppeteer';
import * as Handlebars from 'handlebars';
import * as fs from 'fs/promises';
import * as path from 'path';

import { PreviousNcService, PreviousNcDetailDto } from './previous-nc.service';

@Injectable()
export class PreviousNcPdfService implements OnModuleInit, OnModuleDestroy {
  private readonly logger = new Logger(PreviousNcPdfService.name);
  private browser: puppeteer.Browser | null = null;
  private launchPromise: Promise<puppeteer.Browser> | null = null;

  private ncTemplate: HandlebarsTemplateDelegate | null = null;
  private attendanceTemplate: HandlebarsTemplateDelegate | null = null;

  constructor(
    @Inject(forwardRef(() => PreviousNcService))
    private readonly previousNcService: PreviousNcService,
  ) { }
  private async ncSignatureLookup(ncId: number): Promise<any | null> {
    try {
      // reuse whatever scheme_dbs DataSource this service or previousNcService has.
      // If previousNcService exposes the datasource, use it; otherwise inject one.
      const ds = (this.previousNcService as any).schemeDb;
      if (!ds) return null;
      const rows = await ds.query(
        `SELECT signer_name, signature_img, stamp_img, signed_at, document_hash
           FROM nc_signatures WHERE nc_id = ? LIMIT 1`,
        [ncId],
      );
      return rows?.[0] || null;
    } catch { return null; }
  }
  async onModuleInit(): Promise<void> {
    this.registerHelpers();

    // Pre-compile templates so we don't hit disk on every request.
    const ncTemplatePath = path.join(
      __dirname,
      '..',
      'templates',
      'nc-report.template.html',
    );
    const attendanceTemplatePath = path.join(
      __dirname,
      '..',
      'templates',
      'nc-attendance.template.html',
    );

    const [ncRaw, attendanceRaw] = await Promise.all([
      fs.readFile(ncTemplatePath, 'utf8'),
      fs.readFile(attendanceTemplatePath, 'utf8'),
    ]);

    this.ncTemplate = Handlebars.compile(ncRaw);
    this.attendanceTemplate = Handlebars.compile(attendanceRaw);

    // Warm-launch the browser — but don't fail boot if it errors.
    // The first PDF request will retry.
    try {
      await this.getBrowser();
      this.logger.log('[PREV-NC-PDF] Puppeteer launched, ready');
    } catch (err) {
      this.logger.error(
        `[PREV-NC-PDF] Warm launch failed (will retry on first request): ${(err as Error).message}`,
      );
    }
  }

  async onModuleDestroy(): Promise<void> {
    if (this.browser) {
      try {
        await this.browser.close();
      } catch {
        /* ignore — process may already be dead */
      }
      this.browser = null;
      this.logger.log('[PREV-NC-PDF] Puppeteer closed');
    }
  }

  // ═══════════════════════════════════════════════════════════════
  // PUBLIC API
  // ═══════════════════════════════════════════════════════════════

  async generateNcReportPdf(
    source: 'QRS' | 'TQS' | 'NEW',
    id: number,
    currentUserId: number,
  ): Promise<Buffer> {
    if (!this.ncTemplate) {
      throw new Error('NC template not compiled');
    }
    const data = await this.previousNcService.findOne(source, id, currentUserId);
    const ctx: any = this.buildNcReportContext(data);

    // digital signature/stamp (only exists for NEW/client NCs)
    if (source === 'NEW') {
      const sig = await this.ncSignatureLookup(id);
      if (sig) {
        ctx.auditee_signature_img = sig.signature_img || '';
        ctx.company_stamp_img = sig.stamp_img || '';
        ctx.auditee_signed_date = sig.signed_at
          ? new Date(sig.signed_at).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
          : '';
        ctx.signed_by = sig.signer_name || '';
        ctx.document_hash = sig.document_hash || '';
      }
    }

    const html = this.ncTemplate(ctx);
    return this.htmlToPdf(html, { orientation: 'landscape' });
  }

  async generateAttendancePdf(
    source: 'QRS' | 'TQS' | 'NEW',
    id: number,
    currentUserId: number,
  ): Promise<Buffer> {
    if (!this.attendanceTemplate) {
      throw new Error('Attendance template not compiled');
    }

    this.logger.log(
      `[PREV-NC-PDF] Generating attendance PDF for ${source}/${id}`,
    );

    try {
      const data = await this.previousNcService.findOne(
        source,
        id,
        currentUserId,
      );
      const ctx = this.buildAttendanceContext(data);
      this.logger.log(
        `[PREV-NC-PDF] Attendance ctx: company="${ctx.company_name}", auditees=${ctx.auditees.length}`,
      );
      const html = this.attendanceTemplate(ctx);
      return await this.htmlToPdf(html, { orientation: 'portrait' });
    } catch (err) {
      this.logger.error(
        `[PREV-NC-PDF] Attendance PDF generation failed: ${(err as Error).message}`,
        (err as Error).stack,
      );
      throw err;
    }
  }

  // ═══════════════════════════════════════════════════════════════
  // BROWSER LIFECYCLE — lazy + auto-relaunch
  // ═══════════════════════════════════════════════════════════════

  /**
   * Get a live browser. Launches if needed; relaunches if dead.
   * Guards against concurrent launches with a shared promise.
   */
  private async getBrowser(): Promise<puppeteer.Browser> {
    // Already alive — fast path.
    if (this.browser && this.browser.connected) {
      return this.browser;
    }

    // Another request is already launching — piggyback its promise.
    if (this.launchPromise) {
      return this.launchPromise;
    }

    // Browser is dead or never launched — launch (or relaunch) now.
    if (this.browser) {
      this.logger.warn(
        '[PREV-NC-PDF] Browser disconnected, relaunching Chromium',
      );
      try {
        await this.browser.close();
      } catch {
        /* ignore */
      }
      this.browser = null;
    }

    this.launchPromise = puppeteer
      .launch({
        headless: true,
        // Safer flag set — removed --single-process and --no-zygote which
        // were causing the browser to crash after multiple PDF requests.
        args: [
          '--no-sandbox',
          '--disable-setuid-sandbox',
          '--disable-dev-shm-usage',
          '--disable-gpu',
          '--disable-software-rasterizer',
          '--disable-extensions',
          '--disable-background-networking',
          '--disable-default-apps',
          '--disable-sync',
          '--disable-translate',
          '--hide-scrollbars',
          '--metrics-recording-only',
          '--mute-audio',
          '--no-first-run',
          '--safebrowsing-disable-auto-update',
        ],
        protocolTimeout: 60_000,
      })
      .then((b) => {
        this.browser = b;

        b.on('disconnected', () => {
          this.logger.warn(
            '[PREV-NC-PDF] Browser disconnected event fired',
          );
          this.browser = null;
        });

        this.logger.log(
          `[PREV-NC-PDF] Chromium launched (pid=${b.process()?.pid ?? 'unknown'})`,
        );
        return b;
      })
      .finally(() => {
        this.launchPromise = null;
      });

    return this.launchPromise;
  }

  /**
   * Render HTML → PDF with one retry on transient connection errors.
   */
  private async htmlToPdf(
    html: string,
    opts: { orientation: 'portrait' | 'landscape' },
  ): Promise<Buffer> {
    let lastErr: Error | null = null;

    for (let attempt = 1; attempt <= 2; attempt++) {
      let page: puppeteer.Page | null = null;
      try {
        const browser = await this.getBrowser();
        page = await browser.newPage();

        await page.setContent(html, {
          waitUntil: 'domcontentloaded',
          timeout: 30_000,
        });

        const pdf = await page.pdf({
          format: 'A4',
          landscape: opts.orientation === 'landscape',
          printBackground: true,
          margin: {
            top: '12mm',
            right: '12mm',
            bottom: '12mm',
            left: '12mm',
          },
        });

        return Buffer.from(pdf);
      } catch (err) {
        lastErr = err as Error;
        const msg = lastErr.message || '';

        const isTransient =
          msg.includes('Connection closed') ||
          msg.includes('Target closed') ||
          msg.includes('Session closed') ||
          msg.includes('detached Frame') ||
          msg.includes('Protocol error');

        this.logger.warn(
          `[PREV-NC-PDF] htmlToPdf attempt ${attempt} failed: ${msg}`,
        );

        if (!isTransient || attempt >= 2) break;

        // Force relaunch for the retry.
        if (this.browser) {
          try {
            await this.browser.close();
          } catch {
            /* ignore */
          }
        }
        this.browser = null;
      } finally {
        if (page) {
          try {
            await page.close();
          } catch {
            /* ignore */
          }
        }
      }
    }

    throw lastErr ?? new Error('PDF generation failed for unknown reason');
  }

  // ═══════════════════════════════════════════════════════════════
  // CONTEXT BUILDERS
  // ═══════════════════════════════════════════════════════════════

  private buildNcReportContext(data: PreviousNcDetailDto) {
    const { nc, entries } = data;

    const normalized = (nc.audit_type || '').toLowerCase().replace(/[\s-]/g, '');
    const isInitial = normalized.includes('initialaudit') || normalized === 'initial';
    const isSurveillance = normalized.includes('surveillance');
    const isReassessment =
      normalized.includes('reassessment') ||
      normalized.includes('re-assessment') ||
      normalized.includes('recertification');
    const isOther = !isInitial && !isSurveillance && !isReassessment;

    const surveMatch = nc.audit_type?.match(/no\.\s*(\d+)/i);
    const surveillanceNumber = surveMatch ? surveMatch[1] : '01';

    // 🆕 NEW source: build the AUDITEE NAME field from the structured list
    //    → "Name1 | Designation1, Name2 | Designation2".
    //    QRS/TQS: keep the legacy single auditee_name string (unchanged).
    const structuredAuditees = this.parseStructuredAuditees(
      (nc as any).auditees_json,
    );

    let auditeeNameField: string;
    let auditeeNameOnly: string;

    if (structuredAuditees.length) {
      // multiple auditees → joined display
      auditeeNameField = structuredAuditees
        .map((a) =>
          a.position ? `${a.name} | ${a.position}` : a.name,
        )
        .join(', ');
      // signature line uses the first attendee's name only
      auditeeNameOnly = structuredAuditees[0].name;
    } else {
      // legacy behaviour (QRS/TQS, or NEW with no structured list)
      auditeeNameField = nc.auditee_name || 'N/A';
      auditeeNameOnly = nc.auditee_name || '';
      if (auditeeNameOnly.includes('|')) {
        auditeeNameOnly = auditeeNameOnly.split('|')[0].trim();
      } else if (auditeeNameOnly.includes('-')) {
        auditeeNameOnly = auditeeNameOnly.split('-')[0].trim();
      }
    }

    return {
      total_pages: 1,
      company_name: nc.company_name || 'N/A',
      auditee_name: auditeeNameField,                 // 🆕 combined for NEW
      auditee_name_only: auditeeNameOnly || 'N/A',    // 🆕 first name only
      auditee_designation: nc.designation || 'N/A',
      audit_date: this.formatDate(nc.audit_date),
      audit_type: nc.audit_type || '',
      is_initial: isInitial,
      is_surveillance: isSurveillance,
      is_reassessment: isReassessment,
      is_other: isOther,
      surveillance_number: surveillanceNumber,
      standards: nc.standard_names?.length
        ? nc.standard_names.join(', ')
        : 'N/A',
      auditor_name: nc.created_by_name || 'N/A',
      follow_up_date: this.formatDate(nc.follow_up_date),
      verification_comments: '',
      entries: (entries || []).map((e, i) => ({
        index: i + 1,
        nc_type: e.nc_type || '',
        ncr_statement_html: this.formatNcrStatement(e.ncr_statement),
        criteria_clause_html: this.formatCriteriaClause(e.criteria_clause),
        corrective_action: e.corrective_action || '',
        status: this.ucfirst(e.status || ''),
      })),
    };
  }

  private buildAttendanceContext(data: PreviousNcDetailDto) {
    const nc = data?.nc;

    if (!nc) {
      return {
        total_pages: 1,
        company_name: 'N/A',
        audit_date: 'N/A',
        auditor_name: '',
        auditees: [] as Array<{ name: string; position: string }>,
        empty_rows: this.makeEmptyRows(7),
      };
    }

    // 🆕 NEW source: use the structured auditees array if present.
    //    QRS/TQS: fall back to the legacy string parser (unchanged).
    const structured = this.parseStructuredAuditees((nc as any).auditees_json);
    const auditees = structured.length
      ? structured
      : this.parseAuditees(nc.auditee_name);

    const blanks = Math.max(0, 7 - auditees.length);

    return {
      total_pages: 1,
      company_name: nc.company_name || 'N/A',
      audit_date: this.formatDate(nc.audit_date),
      auditor_name: nc.created_by_name || '',
      auditees,
      empty_rows: this.makeEmptyRows(blanks),
    };
  }
  private makeEmptyRows(n: number): Array<{ idx: number }> {
    const rows: Array<{ idx: number }> = [];
    for (let i = 0; i < n; i++) rows.push({ idx: i + 1 });
    return rows;
  }

  // ═══════════════════════════════════════════════════════════════
  // FORMATTERS
  // ═══════════════════════════════════════════════════════════════

  private formatNcrStatement(raw: string | null): string {
    if (!raw) return '';
    const escaped = this.escapeHtml(raw);
    const match = escaped.match(/^([^\n\r:]{3,}):\s*([\s\S]*)/);
    if (match) {
      const heading = match[1];
      const body = match[2].replace(/\n/g, '<br>');
      return `<strong>${heading}:</strong><br>${body}`;
    }
    return escaped.replace(/\n/g, '<br>');
  }

  private formatCriteriaClause(raw: string | null): string {
    if (!raw) return '';
    return raw
      .split('\n')
      .map((line) => {
        const trimmed = line.trim();
        const match = trimmed.match(/^(\d+(\.\d+)*)(.*)/);
        if (match) {
          const clause = match[1];
          const rest = this.escapeHtml(match[3].trim());
          return `<strong>${clause}</strong><br>${rest}`;
        }
        return this.escapeHtml(line);
      })
      .join('<br>');
  }

  private parseAuditees(
    raw: string | null | undefined,
  ): Array<{ name: string; position: string }> {
    if (!raw || typeof raw !== 'string') return [];

    const trimmed = raw.trim();
    if (!trimmed) return [];

    const parts = trimmed.split('|');
    const namesPart = (parts[0] || '').trim();
    const defaultPos = (parts[1] || '').trim();

    if (!namesPart) return [];

    const entries = namesPart
      .split(',')
      .map((e) => (e || '').trim())
      .filter(Boolean);

    if (!entries.length) return [];

    return entries.map((entry) => {
      if (entry.includes('-')) {
        const dashParts = entry.split('-');
        const name = (dashParts[0] || '').trim();
        const pos = (dashParts[1] || defaultPos || '').trim();
        return { name, position: pos };
      }
      return { name: entry, position: defaultPos };
    });
  }
  private parseStructuredAuditees(
    raw: any,
  ): Array<{ name: string; position: string }> {
    if (!raw) return [];
    let arr: any;
    try {
      arr = typeof raw === 'string' ? JSON.parse(raw) : raw;
    } catch {
      return [];
    }
    if (!Array.isArray(arr)) return [];
    return arr
      .map((a) => ({
        name: String(a?.name ?? '').trim(),
        position: String(a?.designation ?? a?.position ?? '').trim(),
      }))
      .filter((a) => a.name);
  }
  private formatDate(d: string | Date | null | undefined): string {
    if (!d) return 'N/A';
    try {
      const date = typeof d === 'string' ? new Date(d) : d;
      if (!date || isNaN(date.getTime())) return 'N/A';
      const day = String(date.getDate()).padStart(2, '0');
      const month = date.toLocaleString('en-US', { month: 'long' });
      const year = date.getFullYear();
      return `${day} ${month} ${year}`;
    } catch {
      return 'N/A';
    }
  }

  private ucfirst(s: string): string {
    if (!s) return '';
    return s.charAt(0).toUpperCase() + s.slice(1);
  }

  private escapeHtml(s: string): string {
    return s
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#039;');
  }

  private registerHelpers(): void {
    Handlebars.registerHelper('checkbox', (checked: boolean) =>
      checked ? '☒' : '□',
    );
    Handlebars.registerHelper('raw', (s: string) =>
      new Handlebars.SafeString(s || ''),
    );
  }
}