import {
  BadRequestException,
  ConflictException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common';
import * as crypto from 'crypto';
import Redis from 'ioredis';

import {
  ALREADY_ANALYSIS,
  ANALYSIS_FAILED,
  APP_INSTALLATION_REQUIRED,
  INSTITUTIONAL_ERROR,
  NO_COLLECTED_DATA,
  NOT_FOUND_USER,
} from '@api/constants/api.constant';
import { hashWithSHA256 } from '@api/helpers/format.helper';
import { validateResidentInput } from '@api/helpers/identity.helper';
import { newUuid } from '@api/helpers/uid.helper';
import { CodefService } from '@lib/codef/src';
import { CodefResult } from '@lib/codef/src/codef.dto';
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 { HyphenService } from '@lib/hyphen/src';
import {
  providerFromTag,
  TreatDispatcher,
} from './dispatch/treat-dispatcher.service';
import { TreatVendor } from './error';
import { mysqlErrorWrapper } from '@lib/drizzle/src/drizzle.utill';
import { ConfigService } from '@nestjs/config';
import { EventEmitter2 } from '@nestjs/event-emitter';
import dayjs from 'dayjs';
import { AlarmType } from '../alarm/alarm.entity';
import { AlarmService } from '../alarm/alarm.service';
import { AlimtalkService } from '../alimtalk/alimtalk.service';
import { AnalyzeRepository } from '../analyze/analyze.repository';
import { ClientService } from '../client/client/client.service';
import { InstitutionCode } from '../institution-block/institution-block.entity';
import { InstitutionBlockService } from '../institution-block/institution-block.service';
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 { UserService } from '../user/user.service';
import { FcService } from '../fc/fc.service';
import { LogTreatService } from './log/log-treat.service';
import { LogB2cTreatService } from './log-b2c/log-b2c-treat.service';
import {
  CertiftDto,
  CodefTreatResultDto,
  CompareDto,
  GetCertifyResponseDto,
  TreatCompareDto,
  TreatDto,
} from './treat.dto';
import { TreatRepository } from './treat.repository';
import { TreatCarCanaryService } from './treat-car-canary.service';

const CERTIFY_IN_PROGRESS = {
  code: 'E0311',
  message: '이미 인증요청이 진행 중입니다.',
  retryable: true,
  activeCertify: true,
};
const CERTIFY_CLAIM_TTL_MS =
  Number(process.env.CERTIFY_CLAIM_TTL_MS ?? 360_000) || 360_000;
const CERTIFY_RESUME_TTL_MS =
  Number(process.env.CERTIFY_RESUME_TTL_MS ?? 5 * 60_000) || 5 * 60_000;
const CERTIFY_OTP_TTL_MS =
  Number(process.env.CERTIFY_OTP_TTL_MS ?? 170_000) || 170_000;

@Injectable()
export class TreatService {
  private logger = new Logger(TreatService.name);
  private readonly certifyClaimRedis: Redis | null;
  private readonly certifyClaimTtlMs = CERTIFY_CLAIM_TTL_MS;
  private readonly certifyResumeTtlMs = CERTIFY_RESUME_TTL_MS;
  private readonly certifyOtpTtlMs = CERTIFY_OTP_TTL_MS;

  constructor(
    private codefService: CodefService,
    private cooconService: CooconService,
    private octoverService: OctoverService,
    private hyphenService: HyphenService,
    private treatDispatcher: TreatDispatcher,
    private configService: ConfigService,
    private orderService: OrderService,
    private orderDetailService: OrderDetailService,
    private UserService: UserService,
    private alarmService: AlarmService,
    private alimtalkService: AlimtalkService,
    private clientService: ClientService,
    private analyzeRepository: AnalyzeRepository,
    private treatRepository: TreatRepository,
    private eventEmitter: EventEmitter2,
    private logTreatService: LogTreatService,
    private logB2cTreatService: LogB2cTreatService,
    private fcService: FcService,
    private institutionBlockService: InstitutionBlockService,
    private treatCarCanaryService: TreatCarCanaryService,
  ) {
    const url = this.configService.get<string>('redisUrl');
    this.certifyClaimRedis = url
      ? new Redis(url, { maxRetriesPerRequest: 1 })
      : null;
  }

  //* 진료내역 스크래핑 공급자 스위치 (configuration.treatProvider: 'codef'(기본)|'coocon')
  //* CodefService·CooconService 모두 TreatScraper(fetchCertifyMultiple/fetchTreatmentMultiple)를 만족
  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;
    if (p === 'hyphen') return this.hyphenService;
    return this.codefService;
  }

  private certifyClaimKey(altCode: string): string {
    return `certify:claim:${hashWithSHA256(altCode)}`;
  }

  private certifyResumeKey(altCode: string): string {
    return `certify:resume:${hashWithSHA256(altCode)}`;
  }

  private async clearCertifyResume(altCode: string): Promise<void> {
    if (!this.certifyClaimRedis) return;
    try {
      await this.certifyClaimRedis.del(this.certifyResumeKey(altCode));
    } catch (e) {
      this.logger.warn(
        `certify resume 삭제 실패 alt=${hashWithSHA256(altCode).slice(0, 8)}: ${(e as Error).message}`,
      );
    }
  }

  private async saveCertifyResume(
    altCode: string,
    result: GetCertifyResponseDto,
  ): Promise<void> {
    if (!this.certifyClaimRedis) return;
    try {
      await this.certifyClaimRedis.set(
        this.certifyResumeKey(altCode),
        JSON.stringify(result),
        'PX',
        this.certifyResumeTtlMs,
      );
    } catch (e) {
      this.logger.warn(
        `certify resume 저장 실패 alt=${hashWithSHA256(altCode).slice(0, 8)}: ${(e as Error).message}`,
      );
    }
  }

  private certifyOtpExpiresAt(result: GetCertifyResponseDto): number | null {
    const issuedAt = Number(result.data?.twoWayTimestamp);
    return Number.isFinite(issuedAt) && issuedAt > 0
      ? issuedAt + this.certifyOtpTtlMs
      : null;
  }

  private async expireCertifyResume(
    altCode: string,
    result?: GetCertifyResponseDto,
    active?: { oddId: number; ordId: number },
  ): Promise<boolean> {
    const oddId = result?.orderDetail?.oddId ?? active?.oddId;
    let ordId = active?.ordId;
    if (!ordId && oddId) {
      const detail = await this.orderDetailService
        .getOrderDetail(oddId)
        .catch(() => undefined);
      ordId = detail?.order?.ordId;
    }

    const expired =
      oddId && ordId
        ? await this.orderService.expireAuthAndRefund(ordId, oddId)
        : 0;
    if (expired && result?.data?.jti) {
      await this.treatDispatcher.cancelCertify(result.data.jti).catch((e) => {
        this.logger.warn(
          `certify 만료 엔진 취소 실패 alt=${hashWithSHA256(altCode).slice(0, 8)}: ${(e as Error).message}`,
        );
      });
    }
    await this.clearCertifyResume(altCode);
    return (expired ?? 0) > 0;
  }

  private async expireActiveCertifyIfNeeded(altCode: string): Promise<boolean> {
    const detail = await this.alimtalkService.getActiveCertify(altCode);
    if (
      !detail ||
      dayjs(detail.oddCreatedAt)
        .add(this.certifyOtpTtlMs, 'millisecond')
        .valueOf() > Date.now()
    ) {
      return false;
    }
    return this.expireCertifyResume(altCode, undefined, detail);
  }

  async getCertifyResume(altCode: string, attempt?: string) {
    if (attempt !== undefined) await this.alimtalkService.validateReminder(altCode, attempt);
    const detail = await this.alimtalkService.checkAlimtalkEntry(altCode);
    const active =
      (detail as { resumeInProgress?: boolean }).resumeInProgress === true;
    if (!this.certifyClaimRedis) {
      if (active && (await this.expireActiveCertifyIfNeeded(altCode))) {
        return { state: 'EXPIRED' } as const;
      }
      return { state: active ? 'ACTIVE' : 'NONE' } as const;
    }

    try {
      const cached = await this.certifyClaimRedis.get(
        this.certifyResumeKey(altCode),
      );
      if (cached) {
        const result = JSON.parse(cached) as GetCertifyResponseDto;
        const ttl = await this.certifyClaimRedis.pttl(
          this.certifyResumeKey(altCode),
        );
        const redisExpiresAt =
          Date.now() + (ttl > 0 ? ttl : this.certifyResumeTtlMs);
        const otpExpiresAt = this.certifyOtpExpiresAt(result);
        const expiresAt = otpExpiresAt
          ? Math.min(redisExpiresAt, otpExpiresAt)
          : redisExpiresAt;
        if (expiresAt <= Date.now()) {
          const expired = await this.expireCertifyResume(altCode, result);
          return { state: expired ? 'EXPIRED' : 'ACTIVE' } as const;
        }
        return {
          state: 'READY',
          result,
          expiresAt,
        } as const;
      }
      const pending = await this.certifyClaimRedis.exists(
        this.certifyClaimKey(altCode),
      );
      if (pending) {
        const ttl = await this.certifyClaimRedis.pttl(
          this.certifyClaimKey(altCode),
        );
        return {
          state: 'PENDING',
          expiresAt:
            Date.now() + (ttl > 0 ? ttl : this.certifyClaimTtlMs),
        } as const;
      }
    } catch (e) {
      this.logger.warn(
        `certify resume 조회 실패 alt=${hashWithSHA256(altCode).slice(0, 8)}: ${(e as Error).message}`,
      );
    }
    if (active && (await this.expireActiveCertifyIfNeeded(altCode))) {
      return { state: 'EXPIRED' } as const;
    }
    return { state: active ? 'ACTIVE' : 'NONE' } as const;
  }

  private async tryClaimCertify(altCode: string): Promise<string | null> {
    if (!this.certifyClaimRedis) return null;
    const token = crypto.randomUUID();
    try {
      const ok = await this.certifyClaimRedis.set(
        this.certifyClaimKey(altCode),
        token,
        'PX',
        this.certifyClaimTtlMs,
        'NX',
      );
      if (ok !== 'OK') throw new BadRequestException(CERTIFY_IN_PROGRESS);
      return token;
    } catch (e) {
      if (e instanceof BadRequestException) throw e;
      this.logger.warn(
        `certify claim Redis 실패 - fail-open alt=${hashWithSHA256(altCode).slice(0, 8)}: ${(e as Error).message}`,
      );
      return null;
    }
  }

  private async releaseCertifyClaim(
    altCode: string,
    token: string | null,
  ): Promise<void> {
    if (!token || !this.certifyClaimRedis) return;
    try {
      await this.certifyClaimRedis.eval(
        "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end",
        1,
        this.certifyClaimKey(altCode),
        token,
      );
    } catch (e) {
      this.logger.warn(
        `certify claim 해제 실패 alt=${hashWithSHA256(altCode).slice(0, 8)}: ${(e as Error).message}`,
      );
    }
  }

  private async refreshCertifyClaim(
    altCode: string,
    token: string,
  ): Promise<void> {
    if (!this.certifyClaimRedis) return;
    await this.certifyClaimRedis
      .eval(
        "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) else return 0 end",
        1,
        this.certifyClaimKey(altCode),
        token,
        this.certifyClaimTtlMs,
      )
      .catch((e) => {
        this.logger.warn(
          `certify claim 갱신 실패 alt=${hashWithSHA256(altCode).slice(0, 8)}: ${(e as Error).message}`,
        );
      });
  }

  private startCertifyClaimHeartbeat(
    altCode: string,
    token: string | null,
  ): NodeJS.Timeout | undefined {
    if (!token || !this.certifyClaimRedis) return;
    const timer = setInterval(
      () => void this.refreshCertifyClaim(altCode, token),
      Math.min(60_000, Math.floor(this.certifyClaimTtlMs / 3)),
    );
    timer.unref();
    return timer;
  }

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

  /**
   * 심평원 개인인증 요청
   * 성공 : 알림톡 사용완료 O | 주문상세 create 상태값(인증요청중) | FC 알람 X
   * 실패 : 알림톡 사용완료 X | 주문상세 create X | FC 알람 X
   * */
  async certify(dto: CertiftDto) {
    //* 심평원 기관 사용중지 여부 우선 확인 (관리자 설정)
    await this.institutionBlockService.assertNotBlocked(InstitutionCode.심평원);

    //* 사전 입력검증 (체크섬·생년월일·성별/세기코드·만14세·이름) — 잘못된 입력이
    //* 대기열/codef 슬롯을 점유하기 전에 진입 차단. 실패 시 명확한 코드/메시지 반환.
    //* "이름↔주민 실존 매핑"은 본인확인기관(NICE/KCB) 영역이라 여기서 불가 → codef 본인인증이 담당.
    const pre = validateResidentInput(
      dto.usrName,
      dto.usrBirth,
      dto.identityNo,
    );
    if (!pre.ok) {
      throw new BadRequestException({ code: pre.code, message: pre.message });
    }
    //* [실명확인 hook] NICE/KCB 실명확인 계약 시 활성 — SMS 없이 이름+주민 매핑만 사전확인,
    //*   불일치면 여기서 차단(대기열·codef 진입 전). 현재 미계약이라 미사용.
    //*   const real = await this.realNameService.verify(dto.usrName, dto.usrBirth + dto.identityNo);
    //*   if (!real.matched) throw new BadRequestException({ code: 'NAME_MISMATCH', message: '이름과 주민등록번호가 일치하지 않습니다.' });

    let orderInfo;
    let certifyClaimToken: string | null = null;
    let certifyClaimHeartbeat: NodeJS.Timeout | undefined;

    try {

    //* B2C 플로우인 경우 알림톡 검증 건너뛰기
    if (dto.isB2c) {
      // B2C는 orderInfo 없이 진행 (agcCode를 altCode로 사용)
      orderInfo = null;
    } else {
      //* 알림톡 정보 찾기
      orderInfo = await this.alimtalkService.validationAlimtalkForCertify(
        dto.altCode,
      );
      certifyClaimToken = await this.tryClaimCertify(dto.altCode);
      certifyClaimHeartbeat = this.startCertifyClaimHeartbeat(
        dto.altCode,
        certifyClaimToken,
      );
      await this.clearCertifyResume(dto.altCode);
      //* 초대링크 접근 유효성(구독종료/건수소진/결제취소) — 발송 후 만료된 선발송 링크 차단.
      //*   만료/소진 시 EXPIRED_ACCESS_LINK(E0214) → 프론트 "접근 기간 만료" 화면.
      await this.alimtalkService.validateInviteAccess(orderInfo.order!.ordId);
      //* 한도 검증 + 예약수 +1 (인증요청중 진입 시점 차감)
      await this.orderService.reserveQuantity(orderInfo.order!.ordId);
    }

    //* 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,
    };

    //* 1차 진입점에서는 건강만 넘긴다. 멀티 디스패처가 공급자를 고른 뒤,
    //* master+canary가 모두 ON인 inhouse 요청에만 자동차를 합성한다.
    //* 단일 외부 공급자 구성은 자동차를 fail-closed로 유지한다.
    const certifyItems: TreatmentMultipleItemDto[] = [
      { ...codefData, inquiryType: '0' },
    ];
    //* [1단계] 대기열 재요청이면 기존 queueId 인계 → 멀티 디스패처가 만석 시 순번 유지(단일 공급자는 무시)
    const codef = await this.treatScraper.fetchCertifyMultiple(
      certifyItems,
      dto.queueId,
    );

    //* 벤더 응답에 transactionId 없으면(자체엔진 등 다건) 여기서 1회 생성해 codef에 주입 →
    //*   log_treat(1차)와 tn_order_detail(addOrderDetail)이 같은 분석번호를 공유한다.
    //*   미주입 시 자체엔진은 로그=빈값/주문='a'+uuid 로 단절되어 주문↔호출 추적·모니터 집계가 깨짐.
    codef.result.transactionId =
      codef.result.transactionId || 'a' + newUuid(14);

    //* codef response가 반환된 시점에 codef 결과 로그 적재 (정회원만)
    if (!dto.isB2c && orderInfo) {
      // 정회원: 1차 인증 로그 기록
      this.logTreatService
        .createCertLog(orderInfo.order!.ordId, dto, codef)
        .catch((e) => {
          this.logger.error(e);
        });
    }

    //* 대기열 진입(CF-QUEUE, multi 디스패처): 아직 인증 전이므로 예약수 환불(재요청 시 재차감), odd 미생성.
    //* 클라는 응답의 queueId로 순번 폴링 → READY면 동일 certify를 재요청한다.
    if (codef.result.code === 'CF-QUEUE') {
      if (!dto.isB2c && orderInfo) {
        await this.orderService.refundReserveQuantityWithoutOdd(
          orderInfo.order!.ordId,
        );
      }
      return { queueId: (codef.data as any)?.queueId } as GetCertifyResponseDto;
    }

    //* 알람 및 detail 추가 X / 에러코드 반환값
    if (codef.result.code !== 'CF-03002') {
      //* CODEF 인증 실패: 예약수 환불 (알림톡 플로우만, odd 생성 전이라 단순 -1)
      if (!dto.isB2c && orderInfo) {
        await this.orderService.refundReserveQuantityWithoutOdd(
          orderInfo.order!.ordId,
        );
      }
      //* 1차 라우팅 벤더(dispatcher가 응답에 부착) → filter에서 coocon/hyphen 코드 정확 분류
      const vendor: TreatVendor =
        ((codef as any).provider as TreatVendor) ?? 'codef';
      throw new CodefException({
        type: 'cert',
        codefCode: codef.result.code,
        codefMessage: codef.result.message,
        codefExtraMessage: codef.result.extraMessage,
        vendor,
      });
    }

    //* B2C 플로우: B2C 사용자 생성 및 order_detail 생성
    if (dto.isB2c) {
      //* B2C 사용자 생성 또는 조회
      let bcuId = await this.analyzeRepository.getB2cUserIdByPhone(
        dto.usrPhone,
      );

      if (!bcuId) {
        bcuId = await this.analyzeRepository.createB2cUser({
          name: dto.usrName,
          phone: dto.usrPhone,
          birth: dto.usrBirth,
        });
        this.logger.log(`B2C 사용자 생성: bcuId=${bcuId}`);
      }

      //* B2C order_detail 생성 (정회원과 동일하게 certify 시점에 생성)
      const bodId = await this.analyzeRepository.createB2cOrderDetail(
        bcuId,
        dto.altCode || '',
        //* 분석번호: 벤더 응답에 transactionId 없으면(다건 등) 자체 생성 (주문번호와 동일 방식)
        codef.result.transactionId || 'a' + newUuid(14),
        dto.marketingAgree ? parseInt(dto.marketingAgree) : 0,
      );

      this.logger.log(
        `B2C order_detail 생성: bodId=${bodId}, transactionId=${codef.result.transactionId}`,
      );

      //* B2C: 1차 인증 로그 기록 (B2C order_detail 생성 이후)
      if (dto.altCode) {
        const agency = await this.fcService.getAgencyByAgcCode(dto.altCode);
        this.logger.log(
          `agcCode=${dto.altCode}, agency=${JSON.stringify(agency)}`,
        );

        this.logB2cTreatService
          .createCertLog(
            bodId,
            dto,
            codef,
            agency?.agcId ?? undefined,
            agency?.agcCorporateName ?? undefined,
          )
          .catch((e) => {
            this.logger.error(`B2C 1차 인증 로그 적재 실패: ${e}`);
          });
      }

      const returnDate: GetCertifyResponseDto = {
        orderDetail: { oddId: bodId }, // B2C는 bodId를 oddId로 반환
        data: {
          ...codef.data!,
        },
      };
      return returnDate;
    }

    //* 고객 정보 추가 (알림톡 플로우만)
    const usrId = await this.UserService.enroll({
      memId: orderInfo!.tnMember!.memId,
      usrBirth: dto.usrBirth,
      usrName: dto.usrName,
      usrPhone: dto.usrPhone,
    });

    //* usrId가 주문상세에서 null 허용으로 변경되어 검증 추가
    if (!usrId) {
      throw new BadRequestException(NOT_FOUND_USER);
    }

    //* 주문상세(분석)목록 추가 및 알림톡 [사용] update
    const oddId = await this.orderDetailService.addOrderDetail({
      usrId,
      ordId: orderInfo!.order!.ordId,
      altCode: dto.altCode,
      //* 분석번호: 벤더 응답에 transactionId 없으면(다건 등) 자체 생성 (주문번호 'o'+newUuid 와 동일 방식)
      oddTransactionId: codef.result.transactionId || 'a' + newUuid(14),
      oddState: OrderDetailState.인증요청중,
      //* 1차 인증 라우팅된 공급자를 생성 시점부터 기록 → 인증실패도 업체 귀속 (멀티 디스패처)
      oddProvider: providerFromTag((codef.data as any)?.jti) ?? undefined,
    });
    const returnDate: GetCertifyResponseDto = {
      orderDetail: { oddId: oddId! },
      data: {
        ...codef.data!,
      },
    };
    await this.saveCertifyResume(dto.altCode, returnDate);
    if (dto.loginType === '5') {
      const issuedAt = Number(returnDate.data?.twoWayTimestamp) || Date.now();
      void this.alimtalkService
        .scheduleReminder(
          'treat',
          orderInfo!.alimtalk.altId,
          issuedAt,
          this.certifyOtpExpiresAt(returnDate) ??
            issuedAt + this.certifyOtpTtlMs,
        )
        .catch((error) => {
          this.logger.error(`진료분석 리마인드 예약 실패: ${error}`);
        });
    }
    return returnDate;
  } finally {
    if (certifyClaimHeartbeat) clearInterval(certifyClaimHeartbeat);
    if (!dto.isB2c) {
      await this.releaseCertifyClaim(dto.altCode, certifyClaimToken);
    }
  }
  }

  /**
   * 심평원 개인인증확인 및 진료정보요청
   * 성공 : 알림톡 사용완료 O | 주문상세 update 상태값(자료수집중) | FC 알람 X
   * 실패(재시도가능) : 알림톡 사용완료 O | 주문상세 update 상태값(추가인증요청중) | FC 알람 X
   * 실패(사용불가) : 알림톡 사용완료 O | 주문상세 update 상태값(인증싶패) | FC 알람 O
   * */
  async treat(dto: TreatDto) {
    //* 심평원 기관 사용중지 여부 우선 확인 (관리자 설정)
    await this.institutionBlockService.assertNotBlocked(InstitutionCode.심평원);

    await this.alimtalkService.validateTreatReminder(dto.altCode, dto.oddId, dto.reminderAttempt);

    // 주문상세 정보 조회
    const orderDetail = await this.orderDetailService.getOrderDetail(dto.oddId);

    //* 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: orderDetail.member?.memLoginId
        ? hashWithSHA256(orderDetail.member.memLoginId)
        : '', //동일계정으로 구분값이 필요할 경우 암호화 해서 넣기
      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 carEnabledTreat = !!carReservation;
    const treatItems: TreatmentMultipleItemDto[] = [
      { ...codefData, inquiryType: '0' },
      ...(carEnabledTreat
        ? [
            {
              ...codefData,
              organization: '0020', //공통값(원본 carData와 동일)
              type: '1', //민감상병표기(원본 carData와 동일)
              id: orderDetail.member?.memLoginId
                ? hashWithSHA256(orderDetail.member.memLoginId)
                : '', //동일계정 구분값(원본 carData와 동일 소스)
              inquiryType: '1',
            },
          ]
        : []),
    ];
    let res;
    try {
      res = await this.treatScraper.fetchTreatmentMultiple(
        treatItems,
        dto.oddId,
      );
    } finally {
      await this.treatCarCanaryService.release(carReservation);
    }

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

    //* 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,
      });
    }

    // //*  임시데이터 저장
    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}`,
          );
        });
    }

    await this.orderDetailService.updateOrderDetail({
      oddId: dto.oddId,
      oddState: OrderDetailState.자료수집완료,
    });

    return;
  }

  async backgroundTreat(dto: TreatCompareDto) {
    //* 심평원 기관 사용중지 여부 우선 확인 (관리자 설정)
    await this.institutionBlockService.assertNotBlocked(InstitutionCode.심평원);

    //* B2C 플로우: 진료정보 수집 후 고객사 API로 분석 처리
    if (dto.isB2c) {
      this.logger.log('B2C 플로우: 백그라운드 진료정보 수집 및 분석 시작');

      //* eventEmitter를 통해 비동기적 작업 분리 (정회원과 동일)
      this.eventEmitter.emit('b2cTreatCompare', dto);
      return;
    }

    await this.alimtalkService.validateTreatReminder(dto.altCode, dto.oddId, dto.reminderAttempt);

    // 주문상세 정보 조회
    const orderDetail = await this.orderDetailService.getOrderDetail(dto.oddId);

    //* SMS 제출과 OTP 만료 정리 중 정확히 한쪽만 DB 행을 선점한다.
    const claimed = await this.orderService.claimTreatCollection(dto.oddId);
    if (!claimed) {
      this.logger.warn(`진료정보 수집 중복/만료 차단 oddId=${dto.oddId}`);
      return;
    }
    await this.clearCertifyResume(dto.altCode);

    //* eventEmitter를 통해 비동기적 작업 분리
    this.eventEmitter.emit('treatCompare', orderDetail, dto);
    return;
  }
  //---------------------------------------------------//

  /**
   * 심평원 진료정보 분석요청
   * 성공 : 알림톡 사용완료 O | 주문상세 update 상태값(분석중->분석완료) | FC 알람 O
   * 실패 : 알림톡 사용완료 O | 주문상세 update 상태값(분석중->분석완료) | FC 알람 O
   * */
  /**
   * 유저 재시도(분석실패/자료수집완료 → 재분석) 진입점.
   * 관리자 재분석과 동일하게 [기존 분석결과 삭제 + 잔여량 검증 후 예약(차감)] 을 먼저 수행한 뒤 분석.
   * - 유저는 항상 차감(charge=true): 분석완료 시 사용수 +1, 재시도 실패 시 예약 환불.
   * - 재시도 불가 상태(분석중/완료 등)면 ConflictException 으로 차단(중복분석·중복차감 방지).
   * - 잔여량 초과면 prepareReAnalyze 내부에서 ORDER_NOT_AVAILABLE(400).
   *   (이전에는 기존 분석결과를 안 지우고 재분석 → tn_analysis_basic PK 중복 → 404 발생)
   */
  async userReAnalyze(dto: CompareDto) {
    const { oddId } = dto;
    const orderDetail = await this.orderDetailService.getOrderDetail(oddId);
    //* getOrderDetail 은 GET_STATUS 로 라벨을 반환(코드 아님).
    const state = orderDetail.orderDetail.oddState;

    //* 진행/완료 건 재시도 차단(중복분석·중복차감 방지). 분석실패/자료수집완료 등은 통과.
    if (state === '분석완료' || state === '자료분석중') {
      throw new ConflictException(ALREADY_ANALYSIS);
    }

    //* 재분석 가드: 수집된 원본이 없으면 차감(prepareReAnalyze) 전에 차단 → 헛차감·가짜완료 방지.
    //*   수집 실패건(인증오류·처리중오류·타임아웃 등)은 재분석할 원본이 없고, 재수집은 고객 재인증이
    //*   필요해 재시도로 복구 불가하므로 여기서 막는다. (compare에도 동일 가드 — 방어 이중화)
    if (!(await this.treatRepository.hasCollectedData(oddId))) {
      throw new ConflictException(NO_COLLECTED_DATA);
    }

    //* 기존 분석결과 삭제 + 잔여량 검증 후 예약(차감). PK 중복 방지 + 한도 초과 차단.
    await this.orderDetailService.prepareReAnalyze(
      oddId,
      orderDetail.order!.ordId,
      true,
    );

    //* 분석 실행(분석완료 시 addQuantity 로 차감 확정, 실패 시 예약 환불).
    return await this.compare(dto);
  }

  async compare(dto: CompareDto) {
    //* 심평원 기관 사용중지 여부 우선 확인 (관리자 설정)
    await this.institutionBlockService.assertNotBlocked(InstitutionCode.심평원);

    const { oddId } = dto;
    const orderDetail = await this.orderDetailService.getOrderDetail(oddId);
    if (
      orderDetail.orderDetail.oddState === '분석중' ||
      orderDetail.orderDetail.oddState === '분석완료'
    )
      throw new NotFoundException(ALREADY_ANALYSIS);

    //* 재분석 가드: 수집된 진료 원본(tt_treat_basic)이 없으면 재분석 불가 → '가짜완료' 방지.
    //*   인증/수집 실패건(인증오류·처리중오류·타임아웃 등)은 분석할 원본이 없는데,
    //*   재분석을 돌리면 createAnalyze가 무에러로 빈 결과를 만들어 '분석완료(ODD006)'로 오인된다.
    //*   원본 수집은 고객 본인인증이 다시 필요해 재시도로 복구 불가하므로 진입 자체를 차단한다.
    if (!(await this.treatRepository.hasCollectedData(oddId))) {
      this.logger.warn(`재분석 차단 — 수집 데이터 없음: oddId=${oddId}`);
      throw new NotFoundException(NO_COLLECTED_DATA);
    }

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

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

      //* 주문상세이력상태변경(분석실패) - 상태 변경을 먼저 실행
      await this.orderDetailService.updateOrderDetail({
        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=${oddId}, error=${alarmError?.message || alarmError}`,
          );
        });

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

    //* 분석완료 — 사용 카운터와 완료 상태·시각을 **한 트랜잭션**에서 쓴다(M2 D4).
    //*  treat.event-handler 의 같은 자리와 같은 이유다: 예전에는 그 둘 사이에 가드 없는
    //*  addAlarm 이 있어, 알림이 던지면 카운터만 오른 행이 남았다.
    await this.orderService.addQuantity(
      orderDetail.order!.ordId,
      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 errorCertify(dto: CodefResult) {
    if (
      dto.code === 'CF-13000' ||
      dto.code === 'CF-12837' ||
      dto.code === 'CF-12835'
    ) {
      //* 잘못된 개인정보입니다.
      throw new CodefException({
        type: 'cert',
        codefCode: dto.code,
        codefMessage: dto.message,
        codefExtraMessage: dto.extraMessage,
      });

      //* 앱을 설치하지 않았거나 인증서를 발급받지 않으셨습니다.
    } else if (dto.code === 'CF-12871') {
      throw new CodefException(APP_INSTALLATION_REQUIRED);
    } else if (dto.code === 'CF-12100') {
      throw new CodefException(INSTITUTIONAL_ERROR);
    } else {
      this.logger.log(dto);
      throw new CodefException({
        code: dto.code,
        message: dto.message,
      });
    }
  }

  async errorTreat(dto: CodefTreatResultDto) {
    if (dto.code === 'CF-03002') {
      //* CF-03002의 경우 인증을 하지 않고 인증확인을 했을 경우(재사용가능)
      await this.orderDetailService.updateOrderDetail({
        oddId: dto.oddId,
        oddState: OrderDetailState.추가인증요청중,
      });
    } else {
      await this.alarmService.addAlarm({
        memId: dto.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'),
      });
      //* 인증실패: 예약수 환불
      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,
    });
  }

  //*------------------------ Schedule ------------------------*//

  async deleteTreat() {
    return await this.treatRepository.deleteExpiredData();
  }

  async deleteB2cTreat() {
    return await this.treatRepository.deleteB2cExpiredData();
  }

  // async deleteTreat(oddId: number) {
  //   return await this.treatRepository.delete(oddId);
  // }

  //----------------------------- OnEvent -----------------------------//
  // * 기존이벤트처리
  // this.eventEmitter.emit(
  //   'order.created',
  //   new OrderCreatedEvent(dto.oddId, codefData),
  // );

  // @OnEvent('order.created')
  // async handleOrderCreated(payload: OrderCreatedEvent) {
  //   const orderDetail = await this.orderDetailService.getOrderDetail(
  //     payload.oddId,
  //   );
  //   const alarmInput = {
  //     memId: orderDetail.member!.memId,
  //     usrId: orderDetail.user!.usrId,
  //     oddId: payload.oddId,
  //   };
  //   try {
  //     const codef = await this.codefService.fetchTreatmentInfo(
  //       alarmInput,
  //       payload.treat,
  //     );

  //     //*  임시데이터 저장
  //     const addTreat = {
  //       resBasicTreatList: codef.data!.resBasicTreatList!,
  //       resDetailTreatList: codef.data!.resDetailTreatList!,
  //       resPrescribeDrugList: codef.data!.resPrescribeDrugList!,
  //     };
  //     await this.addTreat(payload.oddId, addTreat);
  //   } catch {
  //     await this.alarmService.addAlarm({
  //       memId: alarmInput.memId,
  //       usrId: alarmInput.usrId,
  //       almState: 'ODD003',
  //       almMessage: '자료수집 중 에러가 발생했습니다.',
  //     });
  //     //* 주문상세이력상태변경(요청에러)
  //     await this.orderDetailService.updateOrderDetailStatus({
  //       oddId: alarmInput.oddId,
  //       oddState: OrderDetailState.ODD003, //요청에러
  //       oddFailedAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
  //     });
  //     return;
  //   }
  //   //* 분석요청하기
  //   await this.compare({ oddId: payload.oddId }, false).catch(async (e) => {
  //     await this.alarmService.addAlarm({
  //       memId: alarmInput.memId,
  //       usrId: alarmInput.usrId,
  //       almState: 'ODD004',
  //       almMessage: '분석에 실패했습니다.',
  //     });
  //     //* 주문상세이력상태변경(분석실패)
  //     await this.orderDetailService.updateOrderDetailStatus({
  //       oddId: alarmInput.oddId,
  //       oddState: OrderDetailState.ODD004, //분석실패
  //       oddFailedAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
  //     });
  //   });
  //   return;
  // }

  // @OnEvent('reanalyze.created')
  // async handleReanalyzeCreated(payload: ReanalyzeEvent) {
  //   const orderDetail = await this.orderDetailService.getOrderDetail(
  //     payload.oddId,
  //   );
  //   //* 주문상세이력상태변경(분석요청)
  //   await this.orderDetailService.updateOrderDetailStatus({
  //     oddId: payload.oddId,
  //     oddState: OrderDetailState.ODD006, //재분석요청일시는 updated_at
  //   });

  //   await this.drizzle
  //     .transaction(async (tx) => {
  //       await tx.execute(sql`CALL PROC_COMPARE_BASIC(${payload.oddId})`);
  //       await tx.execute(sql`CALL PROC_COMPARE_DETAIL(${payload.oddId})`);
  //       await tx.execute(sql`CALL PROC_COMPARE_PRESCRIBE(${payload.oddId})`);
  //     })
  //     .catch(async (e) => {
  //       await this.alarmService.addAlarm({
  //         memId: orderDetail.member!.memId,
  //         usrId: orderDetail.user!.usrId!,
  //         almState: 'ODD004',
  //         almMessage: '분석에 실패했습니다.',
  //       });

  //       //* 주문상세이력상태변경(분석실패)
  //       await this.orderDetailService.updateOrderDetailStatus({
  //         oddId: payload.oddId,
  //         oddState: OrderDetailState.ODD004, //분석실패
  //         oddFailedAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
  //       });
  //       throw '분석에 실패했습니다.';
  //     });

  //   //* 분석성공 시
  //   await this.alarmService.addAlarm({
  //     memId: orderDetail.member!.memId,
  //     usrId: orderDetail.user!.usrId,
  //     almState: 'ODD005',
  //     almMessage: '분석이 완료되었습니다.',
  //     almRedirectId: payload.oddId,
  //   });
  //   //* 주문상세이력상태변경(분석완료)
  //   await this.orderDetailService.updateOrderDetailStatus({
  //     oddId: payload.oddId,
  //     oddState: OrderDetailState.ODD005, //분석완료
  //     oddCompletedAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
  //   });
  //   return;
  // }
}
