import { Injectable, Logger } from '@nestjs/common';
import axios, {
  AxiosBasicCredentials,
  AxiosError,
  AxiosInstance,
  AxiosRequestConfig,
} from 'axios';

export interface IWebClientRequestConfig {
  headers?: {
    [key: string]: string;
  };
  auth?: AxiosBasicCredentials;
  timeout?: number;
  maxRedirects?: number;
}

//* 응답 본문 + 헤더를 함께 돌려주는 래퍼. 기본 post/get은 본문만(response.data) 반환하므로
//*   Set-Cookie 같은 응답 헤더가 필요한 호출(예: inhouse ALB sticky)은 이걸 쓴다.
export interface IWebClientResponse<T> {
  data: T;
  headers: Record<string, any>;
}

export abstract class WebClientService {
  abstract post<T>(
    url: string,
    body: any,
    options?: IWebClientRequestConfig,
  ): Promise<T>;

  abstract get<T>(url: string, options?: IWebClientRequestConfig): Promise<T>;

  //* 본문과 응답 헤더를 함께 반환(Set-Cookie 등). 헤더가 필요 없으면 post/get을 그대로 쓰면 된다.
  abstract postWithResponse<T>(
    url: string,
    body: any,
    options?: IWebClientRequestConfig,
  ): Promise<IWebClientResponse<T>>;
}

@Injectable()
export class AxiosWebClientService implements WebClientService {
  private client: AxiosInstance;
  //* 본문만 반환하는 기본 client와 달리, 응답 헤더가 필요한 호출용(Set-Cookie 등).
  //*   response 인터셉터가 response.data로 줄여버리지 않게 별도 인스턴스를 둔다.
  private rawClient: AxiosInstance;
  private logger: Logger = new Logger(AxiosWebClientService.name);

  //* 로그 마스킹(보안감사 C5): 민감 헤더(x-api-key/authorization/cookie)·PII 필드(이름/주민/폰/생년/
  //*   통신사/이메일/주소)를 로그에 평문으로 남기지 않는다. certify 바디·엔진 api-key·진료응답이 평문으로
  //*   찍히던 걸 차단. (디버그 가치는 유지 — 비민감 필드는 그대로, 민감 키만 ***)
  private static readonly SENSITIVE_HEADERS = [
    'x-api-key',
    'authorization',
    'cookie',
    'set-cookie',
  ];
  private static readonly PII_KEY =
    /(name|phone|mobile|birth|telecom|identity|jumin|rrn|ssn|resident|email|addr|(?:conn)?header|(?:api|sender)key|receiver|message|button)/i;
  private redactHeaders(headers: unknown): unknown {
    if (!headers || typeof headers !== 'object') return headers;
    const out: Record<string, unknown> = {
      ...(headers as Record<string, unknown>),
    };
    for (const k of Object.keys(out)) {
      if (AxiosWebClientService.SENSITIVE_HEADERS.includes(k.toLowerCase()))
        out[k] = '***';
    }
    return out;
  }
  private redactPII(data: unknown, depth = 0): unknown {
    if (depth > 4 || data == null) return data;
    if (Array.isArray(data))
      return data.map((v) => this.redactPII(v, depth + 1));
    if (typeof data === 'object') {
      const out: Record<string, unknown> = {};
      for (const [k, v] of Object.entries(data as Record<string, unknown>)) {
        out[k] = AxiosWebClientService.PII_KEY.test(k)
          ? '***'
          : this.redactPII(v, depth + 1);
      }
      return out;
    }
    return data;
  }

  //* 요청 로깅 인터셉터(두 인스턴스 공용). 헤더·바디의 민감정보는 위 redact로 마스킹.
  private attachRequestLogger(instance: AxiosInstance) {
    instance.interceptors.request.use(
      (request) => {
        let logData = request.data;
        if (request.data && typeof request.data?.getBuffer === 'function') {
          logData = '[FormData]';
        }
        const loggedParams = {
          method: request.method,
          url: request.url,
          headers: this.redactHeaders(request.headers),
          data: this.redactPII(logData),
          timeout: request.timeout,
        };
        this.logger.log(loggedParams, 'axios request completed');
        return request;
      },
      (error) => {
        error instanceof AxiosError &&
          this.logger.error(error.message, 'axios request errored');
        throw error;
      },
    );
  }

  constructor() {
    const instance = axios.create();
    this.attachRequestLogger(instance);
    instance.interceptors.response.use(
      (response) => {
        const isCodef = response.config?.url?.includes('codef');
        if (!isCodef) {
          const loggedParams = {
            status: response.status,
            data: this.redactPII(response.data),
          };
          this.logger.log(loggedParams, 'axios response completed');
        }
        return response.data;
      },
      (error) => {
        error instanceof AxiosError &&
          this.logger.error(error.message, 'axios response errored');
        throw error;
      },
    );

    this.client = instance;

    //* rawClient: 본문+헤더 전체를 그대로 돌려준다(response.data로 축약 X).
    //*   에러 로깅만 붙이고, Set-Cookie가 헤더에 실릴 수 있어 성공 응답 본문/헤더는 로깅하지 않는다.
    const rawInstance = axios.create();
    this.attachRequestLogger(rawInstance);
    rawInstance.interceptors.response.use(
      (response) => response,
      (error) => {
        error instanceof AxiosError &&
          this.logger.error(error.message, 'axios response errored');
        throw error;
      },
    );
    this.rawClient = rawInstance;
  }

  async post<T>(
    url: string,
    body: any,
    config?: AxiosRequestConfig,
  ): Promise<T> {
    return await this.client.post(url, body, config);
  }

  async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return await this.client.get(url, config);
  }

  async postWithResponse<T>(
    url: string,
    body: any,
    config?: AxiosRequestConfig,
  ): Promise<IWebClientResponse<T>> {
    const response = await this.rawClient.post<T>(url, body, config);
    return { data: response.data, headers: response.headers ?? {} };
  }
}
