import { WebClientService } from '@lib/webclient/src';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { plainToClass } from 'class-transformer';
import {
  CertifyResponseDto,
  Treatment,
  TreatmentMultipleResponseDto,
} from '@lib/codef/src/codef.dto';
import { TreatmentMultipleItemDto } from '@lib/codef/src/codef.interface';
import { TreatScraper } from '@lib/coocon/src/coocon.interface';
import { TreatRepository } from '@api/modules/treat/treat.repository';
import { DEFAULT_OCTOVER_PUBLIC_KEY, octoverEncrypt } from './octover.crypto';
import {
  mapOctoverCarToTreatment,
  mapOctoverToTreatment,
} from './octover.mapper';
import {
  AB001Node,
  LOGIN_LEVEL_TO_OCTOVER,
  OCTOVER_OK,
  OCTOVER_COLLECT_OK,
  OctoverMethod,
  OctoverResponse,
  OctoverSession,
  buildEnvelope,
  decodeOctoverSession,
  encodeOctoverSession,
  envelope,
} from './octover.types';

/**
 * 옥토버(Dozn Octover) 진료내역 공급자 — CODEF/쿠콘과 동일 TreatScraper 구현.
 * TreatService가 provider 무관하게 호출(treatProvider='octover').
 *
 * 인증 2종 (base.loginType):
 *  - '2' 휴대폰(문자): phoneVerify(1차) → phoneVerifyResult(SMS코드)+AB001(체인)
 *  - 그 외 간편: simpleAuthLogin(1차) → simpleAuthLoginResult+AB001(체인)
 *
 * ⚠️ 로그인결과와 AB001 은 한 요청 methods 에 체인해야 함(분리 시 1000101). 레퍼런스 실호출 검증.
 * 2-way 세션(encInput)은 CODEF jti 자리에 base64 인코딩하여 클라 계약 무변경 왕복.
 * 범위: 건강보험 진료내역(보험종별 0)만. PII 는 RSA-OAEP 암호화.
 */
@Injectable()
export class OctoverService implements TreatScraper {
  private logger = new Logger(OctoverService.name);

  constructor(
    private config: ConfigService,
    private http: WebClientService,
    private treatRepository: TreatRepository,
  ) {}

  /** 1차: 옥토버 본인인증 요청 → CODEF CertifyResponseDto(CF-03002) 정규화 */
  async fetchCertifyMultiple(
    items: TreatmentMultipleItemDto[],
  ): Promise<CertifyResponseDto> {
    const base = items?.[0];
    if (!base) throw new Error('OCTOVER certify: 빈 요청');

    const isMobile = base.loginType === '2';
    const enc = (v: string) => octoverEncrypt(v, this.pubKey());

    let resp: OctoverResponse;
    let method: OctoverMethod;
    if (isMobile) {
      method = 'phoneVerify';
      resp = await this.post(
        envelope(this.apiKey(), method, {
          telecomType: base.telecom ?? '',
          name: base.userName,
          juminNum: enc(base.identity),
          phoneNum: enc(base.phoneNo),
        }),
      );
    } else {
      method = 'simpleAuthLogin';
      resp = await this.post(
        envelope(this.apiKey(), method, {
          useAnyId: '',
          anyIdRegister: '',
          loginType: LOGIN_LEVEL_TO_OCTOVER[base.loginTypeLevel] ?? '2',
          name: base.userName,
          juminNum: enc(base.identity),
          phoneNum: enc(base.phoneNo),
          telecomType: base.telecom ?? '',
        }),
      );
    }

    const node = this.node(resp, method);
    if (!node) {
      return this.failCertify(resp, '본인인증 실패');
    }
    const encInput = String((node as any).encInput ?? '');
    const session: OctoverSession = {
      encInput,
      authType: isMobile ? 'mobile' : 'simple',
    };

    return plainToClass(CertifyResponseDto, {
      result: {
        code: 'CF-03002',
        message: '',
        extraMessage: '',
        transactionId: '',
      },
      data: {
        continue2Way: 1,
        method: isMobile ? 'sms' : 'simpleAuth',
        jobIndex: 0,
        threadIndex: 0,
        jti: encodeOctoverSession(session),
        twoWayTimestamp: Date.now(),
        extraInfo: {
          reqSecureNo: '',
          reqSecureNoRefresh: '',
          commSimpleAuth: '',
        },
      },
    });
  }

  /**
   * 2차: (로그인결과 + AB001) 체인 호출 → TreatmentMultipleResponseDto(data=[건강, 자동차]).
   *  - 건강(insureType=0)은 필수. 실패 시 전체 실패.
   *  - 건강 호출에서 connReq로 세션을 유지하고, 응답 header로 자동차 AB001을 이어서 호출한다.
   *  - 자동차 실패는 건강 성공을 뒤집지 않는 best-effort 정책이다.
   */
  async fetchTreatmentMultiple(
    items: TreatmentMultipleItemDto[],
    oddId: number,
  ): Promise<TreatmentMultipleResponseDto> {
    const base = items?.[0];
    if (!base) throw new Error('OCTOVER treat: 빈 요청');

    const session = decodeOctoverSession(base.twoWayInfo?.jti);
    if (!session) {
      this.logger.error('OCTOVER treat: 세션(jti) 복원 실패');
      return this.failResult('CF-12000', '세션 복원 실패');
    }

    const loginMethod: OctoverMethod =
      session.authType === 'mobile'
        ? 'phoneVerifyResult'
        : 'simpleAuthLoginResult';
    const loginInput =
      session.authType === 'mobile'
        ? { certificationNum: base.smsAuthNo ?? '', encInput: session.encInput }
        : { encInput: session.encInput };

    const wantsCar = items.some((i) => i.inquiryType === '1');

    //* 건강보험(insureType=0) — 필수. 자동차가 뒤따를 때만 연결 세션을 요청한다.
    const basic = await this.collectAB001(
      loginMethod,
      loginInput,
      base,
      '0',
      oddId,
      'BASIC',
      wantsCar ? '' : undefined,
    );
    if (!basic.ok) return basic.fail;
    const data: Treatment[] = [basic.treatment];

    //* 자동차보험(insureType=1) — 건강 응답의 연결 header로 AB001만 이어서 호출.
    if (wantsCar) {
      if (!basic.connHeader) {
        this.logger.warn(
          `옥토버 자동차 세션 header 없음(건강 성공 유지): oddId=${oddId}`,
        );
      } else {
        try {
          const car = await this.collectAB001(
            loginMethod,
            loginInput,
            base,
            '1',
            oddId,
            'CAR',
            basic.connHeader,
          );
          if (!car.ok) {
            this.logger.warn(
              `옥토버 자동차(AB001 insureType=1) 수집 실패(건강 성공 유지): oddId=${oddId} code=${car.fail.result?.code}`,
            );
          } else {
            data[1] = car.treatment;
          }
        } catch (e: any) {
          this.logger.warn(
            `옥토버 자동차 호출 실패(건강 성공 유지): oddId=${oddId} ${e?.message || e}`,
          );
        }
      }
    }

    return plainToClass(TreatmentMultipleResponseDto, {
      result: {
        code: 'CF-00000',
        message: '정상처리되었습니다.',
        extraMessage: '',
        transactionId: '',
      },
      data,
    });
  }

  /**
   * 로그인결과 + AB001(insureType) 한 요청 체인 호출 → 매핑된 Treatment 반환.
   * 건강(insureType='0')/자동차('1') 공통. 실패 시 ok=false + failResult.
   */
  private async collectAB001(
    loginMethod: OctoverMethod,
    loginInput: Record<string, unknown>,
    base: TreatmentMultipleItemDto,
    insureType: string,
    oddId: number,
    saveType: 'BASIC' | 'CAR',
    connHeader?: string,
  ): Promise<
    | { ok: true; treatment: Treatment; connHeader?: string }
    | { ok: false; fail: TreatmentMultipleResponseDto }
  > {
    //* AB001 요청 파라미터(건강/자동차 공통).
    const ab001 = {
      insureType, //* '0' 건강보험 / '1' 자동차보험(기본·세부진료만, 처방 없음)
      diseaseGubun: '1', //* 주상병코드 포함
      sensitiveGubun: base.type === '1' ? '1' : '0',
      inqrDtStrt: base.startDate,
      inqrDtEnd: base.endDate,
    };
    const isCar = saveType === 'CAR';
    const inputs: Record<string, unknown> = isCar
      ? { AB001: ab001 }
      : { [loginMethod]: loginInput, AB001: ab001 };
    const methods: OctoverMethod[] = isCar ? ['AB001'] : [loginMethod, 'AB001'];

    const resp = await this.post(
      buildEnvelope(this.apiKey(), inputs, methods, connHeader),
    );

    //* 첫 거래만 로그인 결과를 검증한다. 자동차는 connHeader 세션으로 AB001만 호출한다.
    if (!isCar && !this.node(resp, loginMethod)) {
      return {
        ok: false,
        fail: this.failResult(
          resp.out?.code || 'CF-99999',
          resp.out?.msg || '인증 확정 실패',
        ),
      };
    }
    //* AB001(수집)은 OCTOVER_OK(1000200) 외에 시트 '정상등급' 코드도 성공으로 인정한다.
    //*   1000204 내용없음(자료없음)/1000206 부분성공/1000210 마지막거래. node()는 본인인증 등과
    //*   공용이라 엄격 OCTOVER_OK 유지하고, 수집 노드만 OCTOVER_COLLECT_OK 로 별도 판정.
    //*   자료없음이면 data 가 비어도 매퍼가 빈 Treatment 를 반환 → 빈 분석 성공(환불 X).
    const abNode = resp.out?.data?.AB001;
    if (!abNode || !OCTOVER_COLLECT_OK.has(abNode.code)) {
      this.logger.warn(
        `옥토버 AB001 실패(insureType=${insureType}): code=${abNode?.code ?? resp.out?.code} msg=${abNode?.msg ?? resp.out?.msg}`,
      );
      return {
        ok: false,
        fail: this.failResult(
          abNode?.code || resp.out?.code || 'CF-99999',
          abNode?.msg || resp.out?.msg || '수집 실패',
        ),
      };
    }
    const ab = abNode as unknown as AB001Node;

    //* 자동차(CAR)는 진료일수/자동차보험총진료비를 자동차 전용 필드로 채우는 매퍼 사용
    const treatment =
      saveType === 'CAR'
        ? mapOctoverCarToTreatment(ab.data)
        : mapOctoverToTreatment(ab.data);

    this.treatRepository
      .saveTest(
        oddId,
        JSON.stringify({ result: { code: 'CF-00000' }, data: ab.data }),
        saveType,
      )
      ?.catch?.((e: any) =>
        this.logger.error(
          `saveTest 실패(${saveType}/octover): oddId=${oddId} ${e}`,
        ),
      );

    const nextHeader = resp.out?.header;
    return {
      ok: true,
      treatment,
      connHeader:
        typeof nextHeader === 'string' && nextHeader ? nextHeader : undefined,
    };
  }

  //* ───── 내부 ─────

  private apiKey(): string {
    const k = this.config.get<string>('octover.apiKey');
    if (!k) throw new Error('octover.apiKey 미설정 (OCTOVER_API_KEY)');
    return k;
  }

  private pubKey(): string {
    return (
      this.config.get<string>('octover.rsaPublicKey') ||
      DEFAULT_OCTOVER_PUBLIC_KEY
    );
  }

  private async post(body: unknown): Promise<OctoverResponse> {
    const url = this.config.get<string>('octover.baseUrl');
    if (!url) throw new Error('octover.baseUrl 미설정 (OCTOVER_BASE_URL)');
    return this.http
      .post<OctoverResponse>(url, body, {
        timeout: 300000,
        headers: { 'Content-Type': 'application/json; charset=UTF-8' },
      })
      .catch((e) => {
        this.logger.error(`옥토버 호출 실패: ${e?.message || e}`);
        throw e;
      });
  }

  //* 메서드 노드 추출 + 성공코드 검증. 실패면 null.
  private node(
    resp: OctoverResponse,
    method: OctoverMethod,
  ): Record<string, unknown> | null {
    const n = resp.out?.data?.[method];
    if (!n || n.code !== OCTOVER_OK) {
      this.logger.warn(
        `옥토버 ${method} 실패: code=${n?.code ?? resp.out?.code} msg=${n?.msg ?? resp.out?.msg}`,
      );
      return null;
    }
    return n;
  }

  private failCertify(
    resp: OctoverResponse,
    fallback: string,
  ): CertifyResponseDto {
    return plainToClass(CertifyResponseDto, {
      result: {
        code: resp.out?.code || 'CF-99999',
        message: resp.out?.msg || fallback,
        extraMessage: '',
        transactionId: '',
      },
      data: undefined,
    });
  }

  private failResult(
    code: string,
    message?: string,
  ): TreatmentMultipleResponseDto {
    return plainToClass(TreatmentMultipleResponseDto, {
      result: {
        code,
        message: message ?? '',
        extraMessage: '',
        transactionId: '',
      },
      data: [],
    });
  }
}
