import { Injectable } from '@nestjs/common';
import { PDFDocument, PDFFont, PDFPage, rgb, RGB } from 'pdf-lib';
import { InvoiceBuilder } from './invoice.builder';
import { InvoiceRepository } from './invoice.repository';

export type InsurerInvoice = Awaited<
  ReturnType<InvoiceRepository['findInsurerInvoice']>
>[number];

export type Data = Record<string, string>;
export type Style = {
  size: number;
  spacing: number;
  separate: true;
  margin: number;
  isTwoDigit: boolean;
  masking: boolean;
  //* 켜면 그리기 직전에 "FAX: " 를 앞에 붙인다 (관리자 좌표 화면의 "FAX:" 문자추가).
  faxLabel: boolean;
  scale: number;
};

//* site(front/src/shared/helpers/invoice.helper.ts)·admin 구현과 같은 값을 쓴다.
export const FAX_LABEL_PREFIX = 'FAX: ';
@Injectable()
export class InvoiceDrawer {
  private font!: PDFFont;
  private color!: RGB;
  constructor(private invoiceBuilder: InvoiceBuilder) {
    this.color = rgb(0, 0, 0);
  }

  async draw(
    doc: PDFDocument,
    pages: PDFPage[],
    record: InsurerInvoice,
    data: Data,
  ) {
    const page = pages[record.coordinate.page - 1];
    const dimension = this.classifyField(record.invoice.nameEn);
    if (page) {
      switch (dimension) {
        case 'checkbox':
          this.drawCheckbox(page, record);
          break;
        case 'signature':
          await this.drawSignature(doc, page, record, data);
          break;
        case 'plaintext':
          this.drawPlainText(page, record, data);
          break;
      }
    }
  }

  /**
   * `coordinate.style` 은 DB 의 JSON 컬럼이라 `Style` 선언이 런타임 자료형을 보장하지 않는다.
   *
   * 관리자 좌표 화면이 `<input type="number">` 를 react-hook-form `register()` 에 그대로
   * 물려(`EditPdfData.tsx` size·spacing) `valueAsNumber` 없이 저장하므로, 그 화면을 거친
   * 행은 숫자 필드가 **문자열로** 들어 있다. pdf-lib 의 `drawText` 는
   * `assertOrUndefined(options.size, 'options.size', ['number'])` 로 숫자를 강제하므로
   * 문자열 `size` 는 TypeError 를 던지고, 호출부(`InvoiceFaxOverlayService`)가 그것을
   * 삼켜 회신처가 조용히 빠진 PDF 가 나간다 — 스테이징 실측으로 팩스 좌표가 등록된
   * 보험사 4곳 중 3곳(롯데손해보험·KB손해보험·메트라이프)이 이 상태였다.
   *
   * 그리기 직전에 한 번 정규화해 두면 draw 메서드가 늘어나도 같은 누락을 반복할 수 없다.
   * 변환식은 site(`front/src/shared/helpers/invoice.helper.ts`)와 **같은 값**을 내야 한다 —
   * 미리보기와 실제 발송본이 어긋나면 안 되기 때문이다. `scale` 만 `||` 인 것도 site 를
   * 따른 것이다(빈 문자열·0 을 기본값으로 되돌린다).
   */
  private normalizeStyle(style: Style | null | undefined): Style {
    return {
      ...(style ?? {}),
      size: Number(style?.size ?? 0),
      spacing: Number(style?.spacing ?? 0),
      margin: Number(style?.margin ?? 0),
      scale: Number(style?.scale || 0.2),
    } as Style;
  }

  private drawCheckbox(page: PDFPage, { coordinate }: InsurerInvoice) {
    const image = this.invoiceBuilder.getCheckImage();
    const { width, height } = image.scale(
      this.normalizeStyle(coordinate.style as Style).scale,
    );
    page.drawImage(image, {
      x: Number(coordinate.x),
      y: Number(coordinate.y),
      width,
      height,
    });
  }

  private async drawSignature(
    doc: PDFDocument,
    page: PDFPage,
    { coordinate, invoice }: InsurerInvoice,
    data: Data,
  ) {
    const signature = data[invoice.nameEn];
    if (signature) {
      const image = await doc.embedPng(signature);
      const { width, height } = image.scale(
        this.normalizeStyle(coordinate.style as Style).scale,
      );

      page.drawImage(image, {
        x: Number(coordinate.x),
        y: Number(coordinate.y),
        width,
        height,
      });
    }
  }

  private drawPlainText(page: PDFPage, record: InsurerInvoice, data: Data) {
    const font = this.invoiceBuilder.getFont();
    this.font = font;
    this.customize(page, record, data[record.invoice.nameEn]);
  }

  private customize(page: PDFPage, record: InsurerInvoice, text: string) {
    const { invoice, coordinate } = record;
    if (!text) {
      return;
    }

    //* 아래 draw 메서드들은 style 의 숫자 필드를 pdf-lib 에 그대로 넘긴다. 여기서 한 번
    //* 정규화해 두는 것이 그 전제를 만드는 유일한 지점이다 — normalizeStyle 주석 참조.
    const style = this.normalizeStyle(coordinate.style as Style);

    //* 접두사는 타입 분기보다 먼저 붙는다 — 어떤 그리기 방식이든 동일하게 적용된다.
    //* site·admin 구현과 같은 값을 써야 미리보기와 실제 발송본이 어긋나지 않는다.
    if (style?.faxLabel) {
      text = `${FAX_LABEL_PREFIX}${text}`;
    }

    switch (invoice.type) {
      case 'IVC003': // 연락처
        this.drawContact(
          page,
          text,
          style,
          Number(coordinate.x),
          Number(coordinate.y),
        );
        break;
      case 'IVC009': // 주민등록번호
        this.drawIdNumber(
          page,
          text,
          style,
          Number(coordinate.x),
          Number(coordinate.y),
        );
        break;
      case 'IVC005':
        this.drawYear(
          page,
          text,
          style,
          Number(coordinate.x),
          Number(coordinate.y),
        );
        break;
      default:
        this.drawGeneralText(
          page,
          text,
          style,
          Number(coordinate.x),
          Number(coordinate.y),
        );
        return;
    }
  }

  private drawYear(
    page: PDFPage,
    text: string,
    style: Style,
    x: number,
    y: number,
  ) {
    const _text = style.isTwoDigit ? text.slice(2) : text;
    _text.split('').map((char, i) => {
      page.drawText(char, {
        x: this.spacing(x, i, style.spacing),
        y: y,
        size: style.size,
        font: this.font,
        color: this.color,
      });
    });
  }

  private drawIdNumber(
    page: PDFPage,
    text: string,
    style: Style,
    x: number,
    y: number,
  ) {
    const front = text.slice(0, 6);
    const back = style.masking ? text.slice(6, 7) : text.slice(6);
    const xFront = x;
    const xBack = xFront + front.length * style.spacing + style.margin;

    front.split('').map((char, i) => {
      page.drawText(char, {
        x: this.spacing(xFront, i, style.spacing),
        y: y,
        size: style.size,
        font: this.font,
        color: this.color,
      });
    });

    back.split('').map((char, i) => {
      page.drawText(char, {
        x: this.spacing(xBack, i, style.spacing),
        y: y,
        size: style.size,
        font: this.font,
        color: this.color,
      });
    });
  }

  private drawGeneralText(
    page: PDFPage,
    text: string,
    style: Style,
    x: number,
    y: number,
  ) {
    text.split('').map((char, i) => {
      page.drawText(char, {
        x: this.spacing(x, i, style.spacing),
        y: y,
        size: style.size,
        font: this.font,
        color: this.color,
      });
    });
  }

  private drawContact(
    page: PDFPage,
    text: string,
    style: Style,
    x: number,
    y: number,
  ) {
    const part1 = text.slice(0, 3);
    const part2 = text.slice(3, 7);
    const part3 = text.slice(7);

    const xPart1 = x;
    const xPart2 = xPart1 + part1.length * style.spacing + style.margin;
    const xPart3 = xPart2 + part2.length * style.spacing + style.margin;

    [part1, part2, part3].map((part, i) => {
      part.split('').map((char, j) => {
        page.drawText(char, {
          x: this.spacing([xPart1, xPart2, xPart3][i], j, style.spacing),
          y: y,
          size: style.size,
          font: this.font,
          color: this.color,
        });
      });
    });
  }

  private spacing(origin: number, index: number, weight: number) {
    return origin + index * weight;
  }

  private classifyField(field: string) {
    const lowercase = field.toLowerCase();
    if (lowercase.includes('checkbox')) {
      return 'checkbox';
    } else if (lowercase.includes('signature')) {
      return 'signature';
    } else {
      return 'plaintext';
    }
  }
}
