import { CODEF_REQUEST_FAILED } from '@api/constants/api.constant';
import { WebClientService } from '@lib/webclient/src';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { plainToClass } from 'class-transformer';
import { CodefAuthService } from './codef-auth.service';
import {
  CertifyResponseDto,
  MedicineSummaryResponseDto,
  TreatmentMultipleResponseDto,
  TreatmentResponseDto,
} from './codef.dto';
import { CodefException } from './codef.exception';
import {
  TreatmentMultipleItemDto,
  treatmentInfoDto,
} from './codef.interface';
import { TreatRepository } from '@api/modules/treat/treat.repository';
//* 멀티 디스패처 공급자 계약. codef↔coocon 순환참조 방지 위해 type-only import(런타임 제거).
import type { TreatScraper } from '@lib/coocon/src/coocon.interface';

@Injectable()
export class CodefService implements TreatScraper {
  private logger = new Logger(CodefService.name);
  constructor(
    private configService: ConfigService,
    private webClientService: WebClientService,
    private codefTokenService: CodefAuthService,
    private treatRepository: TreatRepository,
  ) {}

  /**
   * 심평원 다건요청 - 1차 인증요청
   * 기본·자동차(확장: 약) 여러 상품을 단일 인증으로 처리하기 위해 배열 1건으로 요청한다.
   * is2Way 미포함(1차) → 공유 twoWayInfo 1세트 반환 → CertifyResponseDto 형태로 정규화하여 반환
   * (외부 규격 미확정 대비: 응답 data가 배열/단건 어느 형태로 와도 공유 twoWayInfo 1세트를 추출)
   */
  async fetchCertifyMultiple(
    items: TreatmentMultipleItemDto[],
  ): Promise<CertifyResponseDto> {
    const token = await this.codefTokenService.genToken();
    const url = this.configService.get('codef.treatMultiUrl');
    //* codef 는 단건(객체)만 받음 — 배열 다건 미지원(CF-09999). 진료(items[0])만 단건 전송.
    const data = await this.webClientService
      .post<CertifyResponseDto>(url, items[0], {
        headers: { Authorization: 'Bearer ' + token },
      })
      .catch((e) => {
        this.logger.error(e);
        throw new CodefException(CODEF_REQUEST_FAILED);
      });
    const decoded = this.decode(data);
    //* 다건 1차 응답: 한 번의 인증으로 여러 상품을 처리하므로 공유 twoWayInfo 1세트가 반환되는 것이 정상이다.
    //* 다만 외부 규격이 항목별 twoWayInfo를 반환할 가능성에 대비해, 배열로 올 경우 항목 간 twoWayInfo 차이를 검사한다.
    //* (항목별로 다르면 공유 가정이 깨져 자동차(index 1) 2차 추가인증이 기본 twoWayInfo로 발사되어 실패할 수 있음)
    let sharedData = decoded.data;
    if (Array.isArray(decoded.data)) {
      const list = decoded.data.filter(Boolean);
      const base = list[0];
      const diverged = list.some(
        (item: any) =>
          item?.jti !== base?.jti ||
          item?.jobIndex !== base?.jobIndex ||
          item?.threadIndex !== base?.threadIndex ||
          item?.twoWayTimestamp !== base?.twoWayTimestamp,
      );
      if (diverged) {
        //* 공유 twoWayInfo 가정이 유효하지 않음 → 자동차 추가인증 불일치 위험을 운영 로그로 노출
        this.logger.warn(
          '다건 1차 인증 응답의 항목별 twoWayInfo가 상이합니다. 공유 twoWayInfo 가정이 깨져 자동차 추가인증이 실패할 수 있습니다. (codef 다건 규격 확인 필요)',
        );
      }
      //* 단일 공유 인증세션 설계상 index 0의 twoWayInfo를 대표값으로 사용
      sharedData = base;
    }
    const shared = {
      result: decoded.result,
      data: sharedData,
    };
    return plainToClass(CertifyResponseDto, shared);
  }

  /**
   * 심평원 다건요청 - 2차 자료수집
   * 1차에서 받은 공유 twoWayInfo를 전 배열요소에 동일 적용(is2Way=true)하여 배열 1건으로 요청한다.
   * 응답 data는 요청 배열과 동일 인덱스(0=기본, 1=자동차)로 정렬되며 인덱스별로 saveTest 적재
   */
  async fetchTreatmentMultiple(
    items: TreatmentMultipleItemDto[],
    oddId: number,
  ): Promise<TreatmentMultipleResponseDto> {
    const token = await this.codefTokenService.genToken();
    const url = this.configService.get('codef.treatMultiUrl');
    //* codef 는 단건(객체)만 받음 — 진료(items[0])만 단건 전송. 응답은 아래에서 [item] 배열로 정규화.
    const data = await this.webClientService
      .post<TreatmentMultipleResponseDto>(url, items[0], {
        headers: { Authorization: 'Bearer ' + token },
        timeout: 300000,
        maxRedirects: 1,
      })
      .catch((e) => {
        this.logger.error(e);
        throw new CodefException(CODEF_REQUEST_FAILED);
      });
    const decoded = this.decode(data);
    //* 응답 data를 배열로 정규화 (단건 객체로 올 경우 배열로 감싸 인덱스 정합 유지)
    const list: any[] = Array.isArray(decoded.data)
      ? decoded.data
      : decoded.data
        ? [decoded.data]
        : [];
    //* 인덱스별 원본 적재 (0=기본 BASIC, 1=자동차 CAR) - 기존 단건 saveTest 의미 보존
    list.forEach((item, index) => {
      const codefType = index === 0 ? 'BASIC' : 'CAR';
      this.treatRepository.saveTest(
        oddId,
        JSON.stringify({ result: decoded.result, data: item }),
        codefType,
      );
    });
    return plainToClass(TreatmentMultipleResponseDto, {
      result: decoded.result,
      data: list,
    });
  }

  /** @deprecated 다건요청(fetchCertifyMultiple)으로 대체됨. 롤백용으로 잔존 */
  async fetchCertify(dto: treatmentInfoDto): Promise<CertifyResponseDto> {
    const token = await this.codefTokenService.genToken();
    const url = this.configService.get('codef.treatUrl');
    const data = await this.webClientService
      .post<CertifyResponseDto>(url, dto, {
        headers: { Authorization: 'Bearer ' + token },
      })
      .catch((e) => {
        this.logger.error(e);
        throw new CodefException(CODEF_REQUEST_FAILED);
      });
    const decoded = this.decode(data);
    return plainToClass(CertifyResponseDto, decoded);
  }

  /** @deprecated 다건요청(fetchTreatmentMultiple)으로 대체됨. 롤백용으로 잔존 */
  async fetchTreatmentInfo(
    input: treatmentInfoDto,
    oddId: number,
  ): Promise<TreatmentResponseDto> {
    const token = await this.codefTokenService.genToken();
    const url = this.configService.get('codef.treatUrl');
    const data = await this.webClientService
      .post<TreatmentResponseDto>(url, input, {
        headers: { Authorization: 'Bearer ' + token },
        timeout: 300000,
        maxRedirects: 1,
      })
      .catch((e) => {
        this.logger.error(e);
        throw new CodefException(CODEF_REQUEST_FAILED);
      });
    const decoded = this.decode(data);

    this.treatRepository.saveTest(oddId, JSON.stringify(decoded), 'BASIC');
    return plainToClass(TreatmentResponseDto, decoded);
  }

  /** @deprecated 다건요청(fetchTreatmentMultiple)으로 대체됨. 롤백용으로 잔존 */
  async fetchTreatmentCarInfo(
    input: treatmentInfoDto,
    oddId: number,
  ): Promise<TreatmentResponseDto> {
    const token = await this.codefTokenService.genToken();
    const url = this.configService.get('codef.treatCarUrl');
    const data = await this.webClientService
      .post<TreatmentResponseDto>(url, input, {
        headers: { Authorization: 'Bearer ' + token },
        timeout: 300000,
        maxRedirects: 1,
      })
      .catch((e) => {
        this.logger.error(e);
        throw new CodefException(CODEF_REQUEST_FAILED);
      });
    const decoded = this.decode(data);

    this.treatRepository.saveTest(oddId, JSON.stringify(decoded), 'CAR');
    return plainToClass(TreatmentResponseDto, decoded);
  }

  //* 심평원 '내가 먹는약 한눈에' API 호출
  //* drugUrl 미설정 시 즉시 빈 응답을 반환하여 기존 플로우 영향 없도록 처리
  async fetchMedicineSummary(
    input: treatmentInfoDto,
    oddId: number,
  ): Promise<MedicineSummaryResponseDto> {
    const url = this.configService.get('codef.drugUrl');
    if (!url) {
      this.logger.warn(
        'codef.drugUrl 미설정 — 내가 먹는약 한눈에 API 호출을 건너뜁니다.',
      );
      return plainToClass(MedicineSummaryResponseDto, {
        result: {
          code: 'CF-00000',
          extraMessage: '',
          message: 'drugUrl 미설정으로 호출 생략',
          transactionId: '',
        },
        data: { resMedicineSummaryList: [] },
      });
    }

    const token = await this.codefTokenService.genToken();
    const data = await this.webClientService
      .post<MedicineSummaryResponseDto>(url, input, {
        headers: { Authorization: 'Bearer ' + token },
        timeout: 300000,
        maxRedirects: 1,
      })
      .catch((e) => {
        this.logger.error(e);
        throw new CodefException(CODEF_REQUEST_FAILED);
      });
    const decoded = this.decode(data);

    //* saveTest는 의도적 fire-and-forget이나, unhandled rejection 방지를 위해 catch로 로깅 fallback
    this.treatRepository
      .saveTest(oddId, JSON.stringify(decoded), 'DRUG')
      .catch((e) => {
        this.logger.error(
          `saveTest 실패 (DRUG): oddId=${oddId}, error=${e?.message || e}`,
        );
      });
    //* 코드에프 응답(조제건 객체/배열 + resDrugList 중첩)을 내부 평면 계약(resMedicineSummaryList)으로 변환해 반환.
    return plainToClass(MedicineSummaryResponseDto, {
      result: decoded?.result,
      data: {
        resMedicineSummaryList: this.flattenMedicineSummary(decoded?.data),
      },
    });
  }

  //* 심평원 '내가 먹는약 한눈에' 응답 → 내부 평면 리스트 변환.
  //* - data: 단건이면 객체, 다건이면 배열 (조제건 단위)
  //* - 약품은 각 조제건의 resDrugList 에 중첩 → 약품 1건당 1행, 조제건 정보(조제일/처방기관/조제기관) 부여
  //* - 약품이 없는 조제건도 방문이력 보존 위해 1행 생성(약품 필드 null)
  //* - 코드에프 응답 필드명을 내부 계약 필드명으로 매핑
  private flattenMedicineSummary(data: any): any[] {
    if (!data) return [];
    const records = Array.isArray(data) ? data : [data];
    const rows: any[] = [];
    for (const rec of records) {
      if (!rec || typeof rec !== 'object') continue;
      const drugs =
        Array.isArray(rec.resDrugList) && rec.resDrugList.length > 0
          ? rec.resDrugList
          : [null];
      for (const drug of drugs) {
        rows.push({
          resDispenseDate: rec.resManufactureDate, // 조제일자(YYYYMMDD)
          resPrescribingHospital: rec.resPrescribeOrg, // 처방기관
          resDispensingPharmacy: rec.commBrandName, // 조제기관(약국/제조사명)
          resPrescribingHospitalCode: null, // 코드에프 미제공
          resDispensingPharmacyCode: null, // 코드에프 미제공
          resDrugName: drug?.resDrugName ?? null, // 약품명
          resTotalDosingDays: drug?.resTotalDosingdays ?? null, // 총 투약일수(코드에프 소문자 days)
        });
      }
    }
    return rows;
  }

  private decode(data: any) {
    return JSON.parse(decodeURIComponent(data));
  }
}
