import { createHash } from 'node:crypto';
import { promises as fs } from 'node:fs';
import path from 'node:path';

import { PDFDocument } from 'pdf-lib';

import type { EoiContext } from '@/lib/services/eoi-context';
import { logger } from '@/lib/logger';

/**
 * Pinned sha256 of `assets/eoi-template.pdf`. Bump this AND the README
 * checksum in lockstep when the template is intentionally re-cut. A
 * mismatch only logs a warning (we don't want a malformed swap to take
 * the EOI flow offline at boot), but ops monitoring will surface it.
 */
export const EXPECTED_EOI_SHA256 =
  'ba495fd88d99ebe4b7f61acbe397fb2f1cd116e1e1f1b217de93106915c7c44b';

let shaCheckPerformed = false;

/**
 * Source PDF for the in-app EOI pathway. Must contain AcroForm fields whose
 * names match the Documenso template's `formValues` keys exactly:
 *
 *   Text:     Name, Email, Address, Yacht Name, Length, Width, Draft,
 *             Berth Number
 *   Checkbox: Lease_10, Purchase
 *
 * See assets/eoi-template/README.md for full details and the field mapping
 * doc at docs/eoi-documenso-field-mapping.md for the canonical list.
 */
const DEFAULT_EOI_TEMPLATE_PATH = path.join(process.cwd(), 'assets', 'eoi-template.pdf');

function eoiTemplatePath(): string {
  return process.env.EOI_TEMPLATE_PDF_PATH ?? DEFAULT_EOI_TEMPLATE_PATH;
}

export async function loadEoiTemplatePdf(): Promise<Uint8Array> {
  const filePath = eoiTemplatePath();
  let bytes: Buffer;
  try {
    bytes = await fs.readFile(filePath);
  } catch (err) {
    throw new Error(
      `EOI source PDF not found at ${filePath}. Drop the same PDF used by the Documenso template (with AcroForm fields: Name, Email, Address, Yacht Name, Length, Width, Draft, Berth Number, Lease_10, Purchase) at this path, or override via EOI_TEMPLATE_PDF_PATH. Original error: ${(err as Error).message}`,
    );
  }

  // SHA-pin check (M-6): warn once per process when the template's bytes
  // don't match the committed hash. Skipped entirely when the override
  // env var is in play (fixture / dev workflows are expected to use
  // arbitrary PDFs). Only checks the default path so test setups stay
  // unconstrained.
  if (!shaCheckPerformed && !process.env.EOI_TEMPLATE_PDF_PATH) {
    shaCheckPerformed = true;
    const actual = createHash('sha256').update(bytes).digest('hex');
    if (actual !== EXPECTED_EOI_SHA256) {
      logger.warn(
        { expected: EXPECTED_EOI_SHA256, actual },
        'EOI source PDF sha256 mismatch - template was modified without an EXPECTED_EOI_SHA256 bump. Update assets/README.md + EXPECTED_EOI_SHA256 in lockstep if this was intentional.',
      );
    }
  }

  return bytes;
}

function formatAddress(address: EoiContext['client']['address']): string {
  if (!address) return '';
  // EOI's Address field renders as: "street, city, REGION, postal, COUNTRY"
  // with REGION as the ISO-3166-2 suffix (e.g. NY) and COUNTRY as the
  // alpha-2 code (e.g. US) so the line fits in the PDF box. The separate
  // `Nationality` PDF field has been retired - the resident's country code
  // here is the canonical replacement.
  return [address.street, address.city, address.subdivision, address.postalCode, address.countryIso]
    .filter(Boolean)
    .join(', ');
}

function setText(form: ReturnType<PDFDocument['getForm']>, name: string, value: string): void {
  try {
    form.getTextField(name).setText(value);
  } catch {
    // Field absent or wrong type - skip so a slightly different PDF
    // template still produces output, but surface a warning so a re-cut
    // template with drifted field names is observable in ops logs
    // instead of shipping silently with empty values. Only warn when
    // there's a real value to render; an empty value would be a no-op
    // even if the field existed.
    if (value && value.trim().length > 0) {
      logger.warn(
        { field: name },
        `EOI in-app PDF template is missing AcroForm field "${name}" - value was dropped. Update the source template.`,
      );
    }
  }
}

function setCheckbox(
  form: ReturnType<PDFDocument['getForm']>,
  name: string,
  checked: boolean,
): void {
  try {
    const cb = form.getCheckBox(name);
    if (checked) cb.check();
    else cb.uncheck();
  } catch {
    logger.warn(
      { field: name, checked },
      `EOI in-app PDF template is missing checkbox AcroForm field "${name}" - checkbox state was dropped. Update the source template.`,
    );
  }
}

/**
 * Fills the AcroForm fields of the EOI source PDF with values drawn from
 * EoiContext. Field names mirror the Documenso template `formValues` keys so
 * a single source PDF can serve both pathways.
 *
 * The form is **flattened** after filling so the recipient can't edit
 * pre-filled values (yacht dimensions, address, berth number) after the
 * fact. Documenso pathway flattens server-side; this brings the in-app
 * pathway to parity. Set metadata so the artifact carries Title/Author/
 * Lang for downstream readers and a11y tooling.
 */
export async function fillEoiFormFields(
  pdfBytes: Uint8Array,
  context: EoiContext,
  options?: { dimensionUnit?: 'ft' | 'm' },
): Promise<Uint8Array> {
  const doc = await PDFDocument.load(pdfBytes);
  const form = doc.getForm();

  setText(form, 'Name', context.client.fullName);
  setText(form, 'Email', context.client.primaryEmail ?? '');
  setText(form, 'Address', formatAddress(context.client.address));
  // Yacht + berth (EOI Section 3) are optional - leave the AcroForm fields
  // blank when the interest hasn't been linked to either. Dimension side
  // (ft|m) honours the drawer's toggle; legacy callers omit and get ft.
  setText(form, 'Yacht Name', context.yacht?.name ?? '');
  const dimUnit: 'ft' | 'm' = options?.dimensionUnit ?? context.yacht?.lengthUnit ?? 'ft';
  const yLen = dimUnit === 'ft' ? context.yacht?.lengthFt : context.yacht?.lengthM;
  const yWid = dimUnit === 'ft' ? context.yacht?.widthFt : context.yacht?.widthM;
  const yDra = dimUnit === 'ft' ? context.yacht?.draftFt : context.yacht?.draftM;
  // Append the unit suffix so the rendered EOI reads "45 ft" / "13.7 m"
  // rather than the bare number - matches the Documenso pathway.
  const withDimUnit = (v: string | null | undefined): string =>
    v && String(v).trim() ? `${String(v).trim()} ${dimUnit}` : '';
  setText(form, 'Length', withDimUnit(yLen));
  setText(form, 'Width', withDimUnit(yWid));
  setText(form, 'Draft', withDimUnit(yDra));
  // Berth Number = compact range for multi-berth, primary mooring for
  // single-berth (formatBerthRange(['A1']) === 'A1' so single-berth is
  // byte-identical to the legacy primary-only path). The dedicated
  // `Berth Range` AcroForm field was retired 2026-05-14 - the source
  // PDF only carries `Berth Number`.
  setText(form, 'Berth Number', context.eoiBerthRange || (context.berth?.mooringNumber ?? ''));

  setCheckbox(form, 'Purchase', true);
  setCheckbox(form, 'Lease_10', false);

  // PDF metadata so the artifact carries Title/Author/Lang downstream.
  doc.setTitle(`EOI – ${context.client.fullName}`);
  doc.setAuthor(context.port.name);
  doc.setSubject('Expression of Interest');
  doc.setLanguage('en-GB');
  doc.setProducer('Port CRM');
  doc.setCreator('Port CRM');

  // Flatten so the signer can't edit pre-filled values after the fact.
  form.flatten();

  return doc.save();
}

/**
 * Convenience: loads the source PDF from disk and returns the filled bytes.
 */
export async function generateEoiPdfFromTemplate(
  context: EoiContext,
  options?: { dimensionUnit?: 'ft' | 'm' },
): Promise<Uint8Array> {
  const bytes = await loadEoiTemplatePdf();
  return fillEoiFormFields(bytes, context, options);
}
