import { Provide } from '@midwayjs/core';
import { BaseService } from '@cool-midway/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import axios, { AxiosError } from 'axios';
import * as crypto from 'crypto';
import { BaseSysParamEntity } from '../../base/entity/sys/param';
import { BasicdataIotEntity } from '../../basicdata/entity/iot';
import { PushVehicleGpsEntity } from '../../push/entity/vehiclegps';
import { DictTypeEntity } from '../../dict/entity/type';
import { DictInfoEntity } from '../../dict/entity/info';

/**
 * 芯跃开放接口配置
 */
interface XinYueApiConfig {
  baseUrl: string;
  appKey: string;
  appSecret: string;
  iotTypeCode: string;
  requestDelayMs: number;
}

/**
 * 芯跃 getDeviceInfo 返回的设备实时数据
 */
interface XinYueDeviceInfo {
  imei?: string;
  locationType?: number;
  speed?: number;
  gpsTime?: string;
  latitude?: number;
  longitude?: number;
  batteryPercentage?: number;
  altitude?: number;
  direction?: number;
  mainsVoltage?: number;
}

/**
 * 单台设备同步结果
 */
interface XinYueDeviceSyncResult {
  iotCode: string;
  saved: boolean;
  skipped: boolean;
  error?: string;
}

const PARAM_KEY_MAP: Array<{ oldKey: string; newKey: string }> = [
  { oldKey: 'xy.baseUrl', newKey: 'xinYue.baseUrl' },
  { oldKey: 'xy.portalUrl', newKey: 'xinYue.portalUrl' },
  { oldKey: 'xy.appKey', newKey: 'xinYue.appKey' },
  { oldKey: 'xy.appSecret', newKey: 'xinYue.appSecret' },
  { oldKey: 'xy.account', newKey: 'xinYue.account' },
  { oldKey: 'xy.password', newKey: 'xinYue.password' },
  { oldKey: 'xy.whitelistIp', newKey: 'xinYue.whitelistIp' },
  { oldKey: 'xy.iotTypeCode', newKey: 'xinYue.iotTypeCode' },
  { oldKey: 'xy.syncIntervalSeconds', newKey: 'xinYue.syncIntervalSeconds' },
  { oldKey: 'xy.requestDelayMs', newKey: 'xinYue.requestDelayMs' },
];

/**
 * 定时任务-从芯跃平台拉取三轮车定位器实时位置和电压
 * 不汇总统计，仅按 iot 表中「三轮车定位」设备号调用 getDeviceInfo 并写入车辆GPS表
 */
@Provide()
export class TaskServiceXinYueGps extends BaseService {
  @InjectEntityModel(BaseSysParamEntity)
  baseSysParamEntity: Repository<BaseSysParamEntity>;

  @InjectEntityModel(BasicdataIotEntity)
  basicdataIotEntity: Repository<BasicdataIotEntity>;

  @InjectEntityModel(PushVehicleGpsEntity)
  pushVehicleGpsEntity: Repository<PushVehicleGpsEntity>;

  @InjectEntityModel(DictTypeEntity)
  dictTypeEntity: Repository<DictTypeEntity>;

  @InjectEntityModel(DictInfoEntity)
  dictInfoEntity: Repository<DictInfoEntity>;

  /**
   * 同步入口
   * @returns 写入条数及失败明细（失败明细由执行器写入 joblog）
   */
  async syncRealtimeData(): Promise<{ recordsProcessed: number; errorMessage: string }> {
    await this.ensureSysParams();
    await this.ensureIotTypeDict();

    const config = await this.loadApiConfig();
    const devices = await this.basicdataIotEntity.find({
      where: { iotTypeCode: config.iotTypeCode, status: 1 },
      select: ['id', 'iotCode', 'iotName', 'businessId', 'departmentId'],
    });

    if (!devices.length) {
      return { recordsProcessed: 0, errorMessage: '' };
    }

    const errors: string[] = [];
    let savedCount = 0;

    for (let i = 0; i < devices.length; i++) {
      const device = devices[i];
      const iotCode = String(device.iotCode || '').trim();
      if (!iotCode) {
        errors.push(`iotId=${device.id} 缺少设备号iotCode`);
        continue;
      }

      try {
        const result = await this.syncOneDevice(device, config);
        if (result.saved) {
          savedCount += 1;
        }
        if (result.error) {
          errors.push(result.error);
        }
      } catch (err) {
        errors.push(`${iotCode}: ${this.stringifyError(err)}`);
      }

      if (i < devices.length - 1 && config.requestDelayMs > 0) {
        await this.sleep(config.requestDelayMs);
      }
    }

    if (errors.length > 0 && savedCount === 0) {
      throw new Error(errors.join('\n'));
    }
    return {
      recordsProcessed: savedCount,
      errorMessage: errors.length
        ? `成功${savedCount}台，失败${errors.length}台：\n${errors.join('\n')}`
        : '',
    };
  }

  /**
   * 读取同步周期（秒），供执行器节流
   */
  async getSyncIntervalSeconds(): Promise<number> {
    await this.ensureSysParams();
    const value = await this.readParamNumber('xinYue.syncIntervalSeconds', 300);
    return value > 0 ? value : 300;
  }

  /**
   * 同步单台设备
   */
  private async syncOneDevice(
    device: BasicdataIotEntity,
    config: XinYueApiConfig
  ): Promise<XinYueDeviceSyncResult> {
    const iotCode = String(device.iotCode).trim();
    const info = await this.getDeviceInfo(iotCode, config);
    if (!info) {
      return { iotCode, saved: false, skipped: true, error: `${iotCode}: 接口未返回设备数据` };
    }

    const voltage = this.toNumber(info.mainsVoltage);
    let batteryPercent = this.toInt(info.batteryPercentage);
    if (batteryPercent == null && voltage != null) {
      batteryPercent = this.estimateBatteryPercent(voltage);
    }

    const gpsTime = this.parseDate(info.gpsTime);
    const longitudeBd09 = this.toNumber(info.longitude);
    const latitudeBd09 = this.toNumber(info.latitude);
    let longitude: number | null = null;
    let latitude: number | null = null;
    if (longitudeBd09 != null && latitudeBd09 != null) {
      const wgs = this.bd09ToWgs84(longitudeBd09, latitudeBd09);
      longitude = wgs.longitude;
      latitude = wgs.latitude;
    }

    const locationType = this.toInt(info.locationType);
    const status = locationType === 1 ? 'A' : 'B';

    const last = await this.pushVehicleGpsEntity.findOne({
      where: { sn: iotCode },
      order: { gpsTime: 'DESC', id: 'DESC' },
    });

    if (last && gpsTime && last.gpsTime && new Date(last.gpsTime).getTime() === gpsTime.getTime()) {
      const patch: Partial<PushVehicleGpsEntity> = {};
      if (voltage != null && Number(last.voltage) !== voltage) {
        patch.voltage = voltage;
      }
      if (batteryPercent != null && last.batteryPercent !== batteryPercent) {
        patch.batteryPercent = batteryPercent;
      }
      if (Object.keys(patch).length > 0) {
        await this.pushVehicleGpsEntity.update(last.id, patch);
        return { iotCode, saved: true, skipped: false };
      }
      return { iotCode, saved: false, skipped: true };
    }

    await this.pushVehicleGpsEntity.save({
      sn: iotCode,
      msgSeq: `xinYue_${iotCode}_${gpsTime ? gpsTime.getTime() : Date.now()}`,
      createDate: new Date(),
      vehicleNo: device.businessId || iotCode,
      departmentId: device.departmentId || 0,
      gpsTime: gpsTime || new Date(),
      longitude,
      latitude,
      speed: this.toNumber(info.speed),
      direction: this.toInt(info.direction),
      elevation: this.toNumber(info.altitude),
      status,
      voltage,
      batteryPercent,
    });

    return { iotCode, saved: true, skipped: false };
  }

  /**
   * 调用芯跃查询设备信息接口
   */
  private async getDeviceInfo(
    imei: string,
    config: XinYueApiConfig
  ): Promise<XinYueDeviceInfo | null> {
    const path = '/open-api/device/getDeviceInfo';
    const body = JSON.stringify({ imei });
    const payload = await this.postJson(config, path, body);
    if (payload?.code !== 200) {
      throw new Error(
        `${imei}: ${payload?.msg || '查询设备信息失败'}（code: ${payload?.code}）`
      );
    }
    return payload?.data || null;
  }

  /**
   * 带签名的 POST JSON 请求，QPS 超限时按文档退避重试
   */
  private async postJson(
    config: XinYueApiConfig,
    path: string,
    jsonBody: string,
    retry = 0
  ): Promise<any> {
    const url = `${config.baseUrl.replace(/\/$/, '')}${path}`;
    const headers = this.buildSignedHeaders(jsonBody, config.appKey, config.appSecret);

    try {
      const resp = await axios.post(url, jsonBody, {
        headers: {
          'Content-Type': 'application/json',
          ...headers,
        },
        timeout: 30000,
        validateStatus: () => true,
      });

      const errorCode = resp.data?.errorCode;
      if (resp.status === 429 && errorCode === 'OPEN_API_QPS_EXCEEDED' && retry < 3) {
        const waitSec = Number(resp.headers?.['retry-after']) || 1;
        await this.sleep((waitSec + retry) * 1000);
        return this.postJson(config, path, jsonBody, retry + 1);
      }
      if (resp.status === 429 && errorCode === 'OPEN_API_DAILY_QUOTA_EXHAUSTED') {
        throw new Error('今日接口调用额度已用完');
      }
      if (resp.status === 403 && errorCode === 'OPEN_API_QUOTA_NOT_CONFIGURED') {
        throw new Error('芯跃套餐未配置或已过期');
      }
      if (resp.status >= 400) {
        throw new Error(
          `HTTP ${resp.status}: ${resp.data?.msg || JSON.stringify(resp.data)}`
        );
      }
      return resp.data;
    } catch (err) {
      if (err instanceof AxiosError && !err.response) {
        throw new Error(`芯跃接口网络异常: ${err.message}`);
      }
      throw err;
    }
  }

  /**
   * 按芯跃文档生成 HMAC-SHA256 签名请求头
   */
  private buildSignedHeaders(
    jsonBody: string,
    appKey: string,
    appSecret: string
  ): Record<string, string> {
    const timestamp = String(Date.now());
    const nonce = crypto.randomUUID().replace(/-/g, '');
    const params: Record<string, string> = {
      'X-Api-Timestamp': timestamp,
      'X-Api-Nonce': nonce,
      'X-Api-AppKey': appKey,
    };
    if (jsonBody) {
      params['X-Api-Body'] = jsonBody;
    }

    const signStr = Object.keys(params)
      .sort()
      .filter(key => params[key] != null && params[key] !== '')
      .map(key => `${key}=${params[key]}`)
      .join('&');

    const signature = crypto
      .createHmac('sha256', appSecret)
      .update(signStr)
      .digest('base64');

    return {
      'X-Api-Timestamp': timestamp,
      'X-Api-Nonce': nonce,
      'X-Api-AppKey': appKey,
      'X-Api-Signature': signature,
    };
  }

  /**
   * 根据电压估算电量百分比。
   * 文档已提供 batteryPercentage 时优先用接口值；缺失时按常见铅酸/锂电组线性估算。
   * 12V：10.5V~12.7V；48V：42V~54V；60V：52V~67V；72V：63V~84V。
   */
  estimateBatteryPercent(voltage: number): number | null {
    if (!Number.isFinite(voltage) || voltage <= 0) {
      return null;
    }
    let minV = 10.5;
    let maxV = 12.7;
    if (voltage > 16 && voltage <= 54) {
      minV = 42;
      maxV = 54;
    } else if (voltage > 54 && voltage <= 70) {
      minV = 52;
      maxV = 67;
    } else if (voltage > 70) {
      minV = 63;
      maxV = 84;
    }
    const percent = ((voltage - minV) / (maxV - minV)) * 100;
    return Math.max(0, Math.min(100, Math.round(percent)));
  }

  /**
   * BD09（芯跃返回）转 WGS84，与萤石云车辆GPS坐标体系统一
   */
  private bd09ToWgs84(bdLon: number, bdLat: number): { longitude: number; latitude: number } {
    const xPi = (Math.PI * 3000.0) / 180.0;
    const x = bdLon - 0.0065;
    const y = bdLat - 0.006;
    const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * xPi);
    const theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * xPi);
    const gcjLon = z * Math.cos(theta);
    const gcjLat = z * Math.sin(theta);

    const d = this.gcj02Delta(gcjLon, gcjLat);
    return {
      longitude: Number((gcjLon - d.lon).toFixed(6)),
      latitude: Number((gcjLat - d.lat).toFixed(6)),
    };
  }

  private gcj02Delta(lon: number, lat: number): { lon: number; lat: number } {
    const a = 6378245.0;
    const ee = 0.00669342162296594323;
    const dLat = this.transformLat(lon - 105.0, lat - 35.0);
    const dLon = this.transformLon(lon - 105.0, lat - 35.0);
    const radLat = (lat / 180.0) * Math.PI;
    const magic = 1 - ee * Math.sin(radLat) * Math.sin(radLat);
    const sqrtMagic = Math.sqrt(magic);
    return {
      lat: (dLat * 180.0) / (((a * (1 - ee)) / (magic * sqrtMagic)) * Math.PI),
      lon: (dLon * 180.0) / ((a / sqrtMagic) * Math.cos(radLat) * Math.PI),
    };
  }

  private transformLat(x: number, y: number): number {
    let ret =
      -100.0 +
      2.0 * x +
      3.0 * y +
      0.2 * y * y +
      0.1 * x * y +
      0.2 * Math.sqrt(Math.abs(x));
    ret +=
      ((20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0) / 3.0;
    ret +=
      ((20.0 * Math.sin(y * Math.PI) + 40.0 * Math.sin((y / 3.0) * Math.PI)) * 2.0) / 3.0;
    ret +=
      ((160.0 * Math.sin((y / 12.0) * Math.PI) + 320 * Math.sin((y * Math.PI) / 30.0)) * 2.0) /
      3.0;
    return ret;
  }

  private transformLon(x: number, y: number): number {
    let ret =
      300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
    ret +=
      ((20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0) / 3.0;
    ret +=
      ((20.0 * Math.sin(x * Math.PI) + 40.0 * Math.sin((x / 3.0) * Math.PI)) * 2.0) / 3.0;
    ret +=
      ((150.0 * Math.sin((x / 12.0) * Math.PI) + 300.0 * Math.sin((x / 30.0) * Math.PI)) * 2.0) /
      3.0;
    return ret;
  }

  private async loadApiConfig(): Promise<XinYueApiConfig> {
    const baseUrl = await this.readParamString('xinYue.baseUrl', 'https://open-api.yznapp.com');
    const appKey = await this.readParamString('xinYue.appKey', '');
    const appSecret = await this.readParamString('xinYue.appSecret', '');
    const iotTypeCode = await this.readParamString('xinYue.iotTypeCode', '三轮车定位');
    const requestDelayMs = await this.readParamNumber('xinYue.requestDelayMs', 400);

    if (!appKey || !appSecret) {
      throw new Error('芯跃 KEY/秘钥未配置，请在【系统参数】中维护 xinYue.appKey / xinYue.appSecret');
    }
    return { baseUrl, appKey, appSecret, iotTypeCode, requestDelayMs };
  }

  /**
   * 首次运行时写入系统参数，已存在则不覆盖
   */
  private async ensureSysParams() {
    await this.migrateOldXyParams();

    const defaults: Array<{
      keyName: string;
      name: string;
      data: string;
      remark: string;
    }> = [
      {
        keyName: 'xinYue.baseUrl',
        name: '芯跃开放接口域名',
        data: 'https://open-api.yznapp.com',
        remark: '对应文档 api.example.com，正式环境为 open-api.yznapp.com',
      },
      {
        keyName: 'xinYue.portalUrl',
        name: '芯跃平台门户地址',
        data: 'https://xy-gnss.com',
        remark: '芯跃 GNSS 管理后台地址',
      },
      {
        keyName: 'xinYue.appKey',
        name: '芯跃开放平台KEY',
        data: 'JM7JX2yCxcjmOkYt',
        remark: '请求头 X-Api-AppKey',
      },
      {
        keyName: 'xinYue.appSecret',
        name: '芯跃开放平台秘钥',
        data: '3CXm9ZD7JC1iBHaxnA1bBswL95dVI2Ip',
        remark: 'HMAC-SHA256 签名秘钥',
      },
      {
        keyName: 'xinYue.account',
        name: '芯跃平台账号',
        data: '京申科技',
        remark: '门户登录账号，开放接口鉴权不使用',
      },
      {
        keyName: 'xinYue.password',
        name: '芯跃平台密码',
        data: '123456',
        remark: '门户登录密码，开放接口鉴权不使用',
      },
      {
        keyName: 'xinYue.whitelistIp',
        name: '芯跃接口白名单IP',
        data: '115.159.85.154',
        remark: '调用方公网IP，需在芯跃后台配置白名单',
      },
      {
        keyName: 'xinYue.iotTypeCode',
        name: '芯跃同步终端类型',
        data: '三轮车定位',
        remark: '从 t_basicdata_iot.iotTypeCode 筛选设备，iotCode 为设备号/IMEI',
      },
      {
        keyName: 'xinYue.syncIntervalSeconds',
        name: '芯跃位置电压同步周期(秒)',
        data: '300',
        remark: '定时拉取位置和电压的最小间隔，建议后台任务 cron 不大于该值。默认300秒',
      },
      {
        keyName: 'xinYue.requestDelayMs',
        name: '芯跃接口请求间隔(毫秒)',
        data: '400',
        remark: '逐台查询设备时的间隔，避免触发 QPS 限流',
      },
    ];

    for (const item of defaults) {
      const exist = await this.baseSysParamEntity.findOneBy({ keyName: item.keyName });
      if (!exist) {
        await this.baseSysParamEntity.save({
          keyName: item.keyName,
          name: item.name,
          data: item.data,
          dataType: 0,
          remark: item.remark,
        });
      }
    }
  }

  /**
   * 将旧版 xy.* 系统参数键迁移为 xinYue.*
   */
  private async migrateOldXyParams() {
    for (const item of PARAM_KEY_MAP) {
      const oldRow = await this.baseSysParamEntity.findOneBy({ keyName: item.oldKey });
      if (!oldRow) {
        continue;
      }
      const newRow = await this.baseSysParamEntity.findOneBy({ keyName: item.newKey });
      if (newRow) {
        await this.baseSysParamEntity.delete(oldRow.id);
        continue;
      }
      oldRow.keyName = item.newKey;
      await this.baseSysParamEntity.save(oldRow);
    }
  }

  /**
   * 确保字典「终端类型」中有「三轮车定位」，方便在 iot 基础数据里选择
   */
  private async ensureIotTypeDict() {
    const dictType = await this.dictTypeEntity.findOneBy({ key: 'iottype' });
    if (!dictType) {
      return;
    }
    const exist = await this.dictInfoEntity.findOneBy({
      typeId: dictType.id,
      value: '三轮车定位',
    });
    if (exist) {
      return;
    }
    const maxOrder = await this.dictInfoEntity
      .createQueryBuilder('a')
      .select('MAX(a.orderNum)', 'maxOrder')
      .where('a.typeId = :typeId', { typeId: dictType.id })
      .getRawOne();
    await this.dictInfoEntity.save({
      typeId: dictType.id,
      name: '三轮车定位',
      value: '三轮车定位',
      orderNum: (Number(maxOrder?.maxOrder) || 0) + 1,
      remark: '芯跃4G车辆定位器',
    });
  }

  private async readParamString(keyName: string, defaultValue: string): Promise<string> {
    const row = await this.baseSysParamEntity.findOneBy({ keyName });
    const value = row?.data == null ? '' : String(row.data).trim();
    return value || defaultValue;
  }

  private async readParamNumber(keyName: string, defaultValue: number): Promise<number> {
    const raw = await this.readParamString(keyName, String(defaultValue));
    const num = Number(raw);
    return Number.isFinite(num) ? num : defaultValue;
  }

  private toNumber(value: any): number | null {
    if (value == null || value === '') {
      return null;
    }
    const num = Number(value);
    return Number.isFinite(num) ? num : null;
  }

  private toInt(value: any): number | null {
    const num = this.toNumber(value);
    return num == null ? null : Math.round(num);
  }

  private parseDate(value: string): Date | null {
    if (!value) {
      return null;
    }
    const date = new Date(String(value).replace(' ', 'T'));
    return Number.isNaN(date.getTime()) ? null : date;
  }

  private sleep(ms: number) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  private stringifyError(error: any): string {
    if (error instanceof Error) {
      return error.stack || error.message;
    }
    if (typeof error === 'string') {
      return error;
    }
    try {
      return JSON.stringify(error);
    } catch {
      return String(error);
    }
  }
}
