import { Injectable, Logger, NotFoundException, OnModuleDestroy } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as fs from 'fs';
import * as path from 'path';
import * as Handlebars from 'handlebars';
import * as puppeteer from 'puppeteer';
import { PDFDocument } from 'pdf-lib';

import { AuditReport } from '../entities/audit-report.entity';
import { ReportTemplate } from '../entities/report-template.entity';
import { ReportBuilderService } from './report-builder.service';
import { ReportImageService } from './report-image.service';
import { imsIconFor, profileFor } from '../report.standards';

export type RenderPart = 'all' | 'cover' | 'body';

/**
 * Turns a report into HTML / PDF.
 *  - draft / submitted / returned  → rendered live from the database (always current)
 *  - approved / locked             → rendered from the frozen snapshot_json (never changes)
 *
 * One shared Chromium instance is kept for the life of the process (launching one per request is slow).
 */
@Injectable()
export class ReportRenderService implements OnModuleDestroy {
  private readonly logger = new Logger(ReportRenderService.name);
  private browser: puppeteer.Browser | null = null;
  private browserLaunching: Promise<puppeteer.Browser> | null = null;
  private readonly compiled = new Map<string, HandlebarsTemplateDelegate>();
  private assets: { cover: string; crest: string; stripe: string } | null = null;
  private readonly coverCache = new Map<string, string>();
  private readonly hb = Handlebars.create();

  constructor(
    @InjectRepository(AuditReport, 'scheme_dbs') private readonly reports: Repository<AuditReport>,
    @InjectRepository(ReportTemplate, 'scheme_dbs') private readonly templates: Repository<ReportTemplate>,
    private readonly builder: ReportBuilderService,
    private readonly images: ReportImageService,
  ) {
    this.registerHelpers();
  }

  async onModuleDestroy() {
    try { await this.browser?.close(); } catch { /* ignore */ }
  }

  // ───────────────────────── paths ─────────────────────────
  /** Folder holding form-04-2a.hbs and the images. Override with AUDIT_REPORT_TEMPLATE_DIR. */
  private templateDir(): string {
    if (process.env.AUDIT_REPORT_TEMPLATE_DIR) return process.env.AUDIT_REPORT_TEMPLATE_DIR;
    // Works from dist (when the assets entry copies the folder) and from src (ts-node / nest start --watch).
    const candidates = [
      path.join(__dirname, '..', 'templates'),
      path.join(process.cwd(), 'dist', 'audit-report-builder', 'templates'),
      path.join(process.cwd(), 'src', 'audit-report-builder', 'templates'),
    ];
    return candidates.find((d) => fs.existsSync(path.join(d, 'form-04-2a.hbs'))) ?? candidates[0];
  }

  /** Same folder, exposed so the Word export can read the cover / crest / stripe images. */
  templateDirPublic(): string {
    return this.templateDir();
  }

  private dataUri(file: string, mime: string): string {
    const p = path.join(this.templateDir(), file);
    if (!fs.existsSync(p)) {
      this.logger.warn(`[REPORT-RENDER] asset missing: ${p}`);
      return '';
    }
    return `data:${mime};base64,${fs.readFileSync(p).toString('base64')}`;
  }

  /** Cover picture of a theme (cover.jpg = blue 9001 design, cover-14001.jpg = green 14001 design). */
  private coverFor(file: string): string {
    if (!file) return '';
    if (!this.coverCache.has(file)) this.coverCache.set(file, this.dataUri(file, /\.png$/i.test(file) ? 'image/png' : 'image/jpeg'));
    return this.coverCache.get(file)!;
  }

  private loadAssets() {
    if (!this.assets) {
      this.assets = {
        cover: this.dataUri('cover.jpg', 'image/jpeg'),
        crest: this.dataUri('crest.png', 'image/png'),
        stripe: this.dataUri('header-stripe.jpg', 'image/jpeg'),
      };
    }
    return this.assets;
  }

  private registerHelpers() {
    this.hb.registerHelper('or', (a: any, b: any) => a || b);
    this.hb.registerHelper('eq', (a: any, b: any) => a === b);
    this.hb.registerHelper('inc', (n: number) => Number(n) + 1);
    this.hb.registerHelper('join', (arr: any, sep: any) => (Array.isArray(arr) ? arr.join(typeof sep === 'string' ? sep : ' / ') : ''));
    // one rich-text run (bold / italic / underline set by the auditor in the editor), HTML-escaped
    this.hb.registerHelper('richRun', (r: any) => {
      let h = this.hb.escapeExpression(String(r?.t ?? ''));
      if (r?.u) h = `<u>${h}</u>`;
      if (r?.i) h = `<i>${h}</i>`;
      if (r?.b) h = `<b>${h}</b>`;
      return new this.hb.SafeString(h);
    });
    this.hb.registerHelper('cb', (on: any) =>
      new this.hb.SafeString(`<span class="cb${on ? ' on' : ''}"></span>`),
    );
  }

  private compile(file: string): HandlebarsTemplateDelegate {
    const p = path.join(this.templateDir(), file);
    const stat = fs.statSync(p, { throwIfNoEntry: false } as any);
    if (!stat) throw new NotFoundException(`Report template file not found: ${p}`);
    const key = `${p}:${stat.mtimeMs}`; // recompiles automatically when the file is edited
    let fn = this.compiled.get(key);
    if (!fn) {
      fn = this.hb.compile(fs.readFileSync(p, 'utf8'));
      this.compiled.set(key, fn);
    }
    return fn;
  }

  // ───────────────────────── view model ─────────────────────────
  async getViewModel(reportId: number): Promise<{ vm: any; report: AuditReport }> {
    const report = await this.builder.getReportOrFail(reportId);
    if (['approved', 'locked'].includes(report.status) && report.snapshot_json) {
      return { vm: JSON.parse(report.snapshot_json), report };
    }
    return { vm: await this.builder.buildViewModel(reportId), report };
  }

   /**
   * Stage 1 (initial readiness) and Stage 2 / Surveillance / Recertification each have their own form layout,
   * copied from the Word originals. A template row that names a file other than the old shared form wins.
   */
  private async templateFile(report: AuditReport): Promise<string> {
    if (report.template_id) {
      const t = await this.templates.findOne({ where: { id: report.template_id } });
      if (t?.hbs_file && t.hbs_file !== 'form-04-2a.hbs') return t.hbs_file;
    }
    return report.stage_type === 'stage1' ? 'form-04-1-stage1.hbs' : 'form-04-2-stage2.hbs';
  }

  // ───────────────────────── HTML ─────────────────────────
  async renderHtml(reportId: number, part: RenderPart = 'all', opts: { watermark?: boolean } = {}): Promise<string> {
    const { vm, report } = await this.getViewModel(reportId);
    return this.renderFromVm(vm, report, part, opts);
  }

  async renderFromVm(vm: any, report: AuditReport, part: RenderPart, opts: { watermark?: boolean } = {}): Promise<string> {
    const fn = this.compile(await this.templateFile(report));
    const a = this.loadAssets();
    const profile = profileFor(vm.meta?.standard_name);
    const theme = vm.theme ?? profile.theme;
    return fn({
      ...this.images.inline(vm),
      // reports approved before the per-standard wording existed have no labels in their snapshot
      labels: vm.labels ?? profile.labels,
      theme,
      // 2026-11-06: IMS cover columns carry the round icon of each standard (as in the sample Word cover)
      cover: vm.cover && Array.isArray(vm.cover.ims_columns)
        ? { ...vm.cover, ims_columns: vm.cover.ims_columns.map((k: any) => { const f = imsIconFor(k?.main); return { ...k, icon: f ? this.coverFor(f) : '' }; }) }
        : vm.cover,
      assets: { ...a, cover: this.coverFor(theme.cover.file) || a.cover, crest_cover: theme.cover.crest ? this.coverFor(theme.cover.crest) : '' },
      part_cover: part === 'all' || part === 'cover',
      part_body: part === 'all' || part === 'body',
      show_watermark: opts.watermark ?? vm.meta?.draft,
    });
  }

  // ───────────────────────── PDF ─────────────────────────
  private async getBrowser(): Promise<puppeteer.Browser> {
    if (this.browser && this.browser.connected) return this.browser;
    if (!this.browserLaunching) {
      this.browserLaunching = puppeteer
        .launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] })
        .then((b) => {
          this.browser = b;
          b.on('disconnected', () => { this.browser = null; });
          return b;
        })
        .finally(() => { this.browserLaunching = null; });
    }
    return this.browserLaunching;
  }

  private async htmlToPdf(html: string, withFooter: boolean, footerLeft: string, headerHtml = '<span></span>'): Promise<Buffer> {
    const browser = await this.getBrowser();
    const page = await browser.newPage();
    try {
      await page.setContent(html, { waitUntil: 'load' });
      const buf = await page.pdf({
        format: 'A4',
        printBackground: true,
        preferCSSPageSize: true,
        displayHeaderFooter: withFooter,
        headerTemplate: headerHtml,
        footerTemplate: withFooter
          ? `<div style="width:100%;font-size:8px;font-family:Calibri,Carlito,Arial,sans-serif;color:#555;text-align:center;">
               Page <span class="pageNumber"></span> of <span class="totalPages"></span> &nbsp;—&nbsp; ${footerLeft}
             </div>`
          : '<span></span>',
      });
      return Buffer.from(buf);
    } finally {
      await page.close().catch(() => undefined);
    }
  }

  /** Crest + blue stripe repeated at the top of every body page (the Word header). */
  private headerHtml(): string {
    const a = this.loadAssets();
    if (!a.crest) return '<span></span>';
    // Same header as the Word form (crest 19.4mm tall, stripe through its middle).
    // Chrome draws header templates about 5mm lower than the numbers written here, so the values below are
    // already reduced by that amount: the crest ends at ~24.5mm and the page body starts at 26mm.
    return `<style>html,body{margin:0;padding:0;}</style>
    <div style="width:100%;height:25mm;position:relative;margin:0;padding:0;">
      ${a.stripe ? `<img src="${a.stripe}" style="position:absolute;left:0;top:9.1mm;width:100%;height:4.2mm;">` : ''}
      <img src="${a.crest}" style="position:absolute;left:14.8mm;top:0;height:19.4mm;">
    </div>`;
  }

  /**
   * The cover is printed on its own (full-bleed, no footer) and the body with a page-number footer;
   * pdf-lib joins them. Passing a vm renders from that data (used by approve to freeze a snapshot).
   */
  async renderPdf(reportId: number, override?: { vm: any; report: AuditReport }): Promise<Buffer> {
    const { vm, report } = override ?? (await this.getViewModel(reportId));
    const [coverHtml, bodyHtml] = await Promise.all([
      this.renderFromVm(vm, report, 'cover'),
      this.renderFromVm(vm, report, 'body'),
    ]);
    const footer = 'This document is uncontrolled once printed/downloaded.';
    const [cover, body] = await Promise.all([
      this.htmlToPdf(coverHtml, false, ''),
      this.htmlToPdf(bodyHtml, true, footer, this.headerHtml()),
    ]);

    const out = await PDFDocument.create();
    for (const src of [cover, body]) {
      const doc = await PDFDocument.load(src);
      const pages = await out.copyPages(doc, doc.getPageIndices());
      pages.forEach((p) => out.addPage(p));
    }
    out.setTitle(`Audit Report ${vm.meta?.report_no ?? ''}`.trim());
    out.setAuthor(process.env.AUDIT_REPORT_PDF_AUTHOR || 'Quality Registrar Systems');
    out.setCreator(process.env.AUDIT_REPORT_PDF_AUTHOR || 'Quality Registrar Systems');
    out.setProducer(process.env.AUDIT_REPORT_PDF_AUTHOR || 'Quality Registrar Systems');
    return Buffer.from(await out.save());
  }
    /** Cover page only (Chromium). Used by ReportPdfService, which joins it with the Word-rendered body. */
  async renderCoverPdf(vm: any, report: AuditReport): Promise<Buffer> {
    const html = await this.renderFromVm(vm, report, 'cover');
    return this.htmlToPdf(html, false, '');
  }
}
