import { randomUUID } from 'node:crypto';
import { BadRequestException, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ApiCallLog } from './entities/api-call-log.entity';
import { maskSensitivePayload } from './third-party-security';

export type ThirdPartyPayload = Record<string, unknown>;

type CallOptions = {
  timeoutMs?: number;
};

@Injectable()
export class ThirdPartyClientService {
  constructor(
    private readonly configService: ConfigService,
    @InjectRepository(ApiCallLog)
    private readonly apiCallLogsRepository: Repository<ApiCallLog>,
  ) {}

  async call<T = unknown>(
    act: string,
    payload: ThirdPartyPayload = {},
    options: CallOptions = {},
  ): Promise<T> {
    const startedAt = Date.now();
    const baseUrl = this.configService.getOrThrow<string>('WK_BASE_URL');
    const requestUrl = new URL(baseUrl);
    requestUrl.searchParams.set('act', act);
    let httpStatus: number | null = null;
    let timeout: NodeJS.Timeout | undefined;
    let logged = false;
    let maskedRequestPayload: Record<string, unknown> = maskSensitivePayload({
      act,
      ...payload,
    });

    try {
      const bodyPayload = {
        ...payload,
        uid: this.getUid(),
        key: this.getApiKey(),
      };
      maskedRequestPayload = maskSensitivePayload({ act, ...bodyPayload });
      const requestBody = this.toUrlEncodedBody(bodyPayload);
      this.debugLogRequest(act, requestUrl.toString(), requestBody);
      const controller = new AbortController();
      timeout = setTimeout(
        () => controller.abort(),
        options.timeoutMs ?? 15_000,
      );

      const response = await fetch(requestUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: requestBody,
        signal: controller.signal,
      });

      httpStatus = response.status;
      const { parsedResponse, responseText } = await this.parseResponse(response);
      const maskedResponsePayload = maskSensitivePayload(parsedResponse);
      const businessError = this.getBusinessError(parsedResponse);
      const errorMessage = response.ok ? businessError : `HTTP ${response.status}`;
      const status = response.ok && !businessError ? 'success' : 'failure';
      this.debugLogResponse({
        act,
        status,
        httpStatus,
        durationMs: Date.now() - startedAt,
        responseText,
        parsedResponse: maskedResponsePayload,
        errorMessage,
      });

      await this.writeLog({
        act,
        url: requestUrl.toString(),
        status,
        httpStatus,
        durationMs: Date.now() - startedAt,
        requestPayload: maskedRequestPayload,
        responsePayload: maskedResponsePayload,
        errorMessage,
      });
      logged = true;

      if (!response.ok) {
        throw new BadRequestException(`第三方接口请求失败：HTTP ${response.status}`);
      }
      if (businessError) {
        throw new BadRequestException(`第三方接口返回失败：${businessError}`);
      }

      return parsedResponse as T;
    } catch (error) {
      if (logged) {
        throw error;
      }
      const message = this.getErrorMessage(error);
      await this.writeLog({
        act,
        url: requestUrl.toString(),
        status: 'failure',
        httpStatus,
        durationMs: Date.now() - startedAt,
        requestPayload: maskedRequestPayload,
        responsePayload: null,
        errorMessage: message,
      });
      throw this.toClientException(error);
    } finally {
      if (timeout) {
        clearTimeout(timeout);
      }
    }
  }

  async testConnection() {
    return this.call('getcate');
  }

  getConfigStatus() {
    const apiKey = this.configService.get<string>('WK_APP_KEY');

    return {
      provider: 'biedawo',
      baseUrl: this.configService.get<string>('WK_BASE_URL') ?? null,
      uidConfigured: Boolean(this.configService.get<string>('WK_APP_UID')),
      keyConfigured: Boolean(apiKey),
      debugLogEnabled: this.isDebugEnabled(),
    };
  }

  private getUid() {
    const uid = this.configService.get<string>('WK_APP_UID');
    if (!uid) {
      throw new BadRequestException('缺少 WK_APP_UID，无法调用第三方接口');
    }
    return uid;
  }

  private getApiKey() {
    const apiKey = this.configService.get<string>('WK_APP_KEY');
    if (!apiKey) {
      throw new BadRequestException('缺少 WK_APP_KEY，无法调用第三方接口');
    }
    return apiKey;
  }

  private toUrlEncodedBody(payload: ThirdPartyPayload) {
    const params = new URLSearchParams();
    for (const [key, value] of Object.entries(payload)) {
      if (value === undefined || value === null) {
        continue;
      }
      params.set(
        key,
        typeof value === 'object' ? JSON.stringify(value) : String(value),
      );
    }
    return params;
  }

  private async parseResponse(response: Response) {
    const text = await response.text();
    if (!text) {
      return { parsedResponse: null, responseText: text };
    }

    try {
      return { parsedResponse: JSON.parse(text) as unknown, responseText: text };
    } catch {
      return { parsedResponse: { raw: text }, responseText: text };
    }
  }

  private debugLogRequest(
    act: string,
    url: string,
    requestBody: URLSearchParams,
  ) {
    if (!this.isDebugEnabled()) {
      return;
    }

    const requestPayload = Object.fromEntries(requestBody.entries());
    const key = requestBody.get('key') ?? '';
    const uid = requestBody.get('uid') ?? '';
    console.log('[third-party] request', {
      act,
      method: 'POST',
      url,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: maskSensitivePayload(requestPayload),
      bodyString: this.maskBodyString(requestBody.toString()),
      uidLength: uid.length,
      uidHasOuterWhitespace: uid !== uid.trim(),
      keyLength: key.length,
      keyPreview: this.previewSecret(key),
      keyHasOuterWhitespace: key !== key.trim(),
    });
  }

  private debugLogResponse(input: {
    act: string;
    status: 'success' | 'failure';
    httpStatus: number | null;
    durationMs: number;
    responseText: string;
    parsedResponse: unknown;
    errorMessage: string | null;
  }) {
    if (!this.isDebugEnabled()) {
      return;
    }

    console.log('[third-party] response', {
      act: input.act,
      status: input.status,
      httpStatus: input.httpStatus,
      durationMs: input.durationMs,
      responseText: this.maskResponseText(input.responseText),
      parsedResponse: input.parsedResponse,
      errorMessage: input.errorMessage,
    });
  }

  private isDebugEnabled() {
    return ['1', 'true', 'yes', 'on'].includes(
      String(this.configService.get<string>('WK_DEBUG_LOG') || '').toLowerCase(),
    );
  }

  private maskBodyString(value: string) {
    const params = new URLSearchParams(value);
    for (const key of Array.from(params.keys())) {
      if (/key|pass|password|secret|token|authorization/i.test(key)) {
        params.set(key, String(maskSensitivePayload({ [key]: params.get(key) })[key]));
      }
    }
    return params.toString();
  }

  private maskResponseText(value: string) {
    if (!value) {
      return value;
    }

    try {
      return JSON.stringify(maskSensitivePayload(JSON.parse(value)));
    } catch {
      return value;
    }
  }

  private previewSecret(value: string) {
    if (!value) {
      return '';
    }
    if (value.length <= 8) {
      return '*'.repeat(value.length);
    }
    return `${value.slice(0, 4)}****${value.slice(-4)}`;
  }

  async writeLog(input: {
    act: string;
    url: string;
    method?: string;
    status: 'success' | 'failure';
    httpStatus: number | null;
    durationMs: number;
    requestPayload: Record<string, unknown>;
    responsePayload: unknown | null;
    errorMessage: string | null;
  }) {
    const entity = this.apiCallLogsRepository.create({
      id: randomUUID(),
      provider: 'biedawo',
      act: input.act,
      method: input.method ?? 'POST',
      url: input.url,
      status: input.status,
      success: input.status === 'success',
      httpStatus: input.httpStatus,
      durationMs: input.durationMs,
      requestPayload: input.requestPayload,
      responsePayload: input.responsePayload,
      errorMessage: input.errorMessage,
    });
    await this.apiCallLogsRepository.save(entity);
  }

  private getErrorMessage(error: unknown) {
    if (error instanceof Error) {
      return error.name === 'AbortError' ? '第三方接口请求超时' : error.message;
    }
    return '第三方接口请求失败';
  }

  private toClientException(error: unknown) {
    if (error instanceof BadRequestException) {
      return error;
    }
    return new BadRequestException(this.getErrorMessage(error));
  }

  private getBusinessError(response: unknown) {
    if (!response || typeof response !== 'object' || !('code' in response)) {
      return null;
    }

    const body = response as { code?: unknown; msg?: unknown; message?: unknown };
    if (body.code === 1 || body.code === '1' || body.code === 200 || body.code === '200') {
      return null;
    }

    return String(body.msg || body.message || `业务状态码 ${String(body.code)}`);
  }
}
