import { ANALYSIS_FAILED } from '@api/constants/api.constant';
import { hashWithSHA256 } from '@api/helpers/format.helper';
import { CodefService } from '@lib/codef/src';
import { CodefException } from '@lib/codef/src/codef.exception';
import { TreatmentMultipleItemDto } from '@lib/codef/src/codef.interface';
import { CooconService } from '@lib/coocon/src';
import { TreatScraper } from '@lib/coocon/src/coocon.interface';
import { OctoverService } from '@lib/octover/src';
import {
  TreatDispatcher,
  providerFromTag,
} from './dispatch/treat-dispatcher.service';
import { resolveTreatError, TreatErrorCategory, TreatVendor } from './error';
import { ConfigService } from '@nestjs/config';
import {
  CodefResult,
  Treatment,
  TreatmentResponseDto,
} from '@lib/codef/src/codef.dto';
import { mysqlErrorWrapper } from '@lib/drizzle/src/drizzle.utill';
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import dayjs from 'dayjs';
import { AlarmType } from '../alarm/alarm.entity';
import { AlarmService } from '../alarm/alarm.service';
import { AdminAlarmService } from '../alarm/alarm-admin.service';
import {
  AdminAlarmResultType,
  AdminAlarmType,
} from '../alarm/alram-admin.entity';
import { OrderDetailState } from '../order/order-detail/order-detail.entity';
import { OrderDetailService } from '../order/order-detail/order-detail.service';
import { OrderService } from '../order/order/order.service';
import { CodefRequestType } from './log/log-treat.entity';
import { LogTreatService } from './log/log-treat.service';
import { CodefTreatResultDto, TreatCompareDto } from './treat.dto';
import { TreatRepository } from './treat.repository';
import { TreatService } from './treat.service';
import { AnalyzeRepository } from '../analyze/analyze.repository';
import { AlimtalkService } from '../alimtalk/alimtalk.service';
import { LogB2cTreatService } from './log-b2c/log-b2c-treat.service';
import { CodefRequestType as B2cCodefRequestType } from './log-b2c/log-b2c-treat.entity';
import { TreatCarCanaryService } from './treat-car-canary.service';

@Injectable()
export class TreatEventHandler {
  private logger = new Logger(TreatService.name);
  constructor(
    private codefService: CodefService,
    private cooconService: CooconService,
    private octoverService: OctoverService,
    private treatDispatcher: TreatDispatcher,
    private configService: ConfigService,
    private alarmService: AlarmService,
    private orderDetailService: OrderDetailService,
    private treatRepository: TreatRepository,
    private orderService: OrderService,
    private logTreatService: LogTreatService,
    private analyzeRepository: AnalyzeRepository,
    private alimtalkService: AlimtalkService,
    private logB2cTreatService: LogB2cTreatService,
    private treatService: TreatService,
    private adminAlarmService: AdminAlarmService,
    private treatCarCanaryService: TreatCarCanaryService,
  ) {}

  //* 진료내역 스크래핑 공급자 스위치 (configuration.treatProvider: 'codef'(기본)|'coocon')
  private get treatScraper(): TreatScraper {
    const p = this.configService.get('treatProvider');
    if (p === 'multi') return this.treatDispatcher;
    if (p === 'coocon') return this.cooconService;
    if (p === 'octover') return this.octoverService;
    return this.codefService;
  }

  //* 자동차보험 진료 수집 ON/OFF는 TreatProviderService.isCarEnabled()로 일원화
  //*   (DB tn_treat_setting 우선, env enableTreatCar 폴백). 운영 중 어드민 토글로 즉시 차단/복구.

  /**
   * 관리자 재분석 이벤트 (비동기 처리).
   * 자료수집은 건너뛰고 compare(분석)만 실행. 끝나면 요청한 관리자에게 알람 push.
   */
  @OnEvent('reAnalyze', { async: true })
  async handleReAnalyze(dto: {
    oddId: number;
    admId: number;
    chargeQuantity: boolean;
    transactionId?: string;
    memLoginId?: string;
  }) {
    const target = [
      dto.memLoginId ? `회원: ${dto.memLoginId}` : '',
      dto.transactionId ? `분석번호: ${dto.transactionId}` : '',
    ]
      .filter(Boolean)
      .join(' / ');
    const charge = dto.chargeQuantity ? '차감' : '미차감';

    try {
      await this.treatService.compare({ oddId: dto.oddId });
      await this.adminAlarmService.addAlarm({
        admId: dto.admId,
        aadType: AdminAlarmType.분석,
        aadResultType: AdminAlarmResultType.성공,
        aadMessage: `재분석이 완료되었습니다 (${target}, ${charge}).`,
      });
    } catch (e) {
      this.logger.error(`reAnalyze compare 실패: oddId=${dto.oddId}`, e);
      await this.adminAlarmService.addAlarm({
        admId: dto.admId,
        aadType: AdminAlarmType.분석,
        aadResultType: AdminAlarmResultType.실패,
        aadMessage: `재분석이 실패했습니다 (${target}, ${charge}).`,
      });
    }
  }

  /**
   * Codef 응답 코드(CF-*)를 B2C 내부 코드(BOD*)로 변환
   */
  private convertCodefCodeToBodCode(codefCode: string): string {
    switch (codefCode) {
      case 'CF-00000':
        return 'BOD004'; // 분석완료
      case 'CF-03002':
        return 'BOD002'; // 자료수집실패
      case 'CF-13002':
        return 'BOD005'; // 분석실패
      default:
        // 기타 에러 코드는 분석실패로 처리
        return 'BOD005';
    }
  }

  /**
   * 자동차(2차) 로그 적재용 응답 구성
   * 다건 API는 배치 전체 공유 result 1세트만 반환하므로 자동차 개별 성공/실패를 result.code로 알 수 없다.
   * 자동차 로그가 기본(basic) 호출의 result로 위조되지 않도록, 자동차 데이터 수집 여부로 결과를 보정한다.
   * - 공유 인증 실패(code !== CF-00000): 자동차도 동일 배치 사유로 실패 → 공유 result 그대로 사용
   * - 공유 인증 성공 + 자동차 데이터 수집됨: 자동차 성공 → 공유 result(성공) 그대로 사용
   * - 공유 인증 성공 + 자동차 데이터 미수집: 자동차 미수집 → 성공코드 대신 미수집 상태로 보정(로그 위조 방지)
   */
  private buildCarLogResponse(
    sharedResult: CodefResult,
    carData?: Treatment,
  ): TreatmentResponseDto {
    const carCollected =
      (carData?.resBasicTreatList?.length ?? 0) > 0 ||
      (carData?.resDetailTreatList?.length ?? 0) > 0;

    if (sharedResult.code !== 'CF-00000' || carCollected) {
      return { result: sharedResult, data: carData };
    }

    //* 기본 인증은 성공했으나 자동차 진료정보가 수집되지 않은 경우(미수집)
    //* 항목별 result 미제공으로 인한 합성 상태값 → 결과상태는 실패(미수집)로 적재
    return {
      result: {
        ...sharedResult,
        code: 'CAR-NODATA',
        message: '자동차 진료정보 미수집',
        extraMessage: '',
      },
      data: carData,
    };
  }

  @OnEvent('treatCompare', { async: true })
  async handlerTreat(
    orderDetail: Awaited<ReturnType<OrderDetailService['getOrderDetail']>>,
    dto: TreatCompareDto,
  ) {
    //* 5년
    const nowDate = dayjs().format('YYYYMMDD');
    const startDate = dayjs().subtract(1825, 'day').format('YYYYMMDD');

    const codefData = {
      startDate: startDate,
      endDate: nowDate,
      organization: '0020', //공통값
      type: '1', //민감상병표기
      id: dto.altCode ? hashWithSHA256(dto.altCode) : '', //동일계정으로 구분값이 필요할 경우 암호화 해서 넣기
      loginType: dto.loginType,
      loginTypeLevel: dto.loginTypeLevel,
      authMethod: dto.authMethod,
      telecom: dto.telecom,
      timeout: dto.timeout,
      userName: dto.usrName,
      phoneNo: dto.usrPhone,
      identity: dto.usrBirth + dto.identityNo,
      secureNoYN: dto.secureNoYN,

      //* 2차 진료정보 요청시 필요값
      secureNo: dto.secureNo, // 보안문자 이미지정보(필수)
      secureNoRefresh: dto.secureNoRefresh, // 보안문자 새로고침 "0": 기본, "1": 재요청, "2" : 입력취소(default:0)
      smsAuthNo: dto.smsAuthNo, //[SMS 인증]필수
      simpleAuth: dto.simpleAuth, //! 추가확인필요 "0": cancel , "1": ok
      is2Way: dto.is2Way === 1 ? true : false, //인증후 요청일 시 true 고정
      twoWayInfo: {
        jobIndex: dto.jobIndex,
        threadIndex: dto.threadIndex,
        jti: dto.jti,
        twoWayTimestamp: dto.twoWayTimestamp,
      },
    };

    //* 2차 자료수집을 다건(배열)으로 발사 - 공유 twoWayInfo를 전 요소에 동일 적용 (index 0=기본, 1=자동차)
    //* 자동차요소(index 1)는 원본 carData와 동일한 자동차 수집 파라미터(organization '0020'/type '1' 민감상병표기/id)를
    //* 명시적으로 유지한다. 상품 구분자는 inquiryType만 사용하며, 다건전환으로 자동차 전용 파라미터가 누락되지 않도록 명시한다.
    //* 자동차(inquiryType='1')는 자동차 수집 OFF면 제외 (기능 OFF, 코드 보존)
    const carReservation = await this.treatCarCanaryService.acquire(
      providerFromTag(dto.jti) ?? undefined,
    );
    const carEnabled = !!carReservation;
    const treatItems: TreatmentMultipleItemDto[] = [
      { ...codefData, inquiryType: '0' },
      ...(carEnabled
        ? [
            {
              ...codefData,
              organization: '0020', //공통값(원본 carData와 동일)
              type: '1', //민감상병표기(원본 carData와 동일)
              id: dto.altCode ? hashWithSHA256(dto.altCode) : '', //동일계정 구분값(원본 carData와 동일 소스)
              inquiryType: '1',
            },
          ]
        : []),
    ];
    //* 2차 진료조회 호출. 예외성 실패(타임아웃/네트워크/E0304 등)는 createTreatLog 적재 전에
    //* 핸들러가 중단되어 로그가 통째로 누락되던 문제가 있었다(로그관리 미표시). 예외도 실패 로그를 남기고 실패 처리한다.
    let res;
    try {
      res = await this.treatScraper
        .fetchTreatmentMultiple(treatItems, dto.oddId)
        .catch(async (e: any) => {
          const failResult = {
            code: e?.codefCode ?? 'E0304',
            message: e?.codefMessage ?? e?.message ?? '2차 진료조회 실패',
            extraMessage: e?.codefExtraMessage ?? '',
            //* 벤더 거래번호(쿠콘 trSeq / 하이픈 reqCd)를 우선 기록. 없을 때만 세션태그(jti)로 폴백.
            //*  (타임아웃·통신실패건의 CS 추적키 — 예전엔 jti 블롭이 저장돼 벤더 검색이 안 됐다.)
            transactionId: e?.vendorTx ?? dto.jti ?? '',
            //* 수집 소요/타임아웃여부(벤더가 예외에 실어줌) — 분할 효과·오래걸린 실패 집계용.
            elapsedMs: e?.elapsedMs,
            isTimeout: e?.isTimeout ?? false,
          };
          //* 실패 로그 적재(2차 기본) — 운영 로그관리에서 추적 가능하도록
          await this.logTreatService
            .createTreatLog(
              dto,
              { result: failResult, data: undefined } as any,
              CodefRequestType.기본,
            )
            .catch((err) => this.logger.error(err));
          //* 실패 상태 변경 + 예약수 환불 (errorTreat은 마지막에 throw하므로 흡수)
          //*   상태는 errorTreat 내부에서 단계별 분기: 수집 타임아웃→분석실패(ODD007), 그 외→인증실패(ODD004).
          await this.errorTreat(
            {
              oddId: dto.oddId,
              memId: orderDetail.member!.memId,
              usrName: dto.usrName,
              ...failResult,
            },
            dto,
          ).catch((err) => this.logger.error(err));
          return null;
        });
    } finally {
      await this.treatCarCanaryService.release(carReservation);
    }
    //* 예외성 실패는 위에서 로그/상태 처리 완료 → 이후 정상 분석 흐름 중단
    if (!res) return;

    //* 심평원 '내가 먹는약 한눈에' 신규 API 별도 호출 (task-01) — 다건과 별개, 실패해도 분석 플로우 유지
    //* 입력은 옛 carData와 동일한 organization '0020' 세트(=treatItems[1]) 사용
    //* [task-01 보류] CODEF_DRUG_URL 인프라 미설정 — 인프라 준비 후 아래 주석 해제 (현재 호출 비활성화, no-op)
    const drugApiPromise = Promise.resolve<any>(null);
    // const drugApiPromise = this.codefService
    //   .fetchMedicineSummary(treatItems[1], dto.oddId)
    //   .catch((e) => {
    //     this.logger.error(
    //       `내가 먹는약 한눈에 API 호출 실패 (정회원): oddId=${dto.oddId}, error=${e?.message || e}`,
    //     );
    //     return null;
    //   });

    //* index 0=기본, 1=자동차로 역다중화 (task-06 다건 결과)
    //* 다건 API는 배치 전체에 대한 공유 result 1세트만 반환하므로(항목별 result 미제공)
    //* 자동차 개별 성공/실패를 result.code로 구분할 수 없다. 자동차 포함 여부는 아래 data[1] 길이로만 판정(부분허용 정책).
    const codef: TreatmentResponseDto = {
      result: res.result,
      data: res.data?.[0],
    };
    const carResponse: TreatmentResponseDto = {
      result: res.result,
      data: res.data?.[1],
    };

    //* 2차 인증(기본) 결과 로그 적재
    this.logTreatService
      .createTreatLog(dto, codef, CodefRequestType.기본)
      .catch((e) => {
        this.logger.error(e);
      });

    //* 2차 인증(자동차) 결과 로그 적재 — 자동차 수집 OFF(carEnabled=false)면 적재 생략.
    //*   OFF면 treatItems에 자동차를 안 실어 실제 수집도 안 하지만, 이 로그 적재가 carEnabled와
    //*   무관하면 CAT002(미수집) 슬롯이 남아 자동차가 나간 것처럼 보였다. 적재도 토글에 일원화.
    if (carEnabled) {
      this.logTreatService
        .createTreatLog(
          dto,
          this.buildCarLogResponse(res.result, carResponse.data),
          CodefRequestType.자동차,
        )
        .catch((e) => {
          this.logger.error(e);
        });
    }

    //* idx0(기본) 실패 시 전체 실패 처리
    //oddId, orderDetail.member.memId, dto.usrName 추가필요
    if (codef.result.code !== 'CF-00000') {
      await this.errorTreat(
        {
          oddId: dto.oddId,
          memId: orderDetail.member!.memId,
          usrName: dto.usrName,
          ...codef.result,
        },
        dto,
      );
    }

    // //*  임시데이터 저장
    const addTreat = {
      resBasicTreatList: codef.data!.resBasicTreatList!,
      resDetailTreatList: codef.data!.resDetailTreatList!,
      resPrescribeDrugList: codef.data!.resPrescribeDrugList!,
    };

    await this.treatRepository
      .create(dto.oddId, addTreat)
      .catch(async (error) => {
        await this.alarmService.addAlarm({
          memId: orderDetail.member!.memId,
          almUserName: dto.usrName,
          almType: AlarmType.분석,
          almMessage: '진료정보분석인증이 실패되었어요.',
        });
        //* 주문상세이력상태변경(인증확인에러) //자료수집에러
        await this.orderDetailService.updateOrderDetail({
          oddId: dto.oddId,
          oddState: OrderDetailState.인증실패,
          oddFailedAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
        });
        //* 인증실패: 예약수 환불
        await this.orderService.refundReserveQuantity(
          orderDetail.order!.ordId,
          dto.oddId,
        );
        await mysqlErrorWrapper(error);
      });

    // 자부상 기본, 상세 데이터 create 기능 추가(2025-03-10)
    if (
      (carResponse.data?.resBasicTreatList?.length ?? 0) > 0 ||
      (carResponse.data?.resDetailTreatList?.length ?? 0) > 0
    ) {
      //* 부분허용 정책: 자동차 데이터 부재(미수집)는 정상 경로이므로 옵셔널/빈배열 기본값으로 안전 처리
      const addTreatCar = {
        resBasicTreatList: carResponse.data?.resBasicTreatList ?? [],
        resDetailTreatList: carResponse.data?.resDetailTreatList ?? [],
      };

      await this.treatRepository
        .createCarTreat(dto.oddId, addTreatCar)
        .catch((error) => {
          //* 자동차는 내부 카나리 부가정보다. 저장 실패가 건강 진료 성공/주문 상태를 뒤집지 않는다.
          this.logger.error(
            `자동차 진료 저장 실패(건강 성공 유지): oddId=${dto.oddId}, error=${error?.message || error}`,
          );
        });
    }

    //* 심평원 '내가 먹는약 한눈에' 응답 처리 — 실패하더라도 분석 플로우는 계속
    //* result/data 필드가 비정상(undefined)이어도 동기 throw가 발생하지 않도록 try/catch + 옵셔널 체이닝 적용
    //* 빈 리스트 응답에서도 createMedicineSummary를 호출해 기존 stale 데이터를 정리 (N→0 케이스 대응)
    try {
      const drugResponse = await drugApiPromise;
      if (drugResponse && drugResponse.result?.code === 'CF-00000') {
        await this.treatRepository
          .createMedicineSummary(
            dto.oddId,
            drugResponse.data?.resMedicineSummaryList ?? [],
          )
          .catch((e) => {
            this.logger.error(
              `내가 먹는약 한눈에 저장 실패 (정회원): oddId=${dto.oddId}, error=${e?.message || e}`,
            );
          });
      }
    } catch (e: any) {
      this.logger.error(
        `내가 먹는약 한눈에 응답 처리 실패 (정회원): oddId=${dto.oddId}, error=${e?.message || e}`,
      );
    }

    //* 바로 분석이 진행되 불필요한 스텝으로 보임
    // await this.orderDetailService.updateOrderDetail({
    //   oddId: dto.oddId,
    //   oddState: OrderDetailState.자료수집완료,
    // });

    //* 분석과 재분석의 상태변경이 달라야 하는지?
    //* 주문상세이력상태변경(분석요청)
    await this.orderDetailService.updateOrderDetail({
      oddId: dto.oddId,
      oddState: OrderDetailState.자료분석중, //분석요청
    });

    //* 분석
    //* 2025-03-12 기준 내부 로직에 자동차 진료기록 분석도 포함
    await this.treatRepository.createAnalyze(dto.oddId).catch(async (e) => {
      this.logger.error(
        `분석 실패: oddId=${dto.oddId}, error=${e?.message || e}`,
      );

      //* 주문상세이력상태변경(분석실패) - 상태 변경을 먼저 실행
      await this.orderDetailService.updateOrderDetail({
        oddId: dto.oddId,
        oddState: OrderDetailState.분석실패,
        oddFailedAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
      });

      await this.alarmService
        .addAlarm({
          memId: orderDetail.member!.memId,
          almUserName: orderDetail.user?.usrName,
          almType: AlarmType.분석,
          almMessage: '진료정보분석이 실패되었어요.',
        })
        .catch((alarmError) => {
          this.logger.error(
            `알람 생성 실패: oddId=${dto.oddId}, error=${alarmError?.message || alarmError}`,
          );
        });

      //* 분석실패: 예약수 환불
      await this.orderService.refundReserveQuantity(
        orderDetail.order!.ordId,
        dto.oddId,
      );
      throw new NotFoundException(ANALYSIS_FAILED);
    });

    //* 분석완료 — 사용 카운터와 완료 상태·시각을 **한 트랜잭션**에서 쓴다(M2 D4).
    //*  예전에는 카운터를 올린 뒤 알림을 보내고 별도 호출로 완료 시각을 썼고, 그 사이의
    //*  가드 없는 addAlarm 이 던지면 «카운터는 올랐는데 완료 시각은 없는» 행이 남았다.
    //*  알림은 이제 완료 기록이 커밋된 **뒤**로 간다.
    await this.orderService.addQuantity(
      orderDetail.order!.ordId,
      dto.oddId,
      dayjs().format('YYYY-MM-DD HH:mm:ss'),
    );

    //* 임시데이터 삭제
    // await this.treatRepository.delete(oddId);

    //* 분석성공시 — 완료 기록은 이미 끝났다. 알림 실패가 그것을 되돌리지 않는다.
    await this.alarmService.addAlarm({
      memId: orderDetail.member!.memId,
      almUserName: orderDetail.user?.usrName,
      almType: AlarmType.분석,
      almMessage: '진료정보분석이 완료되었어요.',
    });
  }

  async errorTreat(dto: CodefTreatResultDto, treatDto: TreatCompareDto) {
    //* 처리 벤더 식별 (jti 태그의 provider; 단일·태그없음이면 codef)
    const vendor: TreatVendor =
      (providerFromTag(treatDto.jti) as TreatVendor) ?? 'codef';
    //* 추후 2안으로 변경시 조건에 따라 알림 및 실패 처리 하지 않도록 조치 필요
    //* 1안의 경우 해당 건도 바로 실패건으로 인지해서 알림을 보내야함
    await this.alarmService.addAlarm({
      memId: dto.memId,
      almUserName: dto.usrName,
      almType: AlarmType.분석,
      almMessage: this.codefErrorMessage(
        dto.code,
        treatDto,
        vendor,
        dto.message,
        (dto as any).isTimeout,
      ),
    });

    //* 주문상세이력상태변경 — 2차 실패를 단계별로 분류 (이 경로는 분석 프로시저 전이라 '분석실패' 아님).
    //*   진짜 분석실패(ODD007)는 createAnalyze catch에서만. 여기서는:
    //*     - 인증 단계 실패(인증번호·인증서·미승인·앱미설치·만료·인증타임아웃·개인정보오류) → 인증실패(ODD004)
    //*     - 그 외(수집 타임아웃·처리중/기관 오류·네트워크·시스템 등) → 자료수집실패(ODD008)
    //*   수집 데이터가 없는 상태라, 둘 다 재분석 불가(가드가 막음). 수집 타임아웃은 oddTimeout=1로도 표시.
    const isTimeout = (dto as any).isTimeout === true;
    const { category } = resolveTreatError(vendor, dto.code, {
      errMsg: dto.message,
    });
    const AUTH_FAIL_CATEGORIES = new Set<TreatErrorCategory>([
      TreatErrorCategory.AUTH_CODE_WRONG,
      TreatErrorCategory.AUTH_CERT,
      TreatErrorCategory.AUTH_NOT_APPROVED,
      TreatErrorCategory.AUTH_APP_MISSING,
      TreatErrorCategory.AUTH_TIMEOUT,
      TreatErrorCategory.AUTH_EXPIRED,
      TreatErrorCategory.INPUT_INVALID,
    ]);
    await this.orderDetailService.updateOrderDetail({
      oddId: dto.oddId,
      oddState: AUTH_FAIL_CATEGORIES.has(category)
        ? OrderDetailState.인증실패
        : OrderDetailState.자료수집실패,
      oddFailedAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
      oddTimeout: isTimeout ? 1 : 0,
    });
    //* 인증실패: 예약수 환불 (ordId는 orderDetail에서 조회)
    const orderDetail = await this.orderDetailService.getOrderDetail(dto.oddId);
    if (orderDetail.order?.ordId) {
      await this.orderService.refundReserveQuantity(
        orderDetail.order.ordId,
        dto.oddId,
      );
    }
    this.logger.log(dto);

    throw new CodefException({
      type: 'treat',
      codefCode: dto.code,
      codefMessage: dto.message,
      codefExtraMessage: dto.extraMessage,
      vendor, //* ⑤ filter에서 category·retryable 해석에 사용
    });
  }

  //* (vendor, native코드) → 알림톡 실패 메시지. 단일 출처는 resolveTreatError(error 모듈).
  //*   codef 16코드는 기존 switch와 동일 메시지(resolver 36테스트가 보장).
  //*   coocon·octover·hyphen native코드도 이제 category로 분류돼 generic default를 벗어남.
  //*   CF-03002(2차)만 loginType 의존 특수 메시지라 여기서 분기 유지.
  private codefErrorMessage(
    codefCode: string,
    treatDto: TreatCompareDto,
    vendor: TreatVendor = 'codef',
    errMsg?: string,
    isTimeout?: boolean,
  ): string {
    if (codefCode === 'CF-03002') {
      return treatDto.loginType === '2' && treatDto.loginTypeLevel === '1'
        ? '잘못된 인증번호로 진료정보분석인증이 실패되었어요.'
        : '요청한 인증기관의 미인증으로 진료정보분석인증이 실패되었어요.';
    }
    //* 수집 타임아웃(병력 과다 헤비유저)이면 기본 메시지 대신 건강이음 안내만 보낸다.
    //*   다른 실패(인증/데이터없음 등)는 기존 메시지 유지.
    if (isTimeout) {
      return "병력 데이터가 많아 분석 제한됩니다. '건강이음'을 확인하세요.";
    }
    return resolveTreatError(vendor, codefCode, { errMsg }).alimtalkMessage;
  }

  @OnEvent('b2cTreatCompare', { async: true })
  async handlerB2cTreat(dto: TreatCompareDto) {
    this.logger.log('B2C 플로우: 진료정보 수집 및 분석 시작');

    //* 5년
    const nowDate = dayjs().format('YYYYMMDD');
    const startDate = dayjs().subtract(1825, 'day').format('YYYYMMDD');

    const codefData = {
      startDate: startDate,
      endDate: nowDate,
      organization: '0020', //공통값
      type: '1', //민감상병표기
      id: dto.altCode ? hashWithSHA256(dto.altCode) : '', //B2C는 agcCode 사용
      loginType: dto.loginType,
      loginTypeLevel: dto.loginTypeLevel,
      authMethod: dto.authMethod,
      telecom: dto.telecom,
      timeout: dto.timeout,
      userName: dto.usrName,
      phoneNo: dto.usrPhone,
      identity: dto.usrBirth + dto.identityNo,
      secureNoYN: dto.secureNoYN,
      //* 2차 진료정보 요청시 필요값
      secureNo: dto.secureNo,
      secureNoRefresh: dto.secureNoRefresh,
      smsAuthNo: dto.smsAuthNo,
      simpleAuth: dto.simpleAuth,
      is2Way: dto.is2Way === 1 ? true : false,
      twoWayInfo: {
        jobIndex: dto.jobIndex,
        threadIndex: dto.threadIndex,
        jti: dto.jti,
        twoWayTimestamp: dto.twoWayTimestamp,
      },
    };

    //* B2C 2차는 certify 응답의 oddId(=bodId)를 1차 키로 써야 한다.
    //* agcCode 최신 1건 재조회는 같은 기업 동시 요청에서 다른 고객 주문으로 교차 저장될 수 있다.
    const b2cOrderDetailRow =
      await this.analyzeRepository.getB2cOrderDetailById(dto.oddId);
    if (!b2cOrderDetailRow) {
      this.logger.error(
        `B2C order_detail을 찾을 수 없습니다: bodId=${dto.oddId}, agcCode=${dto.altCode}`,
      );
      return;
    }
    if (b2cOrderDetailRow.agency.agcCode !== dto.altCode) {
      this.logger.error(
        `B2C order_detail agcCode 불일치: bodId=${dto.oddId}, expected=${b2cOrderDetailRow.agency.agcCode}, actual=${dto.altCode}`,
      );
      return;
    }
    const b2cOrderDetail = {
      bodId: b2cOrderDetailRow.orderDetail.bodId,
      bodTransactionId: b2cOrderDetailRow.orderDetail.bodTransactionId,
      bodAgcId: b2cOrderDetailRow.agency.agcId,
      bodAgcCorporateName: b2cOrderDetailRow.agency.agcName,
    };

    const bodId = b2cOrderDetail.bodId;
    this.logger.log(
      `B2C order_detail 조회 완료: bodId=${bodId}, agcCode=${dto.altCode}`,
    );

    try {
      //* 2차 자료수집을 다건(배열)으로 발사 - 공유 twoWayInfo를 전 요소에 동일 적용 (index 0=기본, 1=자동차)
      //* 자동차요소(index 1)는 원본 carData와 동일한 자동차 수집 파라미터(organization '0020'/type '1' 민감상병표기/id)를
      //* 명시적으로 유지한다. 상품 구분자는 inquiryType만 사용하며, 다건전환으로 자동차 전용 파라미터가 누락되지 않도록 명시한다.
      //* 자동차(inquiryType='1')는 자동차 수집 OFF면 제외 (기능 OFF, 코드 보존)
      //* B2C agcCode는 여러 사용자가 공유하므로 G0 1회 카나리에서 항상 제외한다.
      const carEnabled = false;
      const treatItems: TreatmentMultipleItemDto[] = [
        { ...codefData, inquiryType: '0' },
        ...(carEnabled
          ? [
              {
                ...codefData,
                organization: '0020', //공통값(원본 carData와 동일)
                type: '1', //민감상병표기(원본 carData와 동일)
                id: dto.altCode ? hashWithSHA256(dto.altCode) : '', //B2C는 agcCode 사용(원본 carData와 동일 소스)
                inquiryType: '1',
              },
            ]
          : []),
      ];
      const res = await this.treatScraper.fetchTreatmentMultiple(treatItems, 0);

      //* 심평원 '내가 먹는약 한눈에' 신규 API 별도 호출 (B2C, task-01) — 다건과 별개, 실패해도 분석 플로우 유지
      //* 입력은 옛 carData와 동일한 organization '0020' 세트(=treatItems[1]) 사용
      //* [task-01 보류] CODEF_DRUG_URL 인프라 미설정 — 인프라 준비 후 아래 주석 해제 (현재 호출 비활성화, no-op)
      const drugApiPromise = Promise.resolve<any>(null);
      // const drugApiPromise = this.codefService
      //   .fetchMedicineSummary(treatItems[1], 0)
      //   .catch((e) => {
      //     this.logger.error(
      //       `내가 먹는약 한눈에 API 호출 실패 (B2C): bodId=${bodId}, error=${e?.message || e}`,
      //     );
      //     return null;
      //   });

      //* index 0=기본, 1=자동차로 역다중화 (task-06 다건 결과)
      //* 다건 API는 배치 전체에 대한 공유 result 1세트만 반환하므로(항목별 result 미제공)
      //* 자동차 개별 성공/실패를 result.code로 구분할 수 없다. 자동차 포함 여부는 data[1] 존재로만 판정(부분허용 정책).
      const codef: TreatmentResponseDto = {
        result: res.result,
        data: res.data?.[0],
      };
      const carResponse: TreatmentResponseDto = {
        result: res.result,
        data: res.data?.[1],
      };

      //* 2차 인증(기본) API 로그 적재
      this.logB2cTreatService
        .createTreatLog(
          {
            bodId,
            bcuName: dto.usrName,
            bcuPhone: dto.usrPhone,
            bcuBirth: dto.usrBirth,
            agcCode: dto.altCode,
            loginType: dto.loginType,
            loginTypeLevel: dto.loginTypeLevel,
          },
          b2cOrderDetail.bodAgcId ?? undefined,
          b2cOrderDetail.bodAgcCorporateName ?? undefined,
          codef,
          B2cCodefRequestType.기본,
        )
        .catch((e) => {
          this.logger.error(`B2C 로그 적재 실패: ${e}`);
        });

      //* Codef 응답 코드 검증 (정회원과 동일)
      if (codef.result.code !== 'CF-00000') {
        this.logger.error(
          `B2C 진료정보 수집 실패: ${codef.result.code} - ${codef.result.message}`,
        );

        //* B2C order_detail 실패 상태 업데이트
        await this.analyzeRepository.updateB2cOrderDetail(bodId, {
          bodTransactionId:
            codef.result.transactionId || b2cOrderDetail.bodTransactionId,
          bodState: this.convertCodefCodeToBodCode(codef.result.code),
          bodFailedAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
        });

        //* B2C 분석 실패 시 알림톡 발송
        try {
          await this.alimtalkService.sendB2cAnalysisFailure({
            memId: 0,
            ordId: 0,
            bodId,
            fcmCode: dto.altCode,
            memName: dto.usrName,
            memPhone: dto.usrPhone,
          });
          this.logger.log(
            `B2C 진료정보 수집 실패 알림톡 발송 완료: bodId=${bodId}, fcmCode=${dto.altCode}, resultCode=${codef.result.code}`,
          );
        } catch (alimtalkError: any) {
          this.logger.error(
            `B2C 진료정보 수집 실패 알림톡 발송 실패: ${alimtalkError?.message || alimtalkError}`,
            alimtalkError?.stack,
          );
        }

        return;
      }

      //* 2차 인증(자동차) API 로그 적재 — 자동차 수집 OFF(carEnabled=false)면 적재 생략(CAT002 슬롯 미생성).
      if (carEnabled) {
        this.logB2cTreatService
          .createTreatLog(
            {
              bodId,
              bcuName: dto.usrName,
              bcuPhone: dto.usrPhone,
              bcuBirth: dto.usrBirth,
              agcCode: dto.altCode,
              loginType: dto.loginType,
              loginTypeLevel: dto.loginTypeLevel,
            },
            b2cOrderDetail.bodAgcId ?? undefined,
            b2cOrderDetail.bodAgcCorporateName ?? undefined,
            this.buildCarLogResponse(res.result, carResponse.data),
            B2cCodefRequestType.자동차,
          )
          .catch((e) => {
            this.logger.error(`B2C 자동차 로그 적재 실패: ${e}`);
          });
      }

      //* B2C order_detail 업데이트 (transactionId 갱신) - 진행 중 상태
      await this.analyzeRepository.updateB2cOrderDetail(bodId, {
        bodTransactionId:
          codef.result.transactionId || b2cOrderDetail.bodTransactionId,
        bodState: 'BOD003', // 자료분석중
      });

      //* B2C 진료 데이터 처리: B2C 전용 테이블에 저장 및 분석
      this.logger.log(
        `B2C 진료 데이터 저장 시작: bodId=${bodId}, transactionId=${codef.result.transactionId}`,
      );

      await this.treatRepository
        .createB2cAnalyze(bodId, {
          transactionId: codef.result.transactionId || '',
          codefResultCode: codef.result.code || '',
          medical: codef.data,
          car:
            (carResponse.result.code === 'CF-00000' ||
              carResponse.result.code === 'CF-03002') &&
            carResponse.data
              ? carResponse.data
              : undefined,
        })
        .catch((error) => {
          this.logger.error(
            `B2C 진료 데이터 저장 실패: ${error?.message || error}`,
            error?.stack,
          );
          throw error;
        });

      //* 심평원 '내가 먹는약 한눈에' 응답 저장 (B2C) — 실패해도 분석 플로우는 계속
      //* result/data 필드가 비정상(undefined)이어도 동기 throw가 발생하지 않도록 try/catch + 옵셔널 체이닝 적용
      //* 빈 리스트 응답에서도 createB2cMedicineSummary를 호출해 기존 stale 데이터를 정리 (N→0 케이스 대응)
      try {
        const drugResponse = await drugApiPromise;
        if (drugResponse && drugResponse.result?.code === 'CF-00000') {
          await this.treatRepository
            .createB2cMedicineSummary(
              bodId,
              drugResponse.data?.resMedicineSummaryList ?? [],
            )
            .catch((e) => {
              this.logger.error(
                `내가 먹는약 한눈에 저장 실패 (B2C): bodId=${bodId}, error=${e?.message || e}`,
              );
            });
        }
      } catch (e: any) {
        this.logger.error(
          `내가 먹는약 한눈에 응답 처리 실패 (B2C): bodId=${bodId}, error=${e?.message || e}`,
        );
      }

      this.logger.log(`B2C 분석 완료: bodId=${bodId}`);

      //* B2C order_detail 완료 상태 업데이트
      await this.analyzeRepository.updateB2cOrderDetail(bodId, {
        bodState: 'BOD004', // 분석완료
        bodCompletedAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
      });

      //* B2C 분석 성공 시 알림톡 발송
      try {
        await this.alimtalkService.sendB2cAnalysisSuccess({
          memId: 0, // B2C는 memId 없음
          ordId: 0, // B2C는 ordId 없음
          bodId: bodId,
          memName: dto.usrName,
          memPhone: dto.usrPhone,
        });
        this.logger.log(`B2C 분석 성공 알림톡 발송 완료: bodId=${bodId}`);
      } catch (alimtalkError: any) {
        this.logger.error(
          `B2C 분석 성공 알림톡 발송 실패: ${alimtalkError?.message || alimtalkError}`,
          alimtalkError?.stack,
        );
      }
    } catch (error: any) {
      this.logger.error(
        `B2C 분석 처리 오류: ${error?.message || error}`,
        error?.stack,
      );

      //* B2C order_detail 실패 상태 업데이트
      await this.analyzeRepository.updateB2cOrderDetail(bodId, {
        bodState: 'BOD005', // 분석실패
        bodFailedAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
      });

      //* B2C 분석 실패 시 알림톡 발송
      try {
        await this.alimtalkService.sendB2cAnalysisFailure({
          memId: 0, // B2C는 memId 없음
          ordId: 0, // B2C는 ordId 없음
          bodId, // B2C order detail ID
          fcmCode: dto.altCode, // B2C는 altCode를 fcmCode로 사용
          memName: dto.usrName,
          memPhone: dto.usrPhone,
        });
        this.logger.log(
          `B2C 분석 실패 알림톡 발송 완료: fcmCode=${dto.altCode}`,
        );
      } catch (alimtalkError: any) {
        this.logger.error(
          `B2C 분석 실패 알림톡 발송 실패: ${alimtalkError?.message || alimtalkError}`,
          alimtalkError?.stack,
        );
      }
    }
  }
}
