import { WebClientService } from '@lib/webclient/src';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as crypto from 'crypto';
import {
  CooconCommon,
  CooconEnvelope,
  CooconGatewayResult,
  CooconJobRequest,
  CooconSession,
} from './coocon.interface';

/**
 * 쿠콘 스마트 스크래핑 게이트웨이 호출 + 세션/거래번호 관리.
 *
 * ⚠️ 확정 필요 (쿠콘 dev Auth_key 확보 후 실호출로 검증):
 *  - 요청 봉투 구조: 본 구현은 {Common, InputList:[{Module,Class,Job,Input}]} 가정 (가이드 2.1 공통입력부 기준).
 *    개별 Job 샘플(2.3)은 봉투를 생략해 표기되어 있어, 실제 래핑 형태를 dev 응답으로 최종 확인할 것.
 *  - 응답 봉투: {Common:{Result_cd,SAS_SessionID...}, ResultList:{Output:{ErrorCode,ErrorMessage,Result,CallBackfunc,req}}} 가정.
 *    본 파서는 Output 위치(ResultList.Output / ResultList[0].Output / Output / ResultList)를 방어적으로 탐색한다.
 *  - AES: 간편인증은 smart_gateway_aes.jsp(암호화) 권장. useAes=false(평문)이 기본이며, AES256 세부(모드/IV)는 [참조2] 확인 후 확정.
 */
@Injectable()
export class CooconAuthService {
  private logger = new Logger(CooconAuthService.name);

  constructor(
    private configService: ConfigService,
    private webClientService: WebClientService,
  ) {}

  /** 7자리 거래일련번호 채번 */
  genTrSeq(): string {
    const n = Math.floor(Math.random() * 10_000_000);
    return String(n).padStart(7, '0');
  }

  /**
   * 게이트웨이 1회 호출.
   * @param job      개별부 Job (예: '본인인증' | 'OTP' | '내진료정보열람')
   * @param input    개별부 Input
   * @param session  세션 유지가 필요한 2회차 이후 호출 시 1회차 응답 세션
   * @param dsnc     'S' 세션유지 / 'E' 로그아웃(마지막 호출)
   */
  async call(
    job: string,
    input: Record<string, any>,
    session?: CooconSession | null,
    dsnc: 'S' | 'E' = 'S',
  ): Promise<CooconGatewayResult> {
    const url = this.configService.get<string>('coocon.gwUrl');
    if (!url) {
      throw new Error('coocon.gwUrl 미설정 — 쿠콘 게이트웨이 URL이 필요합니다.');
    }
    const authKey = this.configService.get<string>('coocon.authKey') ?? '';
    const trSeq = session?.trSeq ?? this.genTrSeq();

    const common: CooconCommon = {
      Auth_key: authKey,
      Tr_seq: trSeq,
      Gw_sys_no: session?.gwSysNo ?? '',
      Ss_sys_no: session?.ssSysNo ?? '',
      Auth_req: '',
      Thread_no: session?.threadNo ?? '',
      Lbs_no: session?.lbsNo ?? '',
    };

    const jobReq: CooconJobRequest = {
      Module: 'hira',
      Class: '민원신청조회',
      Job: job,
      Input: input,
    };

    //* 봉투: Dsnc/SAS_SessionID/TimeoutRuntime는 Common 밖 최상위 (공식 포스트맨 샘플 기준)
    const envelope: CooconEnvelope = {
      Dsnc: dsnc,
      SAS_SessionID: session?.sasSessionId ?? '',
      //* Job별 구간 타임아웃 차등: 인증/로그아웃 등 단계는 120초(쿠콘 권고, 통신사 지연 여유 확대),
      //*  수집(내진료정보열람 등 무거운 단계)은 300초로 충분히 줘 정상 수집이 끊기지 않게.
      TimeoutStep: ['본인인증', '휴대폰인증_1', '휴대폰인증_2', 'OTP', '로그아웃'].includes(job)
        ? '120'
        : '300',
      TimeoutRuntime: '300', //* 스크래핑 세션 타임아웃 300초 — 8000F101 오류 완화 (쿠콘 권고, 디폴트 150초)
      Common: common,
      InputList: [jobReq],
    };

    const body = this.maybeEncrypt(envelope);

    const start = Date.now();
    const raw = await this.webClientService
      .post<any>(url, body, {
        headers: { 'Content-Type': 'application/json;charset=UTF-8' },
        //* 쿠콘 TimeoutRuntime(300초)보다 30초 길게. 항상 쿠콘이 먼저 8000F101로 끊게 하여
        //*  우리(클라이언트)가 끊는 일을 없앤다 → 쿠콘은 '클라이언트가 끊은 미완료'만 과금하므로
        //*  우리가 안 끊으면 미완료 과금을 피한다. (쿠콘 회신 기준)
        timeout: 330000,
      })
      .catch((e) => {
        this.logger.warn(
          `[coocon job-elapsed] job=${job} elapsed=${Date.now() - start}ms result=fail trSeq=${trSeq}`,
        );
        this.logger.error(e);
        //* 타임아웃/통신실패에도 trSeq(쿠콘 거래일련번호)를 예외에 실어, 상위에서 거래번호로 남긴다.
        //*  (예전엔 trSeq가 예외와 함께 소실돼 로그에 jti 세션블롭이 대신 저장되던 문제.)
        const err: any = new Error(`COOCON_REQUEST_FAILED(job=${job})`);
        err.vendorTx = trSeq;
        //* 원본 axios 에러코드/메시지 보존 (ECONNABORTED 등) — 상위에서 '조회 타임아웃' 판정용.
        err.origCode = e?.code;
        err.origMessage = e?.message;
        throw err;
      });

    this.logger.log(
      `[coocon job-elapsed] job=${job} elapsed=${Date.now() - start}ms result=ok trSeq=${trSeq}`,
    );
    const decoded = this.maybeDecrypt(raw);
    return this.normalize(decoded, trSeq);
  }

  /**
   * 바이패스: 외부(보상닥터)가 보낸 쿠콘 봉투를 우리 인증키만 주입해 쿠콘 게이트웨이로 그대로 전달.
   *  Auth_key는 서버 보유값으로 덮어써 외부 미노출. 응답은 쿠콘 원형 그대로 반환(정규화 안 함).
   */
  async passthrough(envelope: any): Promise<any> {
    const url = this.configService.get<string>('coocon.gwUrl');
    if (!url) {
      throw new Error('coocon.gwUrl 미설정 — 쿠콘 게이트웨이 URL이 필요합니다.');
    }
    const authKey = this.configService.get<string>('coocon.authKey') ?? '';
    const env = envelope ?? {};
    //* 우리가 손대는 건 Auth_key(키 미노출)뿐. TimeoutStep/TimeoutRuntime은 호출측(보상닥터) 봉투 그대로 둔다.
    env.Common = { ...(env.Common ?? {}), Auth_key: authKey };
    const body = this.maybeEncrypt(env);
    //* axios만 호출측 TimeoutRuntime보다 30초 길게(우리가 먼저 끊으면 과금) — 미입력 시 쿠콘 디폴트 150초 기준.
    const rt = Number(env.TimeoutRuntime) || 150;
    const axiosMs = Math.max(330000, (rt + 30) * 1000);
    const raw = await this.webClientService.post<any>(url, body, {
      headers: { 'Content-Type': 'application/json;charset=UTF-8' },
      timeout: axiosMs,
    });
    return this.maybeDecrypt(raw);
  }

  /** 응답에서 Output/Common을 방어적으로 추출하여 정규화 */
  private normalize(decoded: any, trSeq: string): CooconGatewayResult {
    const common = decoded?.Common ?? {};
    //* Output 위치 후보를 순차 탐색 (봉투 형태 미확정 대비)
    const rl = decoded?.ResultList;
    const output =
      rl?.Output ??
      (Array.isArray(rl) ? rl[0]?.Output : undefined) ??
      decoded?.Output ??
      rl ??
      decoded;

    const result = output?.Result ?? {};
    const session: CooconSession = {
      trSeq: common?.Tr_seq ?? trSeq,
      //* 실응답상 SAS_SessionID는 최상위(decoded.SAS_SessionID). Common 안/오탈자(SAS_SessinID)도 방어.
      sasSessionId:
        decoded?.SAS_SessionID ??
        common?.SAS_SessionID ??
        decoded?.SAS_SessinID ??
        common?.SAS_SessinID ??
        '',
      ssSysNo: common?.Ss_sys_no ?? '',
      threadNo: common?.Thread_no ?? '',
      lbsNo: common?.Lbs_no ?? '',
      gwSysNo: common?.Gw_sys_no ?? '',
      otpReq: result?.req?.Input?.req,
    };

    return {
      errorCode: output?.ErrorCode ?? common?.Result_cd ?? '',
      errorMessage: output?.ErrorMessage ?? common?.Result_mg ?? '',
      result,
      callBackFunc: result?.CallBackfunc,
      otpReq: result?.req?.Input?.req,
      session,
    };
  }

  //* AES256-CBC 적용. useAes=false(기본)면 평문 통과.
  //* AES 시 본문 전체 JSON을 암호화→base64하여 { Auth_key, Enc_data } 로 전송 ([참조2]).
  private maybeEncrypt(envelope: CooconEnvelope): any {
    if (!this.configService.get<boolean>('coocon.useAes')) return envelope;
    const aes = this.aesKeyIv();
    if (!aes) {
      this.logger.warn('coocon.useAes=true 이나 AES 키/IV 미설정 — 평문 폴백');
      return envelope;
    }
    const cipher = crypto.createCipheriv('aes-256-cbc', aes.key, aes.iv);
    const enc = Buffer.concat([
      cipher.update(JSON.stringify(envelope), 'utf8'),
      cipher.final(),
    ]).toString('base64');
    return { Auth_key: envelope.Common.Auth_key, Enc_data: enc };
  }

  private maybeDecrypt(raw: any): any {
    const useAes = this.configService.get<boolean>('coocon.useAes');
    //* AES 응답: { Result: <base64 AES256-CBC> }
    if (
      useAes &&
      raw &&
      typeof raw === 'object' &&
      typeof raw.Result === 'string'
    ) {
      const aes = this.aesKeyIv();
      if (aes) {
        try {
          const decipher = crypto.createDecipheriv(
            'aes-256-cbc',
            aes.key,
            aes.iv,
          );
          const dec = Buffer.concat([
            decipher.update(Buffer.from(raw.Result, 'base64')),
            decipher.final(),
          ]).toString('utf8');
          return JSON.parse(dec);
        } catch (e) {
          this.logger.error(`쿠콘 AES 복호화 실패: ${e}`);
          return raw;
        }
      }
    }
    //* 평문 JSON이면 axios가 객체로 파싱. 문자열로 오면 파싱 시도.
    if (typeof raw === 'string') {
      try {
        return JSON.parse(raw);
      } catch {
        this.logger.warn('쿠콘 응답이 비-JSON 문자열 — 원문 반환');
        return { _raw: raw };
      }
    }
    return raw;
  }

  /** AES 키(32byte)/IV(16byte) 로드. utf8 문자열 길이 검증. */
  private aesKeyIv(): { key: Buffer; iv: Buffer } | null {
    const k = this.configService.get<string>('coocon.aesKey');
    const v = this.configService.get<string>('coocon.aesIv');
    if (!k || !v) return null;
    const key = Buffer.from(k, 'utf8');
    const iv = Buffer.from(v, 'utf8');
    if (key.length !== 32 || iv.length !== 16) {
      this.logger.warn(
        `coocon AES 키/IV 길이 비정상 (key=${key.length}, iv=${iv.length}) — 32/16 byte 필요`,
      );
      return null;
    }
    return { key, iv };
  }
}
