import { S3Service } from '@lib/s3/src';
import { ForbiddenException, Injectable, Logger } from '@nestjs/common';
import axios from 'axios';
import { PDFDocument } from 'pdf-lib';
import { InvoiceFaxOverlayService } from './invoice-fax-overlay.service';
import { InvoiceStorageService } from './invoice-storage.service';
import { InvoiceBuilder } from './invoice.builder';
import { InvoiceDrawer } from './invoice.drawer';
import {
  CreateHistoryDto,
  GenerateInvoiceDto,
  GetHistoriesDto,
} from './invoice.dto';
import { InvoiceRepository } from './invoice.repository';

@Injectable()
export class InvoiceService {
  private readonly logger = new Logger(InvoiceService.name);
  private readonly vfaxBaseUrl = process.env.VFAX_BASE_URL || 'https://api.vfax.kr/vFax/v1';
  private readonly vfaxToken = process.env.VFAX_TOKEN || '';
  private readonly cdnBaseUrl = process.env.CDN_BASE_URL || '';

  constructor(
    private invoiceStorageService: InvoiceStorageService,
    private invoiceBuilder: InvoiceBuilder,
    private invoiceRepository: InvoiceRepository,
    private s3Service: S3Service,
    private invoiceFaxOverlayService: InvoiceFaxOverlayService,
  ) {}

  async getInvoiceByInsurer(insCode: string) {
    return await this.invoiceRepository.findInsurerInvoice(insCode);
  }

  async generate({ insurerCode, data }: GenerateInvoiceDto) {
    const pdf = await this.invoiceStorageService.fetch(insurerCode);
    const builder = new InvoiceBuilder(
      this.invoiceBuilder.getPdfService(),
      this.invoiceBuilder.getInvoiceStorageService(),
    );

    const doc = await builder.build(pdf);

    const pages = doc.getPages();
    const records =
      await this.invoiceRepository.findInsurerInvoice(insurerCode);
    const drawer = new InvoiceDrawer(builder);

    await Promise.all(
      records.map(async (record) => {
        await drawer.draw(doc, pages, record, data);
      }),
    );

    return {
      doc: await doc.save(),
      filename: records[0].insurer.insName + '_청구서.pdf',
    };
  }

  async getAllInsurer() {
    return await this.invoiceRepository.findAllInsurer();
  }

  /**
   * 청구서 이력 소유자 검증. ivhId만 바꿔 남의 청구서(피보험자명·연락처·진료내역)를
   * 조회/다운로드할 수 있어 추가한다. 토큰이 없으면 viewerMemId가 -1이라 항상 차단된다.
   */
  private async assertHistoryOwner(
    ivhId: number,
    viewerMemId: number | null,
  ) {
    //* null = 관리자 경로(이미 관리자 가드로 인증됨) → 소유자 제한 없음.
    if (viewerMemId === null) return;

    const ownerMemId =
      await this.invoiceRepository.findHistoryOwnerMemId(ivhId);

    if (ownerMemId === null || Number(ownerMemId) !== Number(viewerMemId)) {
      throw new ForbiddenException('접근 권한이 없습니다');
    }
  }

  async getHistories(dto: GetHistoriesDto) {
    const histories = await this.invoiceRepository.findHistories(dto);

    // 각 히스토리의 팩스 상태를 vFax API로 실시간 조회
    const historiesWithRealTimeStatus = await Promise.all(
      histories.map(async (history) => {
        if (!history.faxHistories || history.faxHistories.length === 0) {
          return history;
        }

        // 각 팩스 히스토리의 실시간 상태 조회
        const updatedFaxHistories = await Promise.all(
          history.faxHistories.map(async (fax) => {
            if (!fax.faxCode) {
              return fax;
            }

            try {
              // vFax API로 실시간 상태 조회
              const response = await axios.get(
                `${this.vfaxBaseUrl}/faxStatus/${fax.faxCode}`,
                {
                  headers: {
                    Authorization: `Bearer ${this.vfaxToken}`,
                  },
                  timeout: 3000, // 3초 타임아웃
                },
              );

              const vfaxStatus = response.data.sendStatus;

              return {
                ...fax,
                vfaxStatus, // vFax API에서 받은 실시간 상태 (0,1,2,3,5,6)
              };
            } catch (error: any) {
              // vFax API 조회 실패 시 DB 상태 그대로 사용
              return fax;
            }
          }),
        );

        return {
          ...history,
          faxHistories: updatedFaxHistories,
        };
      }),
    );

    return historiesWithRealTimeStatus;
  }

  async getHistory(ivhId: number, viewerMemId: number) {
    await this.assertHistoryOwner(ivhId, viewerMemId);
    return await this.invoiceRepository.findHistory(ivhId);
  }

  async getHistoryPdf(ivhId: number, viewerMemId: number | null) {
    await this.assertHistoryOwner(ivhId, viewerMemId);
    const history = await this.invoiceRepository.findHistory(ivhId);

    if (!history) {
      throw new Error('청구서 발급 내역을 찾을 수 없습니다.');
    }

    // th_invoice 테이블에서 s3Key 조회
    const invoice = await this.invoiceRepository.findInvoiceById(ivhId);

    if (!invoice?.ivhS3Key) {
      throw new Error('저장된 청구서 파일이 없습니다.');
    }

    // ivhS3Key가 JSON 배열인지 확인
    let invoiceFiles: Array<{ s3Key: string; originalName: string }> = [];

    if (typeof invoice.ivhS3Key === 'string') {
      // 문자열인 경우: JSON 문자열인지 콤마로 구분된 문자열인지 확인
      if (invoice.ivhS3Key.startsWith('[')) {
        // JSON 배열 문자열
        try {
          invoiceFiles = JSON.parse(invoice.ivhS3Key);
        } catch (error) {
          throw new Error('청구서 파일 데이터 파싱에 실패했습니다.');
        }
      } else {
        // 기존 데이터: 콤마로 구분된 문자열 -> 확장자만 추출
        const s3Keys = invoice.ivhS3Key.split(',').map(key => key.trim());
        invoiceFiles = s3Keys.map((s3Key, index) => {
          const ext = s3Key.split('.').pop() || 'pdf';
          return {
            s3Key,
            originalName: `청구서_${index + 1}.${ext}`,
          };
        });
      }
    } else if (Array.isArray(invoice.ivhS3Key)) {
      // 새 데이터: JSON 배열 (Drizzle이 자동 파싱)
      invoiceFiles = invoice.ivhS3Key as Array<{ s3Key: string; originalName: string }>;
    } else {
      throw new Error('청구서 파일 형식이 올바르지 않습니다.');
    }

    // 보험사 목록(ivhInsurers) 순서에 맞춰 청구서 PDF 묶음을 재정렬.
    // Why: 클라이언트는 보험사 배열과 PDF 배열을 같은 인덱스로 짝지어 발송 요청을 만드는데
    // (FaxSendPage.tsx), 저장된 PDF 순서(ivhS3Key)가 보험사 목록 순서(ivhInsurers)와
    // 다를 경우 잘못된 보험사로 청구서가 발송된다 (2026-05 사고).
    const insurers = (invoice.ivhInsurers as Array<{ insName: string }> | null) || [];
    if (insurers.length > 0 && invoiceFiles.length > 0) {
      const extractInsurerName = (originalName: string): string =>
        (originalName || '').replace(/_청구서\.(pdf|PDF)$/u, '').replace(/\.(pdf|PDF)$/u, '').trim();
      const byInsurer = new Map<string, Array<{ s3Key: string; originalName: string }>>();
      for (const f of invoiceFiles) {
        const key = extractInsurerName(f.originalName);
        if (!byInsurer.has(key)) byInsurer.set(key, []);
        byInsurer.get(key)!.push(f);
      }
      const ordered: Array<{ s3Key: string; originalName: string }> = [];
      const consumed = new Set<{ s3Key: string; originalName: string }>();
      for (const ins of insurers) {
        const bucket = byInsurer.get(ins.insName);
        if (bucket && bucket.length > 0) {
          const f = bucket.shift()!;
          ordered.push(f);
          consumed.add(f);
        }
      }
      // 매핑되지 않은 PDF(보험사명이 목록에 없거나 파싱 실패)는 원래 순서대로 뒤에 추가
      for (const f of invoiceFiles) {
        if (!consumed.has(f)) ordered.push(f);
      }
      invoiceFiles = ordered;
    }

    //* 회신처를 보험사별로 입힌다. 저장본(S3)은 건드리지 않고 읽는 시점에만 덮어쓴다 —
    //* 아카이브는 원본 그대로 두고, 미리보기·다운로드·병합이 이 한 곳을 지나므로
    //* (getMergedPdf 가 이 메서드를 호출한다) 세 경로가 함께 해결된다.
    //*
    //* **발송 이력을 조회하지 않는다(M2).** 무엇을 찍을지는 보험사 마스터의 팩스타입이
    //* 정하므로 팩스를 아직 보내지 않은 청구서에도 회신처가 보인다 — 자동 팩스가 실패해
    //* 청구인이 직접 보내야 할 때가 이 값이 가장 필요한 순간이다.
    const insurerNameOf = (originalName: string): string =>
      (originalName || '')
        .replace(/_청구서\.(pdf|PDF)$/u, '')
        .replace(/\.(pdf|PDF)$/u, '')
        .trim();

    // CDN에서 여러 PDF 다운로드 (보험사별)
    const pdfBuffers: Buffer[] = [];
    const pdfNames: string[] = [];
    for (const file of invoiceFiles) {
      try {
        const cdnUrl = `${this.cdnBaseUrl}${file.s3Key}`;
        const response = await axios.get(cdnUrl, { responseType: 'arraybuffer' });
        const pdfBuffer = Buffer.from(response.data);
        //* 오버레이는 실패해도 원본 버퍼를 그대로 돌려주므로 다운로드를 막지 않는다.
        const insurerName = insurerNameOf(file.originalName);
        const withFax = await this.invoiceFaxOverlayService.overlayReceiverFax(
          pdfBuffer,
          insurerName,
        );
        pdfBuffers.push(withFax);
        pdfNames.push(file.originalName);
      } catch (error: any) {
        // PDF 다운로드 실패 시 계속 진행
      }
    }

    // 첨부파일들도 다운로드 (ivh_attachment_s3_keys)
    const attachmentBuffers: Buffer[] = [];
    const attachmentNames: string[] = [];
    if (invoice.ivhAttachmentS3Keys) {
      let attachmentFiles: Array<{ s3Key: string; originalName: string }> = [];

      if (typeof invoice.ivhAttachmentS3Keys === 'string') {
        // 문자열인 경우: JSON 문자열인지 확인
        if (invoice.ivhAttachmentS3Keys.startsWith('[')) {
          try {
            attachmentFiles = JSON.parse(invoice.ivhAttachmentS3Keys);
          } catch (error) {
            // 첨부파일 JSON 파싱 실패
          }
        }
      } else if (Array.isArray(invoice.ivhAttachmentS3Keys)) {
        const firstItem = invoice.ivhAttachmentS3Keys[0];

        if (typeof firstItem === 'string') {
          // 기존 데이터: 문자열 배열 -> 확장자만 추출
          attachmentFiles = (invoice.ivhAttachmentS3Keys as string[]).map((s3Key, index) => {
            const ext = s3Key.split('.').pop() || '';
            return {
              s3Key,
              originalName: `첨부파일_${index + 1}.${ext}`,
            };
          });
        } else {
          // 새 데이터: 객체 배열
          attachmentFiles = invoice.ivhAttachmentS3Keys as Array<{ s3Key: string; originalName: string }>;
        }
      }

      for (const file of attachmentFiles) {
        try {
          const cdnUrl = `${this.cdnBaseUrl}${file.s3Key}`;
          const response = await axios.get(cdnUrl, { responseType: 'arraybuffer' });
          const attachmentBuffer = Buffer.from(response.data);
          attachmentBuffers.push(attachmentBuffer);
          attachmentNames.push(file.originalName);
        } catch (error: any) {
          // 첨부파일 다운로드 실패
        }
      }
    }

    return {
      pdfBuffers,
      pdfNames,
      attachmentBuffers,
      attachmentNames,
      filename: '청구서.pdf',
    };
  }

  /**
   * 특정 보험사의 청구서 + 첨부파일을 병합한 PDF 반환
   * @param ivhId 청구서 발급 내역 ID
   * @param insurerName 보험사명 (옵션, 없으면 첫번째 청구서)
   */
  async getMergedPdf(
    ivhId: number,
    viewerMemId: number | null,
    insurerName?: string,
  ) {
    //* 소유자 검증은 getHistoryPdf 내부에서 수행된다.
    const { pdfBuffers, pdfNames, attachmentBuffers, attachmentNames } = await this.getHistoryPdf(ivhId, viewerMemId);

    if (pdfBuffers.length === 0) {
      throw new Error('청구서 PDF를 찾을 수 없습니다.');
    }

    // 병합할 청구서 PDF 선택 (보험사명으로 찾거나 첫번째)
    let invoicePdfBuffer: Buffer;
    let invoiceName: string;

    if (insurerName) {
      const index = pdfNames.findIndex(name => name.includes(insurerName));
      if (index === -1) {
        throw new Error(`보험사 "${insurerName}"의 청구서를 찾을 수 없습니다.`);
      }
      invoicePdfBuffer = pdfBuffers[index];
      invoiceName = pdfNames[index];
    } else {
      invoicePdfBuffer = pdfBuffers[0];
      invoiceName = pdfNames[0];
    }

    // PDF 병합 시작
    const mergedPdf = await PDFDocument.create();

    // 1. 청구서 PDF 추가
    const invoicePdf = await PDFDocument.load(invoicePdfBuffer);
    const invoicePages = await mergedPdf.copyPages(invoicePdf, invoicePdf.getPageIndices());
    invoicePages.forEach(page => mergedPdf.addPage(page));

    // 2. 첨부파일 추가 (PDF와 이미지)
    for (let i = 0; i < attachmentBuffers.length; i++) {
      const attachmentBuffer = attachmentBuffers[i];
      const attachmentName = attachmentNames[i] || `첨부파일_${i + 1}`;

      try {
        // PDF 파일인지 확인 (PDF 시그니처 확인)
        const isPdf = attachmentBuffer.length > 4 &&
          attachmentBuffer[0] === 0x25 &&
          attachmentBuffer[1] === 0x50 &&
          attachmentBuffer[2] === 0x44 &&
          attachmentBuffer[3] === 0x46;

        if (isPdf) {
          const attachmentPdf = await PDFDocument.load(attachmentBuffer);
          const attachmentPages = await mergedPdf.copyPages(attachmentPdf, attachmentPdf.getPageIndices());
          attachmentPages.forEach(page => mergedPdf.addPage(page));
        } else {
          // 이미지 파일을 PDF 페이지로 변환하여 추가

          // PNG, JPEG 이미지를 PDF에 임베드
          let image;
          if (attachmentBuffer[0] === 0x89 && attachmentBuffer[1] === 0x50) {
            // PNG 파일
            image = await mergedPdf.embedPng(attachmentBuffer);
          } else if (attachmentBuffer[0] === 0xFF && attachmentBuffer[1] === 0xD8) {
            // JPEG 파일
            image = await mergedPdf.embedJpg(attachmentBuffer);
          } else {
            // 지원하지 않는 이미지 형식
            continue;
          }

          // 이미지를 페이지에 추가
          const page = mergedPdf.addPage();
          const { width, height } = image.scale(1);

          // 페이지 크기를 이미지에 맞추거나, A4 크기로 조정
          const maxWidth = 595; // A4 width in points
          const maxHeight = 842; // A4 height in points

          let scale = 1;
          if (width > maxWidth || height > maxHeight) {
            scale = Math.min(maxWidth / width, maxHeight / height);
          }

          page.setSize(width * scale, height * scale);
          page.drawImage(image, {
            x: 0,
            y: 0,
            width: width * scale,
            height: height * scale,
          });
        }
      } catch (error) {
        // 첨부파일 병합 실패
      }
    }

    // 병합된 PDF를 Buffer로 변환
    const mergedPdfBytes = await mergedPdf.save();
    const mergedPdfBuffer = Buffer.from(mergedPdfBytes);

    return {
      pdfBuffer: mergedPdfBuffer,
      filename: `${invoiceName}_통합본.pdf`,
    };
  }

  async createHistory(dto: CreateHistoryDto, pdfBuffers?: Buffer[], insurerNames?: string[]) {
    const invoiceFiles: Array<{ s3Key: string; originalName: string }> = [];

    // 먼저 히스토리 생성하여 ivhId 획득
    const result = await this.invoiceRepository.insertHistory({ ...dto });
    const ivhId = result.ivhId;

    // 여러 PDF 버퍼를 S3에 업로드 (보험사별)
    if (pdfBuffers && pdfBuffers.length > 0) {
      for (let i = 0; i < pdfBuffers.length; i++) {
        try {
          const originalName = insurerNames && insurerNames[i]
            ? `${insurerNames[i]}_청구서.pdf`
            : `청구서_${i + 1}.pdf`;

          const encryptedFileName = this.s3Service.encFileName(originalName);
          const s3Key = await this.s3Service.imageUpload(
            encryptedFileName,
            pdfBuffers[i],
            'invoice/files/',
          );

          invoiceFiles.push({
            s3Key,
            originalName,
          });
        } catch (error: any) {
          this.logger.error(`[청구서 S3 업로드 실패] ivhId=${ivhId}, index=${i}, error=${error.message}`, error.stack);
        }
      }
    }

    // S3 키 업데이트 (청구서 PDF 배열로 저장)
    if (invoiceFiles.length > 0) {
      await this.invoiceRepository.updateHistoryS3Keys(ivhId, invoiceFiles);
    }

    return { ivhId };
  }

  async uploadAttachments(
    ivhId: number,
    files: Express.Multer.File[],
    fileNames?: string[],
  ) {
    if (!files || files.length === 0) {
      return { message: '업로드할 파일이 없습니다.', attachments: [] };
    }

    const attachments: Array<{ s3Key: string; originalName: string }> = [];

    try {
      for (let i = 0; i < files.length; i++) {
        const file = files[i];
        // 프론트에서 전달한 원본 파일명 사용 (없으면 file.originalname 사용)
        const originalName = fileNames && fileNames[i] ? fileNames[i] : file.originalname;

        const encryptedFileName = this.s3Service.encFileName(originalName);
        const s3Key = await this.s3Service.imageUpload(
          encryptedFileName,
          file.buffer,
          'invoice/files/',
        );

        attachments.push({
          s3Key,
          originalName: originalName,
        });
      }

      // DB 업데이트
      await this.invoiceRepository.updateHistoryS3Keys(ivhId, undefined, attachments);

      return {
        message: `${attachments.length}개 파일 업로드 성공`,
        attachments,
      };
    } catch (error) {
      throw error;
    }
  }
}
