//  Copyright (c) 2024 Huawei Technologies Co., Ltd.
//  openUBMC is licensed under Mulan PSL v2.
//  You can use this software according to the terms and conditions of the Mulan PSL v2.
//  You may obtain a copy of Mulan PSL v2 at:
//        #  http://license.coscl.org.cn/MulanPSL2
//  THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
//  EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
//  MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
//  See the Mulan PSL v2 for more details.
import { getEncryptKey } from '@/utils/common-api';
import { IbmcHttp, default as $http } from '@/utils/http-service';
import axios, { AxiosError, AxiosResponse } from 'axios';
import { arrayBufferToHexStr, importRsaPublicKey, rsaEncrypt } from './crypto';
import { deepClone } from './utils';

const expireDays = 7;

interface ReauthOptionalParams {
  [key: string]: any;
  ReauthKey?: string;
}

type MappedHandler<T extends ReauthOptionalParams> = (params: T) => IbmcHttp;

export const update = async (): Promise<string> => {
  const newKey = (await getEncryptKey()).data?.PubKey ?? '';
  const date = new Date();
  date.setDate(date.getDate() + expireDays);
  localStorage.setItem('key', btoa(JSON.stringify({
    pubKey: newKey,
    nextUpdate: date,
  })));
  return newKey;
};

// 获取缓存的公钥
export const getCachedPubKey = async (): Promise<string> => {
  const key = localStorage.getItem('key') as string;
  try {
    const {pubKey, nextUpdate} = JSON.parse(atob(key));
    return Date.parse(nextUpdate) > Date.now() ? pubKey : await update();
  } catch (e) {
    return await update();
  }
};

type PathLike = string | string[];
const noop = (): void => {};

class SensitiveRequest extends IbmcHttp {
  private currentKey: string | null = null;
  private parentResolve: (value: (AxiosResponse | PromiseLike<AxiosResponse>)) => void = noop;
  private parentReject: (error: AxiosResponse) => void = noop;

  constructor(
    private encryptedPaths: PathLike[],
    private paramGetter: (pubKey: string) => Promise<any>,
  ) {
    super();
  }

  async init(): Promise<string> {
    this.currentKey = await getCachedPubKey();
    return this.currentKey;
  }

  override async rejectOperate(
    resolve: (value: (AxiosResponse | PromiseLike<AxiosResponse>)) => void,
    reject: (error?: AxiosResponse) => void,
    _: AxiosError,
  ): Promise<void> {
    this.parentResolve = resolve;
    this.parentReject = reject;
  }

  override async errorHandle(reqError: AxiosError<any>, method?: string, config?: any): Promise<void> {
    if (reqError.response?.data?.error?.[0].code === 'InvalidPubKey') {
      await this.forceUpdateKey();
      const reqConfig = reqError.config;
      if (!reqConfig) {
        return;
      }
      reqConfig.data = JSON.stringify(await this.paramGetter(this.currentKey ?? ''));
      try {
        const res = await axios.request(reqConfig);
        setTimeout(() => {
          this.parentResolve(res);
        });
      } catch (e: any) {
        super.errorHandle(e, method, config);
        setTimeout(() => {
          this.parentReject(e.response);
        });
      }
    } else {
      super.errorHandle(reqError, method, config);
      setTimeout(() => {
        this.parentReject(reqError.response as AxiosResponse);
      });
    }
  }

  override get = async (url: string, _?: never, config?: any): Promise<AxiosResponse> =>
    await super.get(url, await this.getParams(), this.getConfig(config));

  override post = async (url: string, _?: never, config?: any): Promise<AxiosResponse> =>
    await super.post(url, await this.getParams(), this.getConfig(config));

  override patch = async (url: string, _?: never, config?: any): Promise<AxiosResponse> =>
    await super.patch(url, await this.getParams(), this.getConfig(config));

  override delete = async (url: string, _?: never, config?: any): Promise<AxiosResponse> =>
    await super.delete(url, await this.getParams(), this.getConfig(config));

  private async getParams(): Promise<any> {
    try {
      return await this.paramGetter(this.currentKey ?? await this.init());
    } catch (e) {
      await this.forceUpdateKey();
      return await this.paramGetter(this.currentKey as string);
    }
  }

  private getConfig = (config: any): any => {
    const newConfig = config ?? { headers: {} };
    newConfig.headers['Encrypted-Properties'] = JSON.stringify(this.encryptedPaths);
    return newConfig;
  };

  private async forceUpdateKey(): Promise<void> {
    try {
      this.currentKey = await update();
    } catch (e) {
      setTimeout(() => this.parentReject(e as any));
    }
  }
}

export default function sensitive(encryptedPaths: PathLike[], paramGetter: (pubKey: string) => any): SensitiveRequest {
  return new SensitiveRequest(encryptedPaths, paramGetter);
};

export function mapReauth<T extends ReauthOptionalParams>(keys: PathLike[] = []): MappedHandler<T> {
  return (params: T): IbmcHttp => {
    const reauthRequired = params.ReauthKey !== undefined && params.ReauthKey !== '';
    const encPaths = [...keys].filter((key) => key !== 'ReauthKey');
    const encHeaders = [...encPaths, ...(reauthRequired ? ['ReauthKey'] : [])];
    
    if (encPaths.length === 0 && !reauthRequired) {
      return $http;
    }

    const paramGetter = async (rawPubKey: string): Promise<T> => {
      const pubKey = await importRsaPublicKey(rawPubKey, 'SHA-1');
      const paramsCopy = deepClone(params) as T;

      await Promise.all(encPaths.map(async (encPath) => {
        if (Array.isArray(encPath)) {
          const leafPath = encPath[encPath.length - 1];
          let preLeaf: any = paramsCopy;
          encPath.slice(0, -1).forEach((path) => {
            preLeaf = preLeaf?.[path];
          });
          if (typeof preLeaf[leafPath] !== 'string') {
            return;
          }
          preLeaf[leafPath] = arrayBufferToHexStr(
            await rsaEncrypt(preLeaf[leafPath] as string, pubKey)) as T[keyof T];
        } else {
          const key = encPath as keyof T;
          paramsCopy[key] = arrayBufferToHexStr(await rsaEncrypt(params[key] as string, pubKey)) as T[keyof T];
        }
      }));

      if (reauthRequired) {
        paramsCopy.ReauthKey = arrayBufferToHexStr(await rsaEncrypt(params.ReauthKey as string, pubKey));
      }
      return paramsCopy;
    };
    return sensitive(encHeaders, paramGetter);
  };
}

export const reauthHandler = <T extends ReauthOptionalParams>(params: T): IbmcHttp => mapReauth()(params);
