/**
 * BUG数据提交工具层
 * 
 * Copyright © 2025 imengyu.top imengyu-bugreport-server
 */

export interface BugReporterConfig {
  /**
   * 设置抽象层以 BugReporter 正常工作
   */
  abstractionLayer: BugReporterAbstractionLayer,
  /**
   * 错误收集服务器URL
   */
  serverUrl: string,
  /**
   * 应用ID
   */
  appId: number,
  /**
   * 应用KEY
   */
  appKey: string,
  /**
   * 是否立即提交错误(通常用于调试)，否则为定时收集错误发送。默认：false
   */
  reportImmediately?: boolean,
  /**
   * 定时收集错误模式时错误数量超过此数值，进行提交。默认：8
   */
  delayMaxCount?: number,
  /**
   * 定时收集错误模式时, 超过指定时间（天），则进行提交。默认：2
   */
  delayTime?: number,
  /**
   * 在应用一次运行中，如果相同数量的错误出现次数超出此阈值，则强制提交一次错误。默认：4
   */
  errorSameRunTimeLimit?: number,
}
export interface BugReporterAbstractionLayer {
  /**
   * 获取设备信息钩子
   */
  getDeviceInfo: () => Promise<BugDetailDeviceInfo>;
  /**
   * 输出调试日志的钩子函数，这个函数返回false，将不启用错误提交功能
   * 通常用于在开发时禁止提交错误到服务器
   */
  log: (str: string) => void;
  /**
   * 是否启用
   */
  enable: () => Promise<boolean>;
  /**
   * 自定义HTTP请求钩子
   */
  doPost: (url: string, body: unknown) => Promise<void>;
  /**
   * 自定义获取存储钩子
   */
  getStorage: (key: string) => Promise<unknown>;
  /**
   * 自定义设置存储钩子
   */
  setStorage: (key: string, value: unknown) => Promise<void>;
  /**
   * 自定义删除存储钩子
   */
  reomveStorage: (key: string) => Promise<void>;
}

//========================================================

export function stringHash(str: string) : string {
  let hash = 0, i, chr;
  if (str.length === 0) return hash.toString();
  for (i = 0; i < str.length; i++) {
    chr   = str.charCodeAt(i);
    hash  = ((hash << 5) - hash) + chr;
    hash |= 0; // Convert to 32bit integer
  }
  return hash.toString();
}
function pad(num: number|string, n: number) : string {
  let str = num.toString();
  let len = str.length;
  while (len < n) {
    str = "0" + num;
    len++;
  }
  return str;
}
function formatDate(date: Date, formatStr?: string) {
  let str = formatStr ? formatStr : "YYYY-MM-dd HH:ii:ss";
  str = str.replace(/yyyy|YYYY/, date.getFullYear().toString());
  str = str.replace(/MM/, pad(date.getMonth() + 1, 2));
  str = str.replace(/M/, (date.getMonth() + 1).toString());
  str = str.replace(/dd|DD/, pad(date.getDate(), 2));
  str = str.replace(/d/, date.getDate().toString());
  str = str.replace(/HH/, pad(date.getHours(), 2));
  str = str.replace(/hh/, pad(date.getHours() > 12 ? date.getHours() - 12 : date.getHours(), 2));
  str = str.replace(/mm/, pad(date.getMinutes(), 2));
  str = str.replace(/ii/, pad(date.getMinutes(), 2));
  str = str.replace(/ss/, pad(date.getSeconds(), 2));
  return str;
}

//========================================================

const errStorageKey = 'BugReporterStorage';
const errStorageCountKey = 'BugReporterStorageCount';
const errStorageTimeKey = 'BugReporterStorageLastSubmitTime';
const errStorageCustomKey = 'BugReporterStorageData';

//========================================================

const customDataArr = new Map<string, string>();
const onceTimeHashCheck = new Map<string, number>();
const defaultConfig : BugReporterConfig = {
  abstractionLayer: {} as BugReporterAbstractionLayer,
  serverUrl: '',
  appId: 0,
  appKey: '',
  reportImmediately: false,
  delayMaxCount: 8,
  delayTime: 2,
  errorSameRunTimeLimit: 4,
};
let abstractionLayer : BugReporterAbstractionLayer = {} as BugReporterAbstractionLayer;
let config : BugReporterConfig = {} as BugReporterConfig;

//========================================================

const BugReporter = {
  /**
   * 配置
   */
  config(configData: BugReporterConfig) : void {
    abstractionLayer = configData.abstractionLayer;
    config = {
      ...defaultConfig,
      ...configData,
    };
    if (!abstractionLayer)
      throw new Error('[BugReporter] You have not configured abstractionLayer, so you cannot use BugReporter.');
    if (!configData.serverUrl)
      abstractionLayer.log('serverUrl is not configured!');
    if (!configData.appId)
      abstractionLayer.log('appId is not configured!');
    if (!configData.appKey)
      abstractionLayer.log('appKey is empty!');
  },
  /**
   * 添加与错误一并提交的自定义数据。注意，自定义数据不是持久保存的，在应用关闭后会丢失
   * @param key 
   * @param value 
   */
  addCustomData(key: string, value: string) : void {
    customDataArr.set(key, value);
  },
  /**
   * 获取之前设置的自定义数据
   * @param key 
   */
  getCustomData(key: string) : string|null {
    return customDataArr.get(key) || null;
  },
  /**
   * 删除与错误一并提交的自定义数据
   * @param key 
   */
  deleteCustomData(key: string) : void {
    customDataArr.delete(key);
  },
  /**
   * 设置与错误一并提交的用户ID数据
   * @param userId 
   */
  async setUserId(userId: string) : Promise<void> {
    await abstractionLayer.setStorage(errStorageCustomKey + 'UserId', userId);
  },
  /**
   * 获取之前设置的用户ID
   * @returns 
   */
  async getUserId(): Promise<string | null> {
    return await abstractionLayer.getStorage(errStorageCustomKey + 'UserId') as string|null;
  },
  /**
   * 程序启动时收集错误信息并提交
   */
  async checkAndReportBug() : Promise<boolean> {
    if(!await abstractionLayer.enable())
      return false;
      
    const errorCount = await abstractionLayer.getStorage(errStorageCountKey) as number || 0;
    const submitLastTime = new Date(await abstractionLayer.getStorage(errStorageTimeKey) as string);
  
    if (
      errorCount > 0 && 
      (
        errorCount > (config.delayMaxCount as number) 
        || new Date().getTime() - submitLastTime.getTime() > (config.delayTime as number) * 1000 * 60 * 60 * 24
      )
    ) {
      abstractionLayer.log('The number or time exceeds the threshold, now submit bug ');
      abstractionLayer.log('Last submit time ' + formatDate(submitLastTime));
      abstractionLayer.log('Error count ' + errorCount + '/' + config.delayMaxCount);
  
      this.reportBugsInStorage();
      return true;
    } else {
      abstractionLayer.log('No need to submit bug now');
    }
   
    return false;
  },
  /**
   * 提交在暂存区中的错误信息
   */
  async reportBugsInStorage() : Promise<void> {
    if(!await abstractionLayer.enable())
      return ;
   
    const errorCount = await abstractionLayer.getStorage(errStorageCountKey) as number || 0;
    abstractionLayer.log('Error count ' + errorCount + '/' + config.delayMaxCount);

    //开始提交
    const bugArr = [] as unknown[];
    for(let i = 0; i < errorCount; i++) {
      const obj = await abstractionLayer.getStorage(errStorageKey + i);
      if (obj && typeof obj === 'object' && typeof (obj as Record<string, unknown>).errorType === 'number')
        bugArr.push(obj);
    }

    abstractionLayer.log('Submit error now, count ' + bugArr.length + ' storage count: ' + errorCount);

    try {
      //立即提交错误
      abstractionLayer.doPost(config.serverUrl, {
        appId: config.appId,
        app_key: config.appKey,
        data: {
          errorList: bugArr
        }
      })
    } catch (e) {
      abstractionLayer.log('Failed to report bug, will submit next time error: ' + e);
      
      //太多错误堆积，需要删除一些
      if (errorCount > 256) {
        abstractionLayer.log('Too many errors, clear some errors ' + (errorCount / 3));
        for(let i = 0; i < errorCount / 3; i++)
          abstractionLayer.reomveStorage(errStorageKey + i);
      }
      return;
    }

    //清除之前存储的数据
    for(let i = 0; i < errorCount; i++)
      abstractionLayer.reomveStorage(errStorageKey + i);
    abstractionLayer.setStorage(errStorageCountKey, 0);
    abstractionLayer.log('Clear bug storage count: ' + errorCount);
    abstractionLayer.setStorage(errStorageTimeKey, new Date().toISOString()); //设置当前时间
  },
  /**
   * 提交Bug
   * @param type 0 普通Bug 1 请求Bug 2 崩溃Bug  3 其他
   * @param hash 对这个错误信息进行归类的hash，当相同hash错误发生时，将在后台系统中归类显示，有助于您查找Bug
   * @param summary 对这个错误进行简要描述的文案，有助于您查找Bug
   * @param data 主要错误信息
   * @param extendData 附加错误信息
   */
  async reportBug(type: number, hash: string, summary: string, data: unknown, extendData?: Record<string, string>) : Promise<void> {
    if(!await abstractionLayer.enable())
      return;

    //获取设备信息
    const deviceInfo = await abstractionLayer.getDeviceInfo();
  
    //追加自定义数据
    const finalExtendData : Record<string, string> = extendData || {};
    customDataArr.forEach((v, k) => finalExtendData[k] = v);

    //检查本次运行错误数量
    let forceReportThisTime = false;
    const onceTimeCount = onceTimeHashCheck.get(hash);
    if (onceTimeCount) {
      onceTimeHashCheck.set(hash, onceTimeCount + 1);
      if (onceTimeCount > (config.errorSameRunTimeLimit as number))
        forceReportThisTime = true;
    } else {
      onceTimeHashCheck.set(hash, 1);
    }
  
    //错误对象
    const errorObj = {
      errorHash: hash,
      errorSummary: summary,
      errorData: JSON.stringify(data),
      errorDeviceName: deviceInfo.deviceName,
      errorDeviceVersion: deviceInfo.deviceVersion,
      errorAppVersion: deviceInfo.appVersion,
      errorUserId: await this.getUserId(),
      errorAdditionData: JSON.stringify(finalExtendData),
      errorTime: formatDate(new Date()),
      errorType: type
    };
  
    if (config.reportImmediately) {
      try {
        //立即提交错误
        abstractionLayer.doPost(config.serverUrl, {
          appId: config.appId,
          appKey: config.appKey,
          data: {
            errorList: [ errorObj ]
          }
        });
      } catch (e) {
        abstractionLayer.log('Failed to report bug. error: ' + e);
      }
    } else {
      //存储当前条目至存储中
      const errorCount = await abstractionLayer.getStorage(errStorageCountKey) as number || 0;
      abstractionLayer.setStorage(errStorageKey + errorCount, errorObj);//设置存储
      abstractionLayer.setStorage(errStorageCountKey, errorCount + 1);//数量+1
      
      //检查相关阈值，并提交错误
      if (forceReportThisTime)
        this.reportBugsInStorage();
      else
        await this.checkAndReportBug();
    }
  },
  /**
   * 提交请求类错误
   * @param data 主要错误信息
   * @param extendData 附加错误信息
   */
  async reportRequestBug(data: BugDetailErrorRequestData, extendData?: Record<string, string>) : Promise<void> {
    await this.reportBug(1, stringHash(data.errorMessage), `请求错误：${data.errorMessage}`, data, extendData);
  },
  /**
   * 提交崩溃类错误
   * @param data 主要错误信息
   * @param extendData 附加错误信息
   */
  async reportCrashBug(data: BugDetailErrorCrashData, extendData?: Record<string, string>) : Promise<void> {
    await this.reportBug(2, stringHash(data.errorMessage), `崩溃：${data.errorMessage}`, data, extendData);
  },
  /**
   * 提交普通错误
   * @param e 主要错误信息
   * @param extendData 附加错误信息
   */
  async reportError(e: Error|string, extendData?: Record<string, string>) : Promise<void> {
    await this.reportBug(2, 
      stringHash(typeof e === 'string' ? e : (e.message || ('' + e))), 
      '' + e, 
      (e instanceof Error ? e.stack : ''), 
      extendData
    );
  }
}

export interface BugDetailDeviceInfo {
  deviceName: string;
  deviceVersion: string;
  appVersion: string;
}
export interface BugDetailErrorRequestData {
  apiName: string;
  apiUrl: string;
  code: string;
  errorType: string;
  errorMessage: string;
  errorCodeMessage: string;
  data: unknown;
  rawData: unknown;
  rawRequest: unknown;
}
export interface BugDetailErrorCrashData {
  deviceName: string;
  deviceRom: string;
  deviceSystemVersion: string;
  errorCode: string;
  errorMessage: string;
  errorStack: string;
}

export default BugReporter;