import { BadGatewayException, BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Prisma } from '@prisma/client';
import { AuthUser } from '../../common/current-user.decorator';
import { RegionScopeService } from '../../common/region-scope.service';
import { PrismaService } from '../prisma/prisma.service';

interface JsonRpcResponse<T> {
  jsonrpc: string;
  result: T | null;
  error?: {
    message: string;
    code: number;
  };
}

interface LightingLoginResult {
  token: string;
}

interface ExternalIlcItem {
  id: number;
  ilcName?: string;
  commUid?: string;
  rtuId?: number;
  rtuCommUid?: string;
  workState?: number;
  branchCount?: number;
  description?: string;
  lampPost?: string;
  positionX?: number;
  positionY?: number;
  firm?: string;
  setPower?: string;
  set_power?: string;
  isPowerOn?: boolean;
  isOnline?: boolean;
  lamp_post?: string;
  lastOnlineTime?: string;
}

interface GetIlcListResult {
  rows: ExternalIlcItem[];
  count: number;
}

interface ExternalFaultItem {
  id: number;
  comm_uid?: string;
  commUid?: string;
  loop_idx?: number;
  loopIdx?: number;
  voltage?: number;
  fault_msg?: string;
  faultMsg?: string;
  fault_code?: string;
  faultCode?: string;
  fault_time?: string;
  faultTime?: string;
  operate_status?: number;
  operateStatus?: number;
}

interface GetFaultListResult {
  rows: ExternalFaultItem[];
  count: number;
}

/** 监控终端 RTU，平台实际 JSON-RPC 方法为 /lightm/rtu/list */
interface ExternalRtuItem {
  id: number;
  rtuName?: string;
  commUid?: string;
  groupId?: number;
  description?: string;
  workState?: number;
  uimNumber?: string;
  lampPost?: string;
  lamp_post?: string;
  positionX?: number;
  positionY?: number;
  isOnline?: boolean;
  csq?: number;
  lastOnlineTime?: string;
  lastSyncTime?: string;
}

export interface SyncPageStats {
  total: number;
  created: number;
  updated: number;
  skipped: number;
}

@Injectable()
export class LampsService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly regionScope: RegionScopeService,
    private readonly configService: ConfigService,
  ) {}

  async findAll(user: AuthUser, query: any) {
    const page = Math.max(Number(query.page ?? 1), 1);
    const pageSize = Math.min(Math.max(Number(query.pageSize ?? query.limit ?? 20), 1), 100);
    const onlyTerminal =
      query.onlyTerminal === '1' ||
      query.onlyTerminal === 'true' ||
      query.onlyTerminal === true ||
      query.onlyTerminal === 1;
    const excludeTerminal =
      query.excludeTerminal === '1' ||
      query.excludeTerminal === 'true' ||
      query.excludeTerminal === true ||
      query.excludeTerminal === 1;
    const where = await this.regionScope.lampWhere(user, {
      ...(onlyTerminal ? { deviceType: '监控终端' } : excludeTerminal ? { deviceType: { not: '监控终端' } } : {}),
      ...(query.keyword ? { OR: [{ code: { contains: query.keyword } }, { name: { contains: query.keyword } }] } : {}),
      ...(query.status ? { status: query.status } : {}),
      ...(query.onlineStatus ? { onlineStatus: query.onlineStatus } : {}),
    }, query.regionId);

    const [items, total, onlineTotal, abnormalTotal] = await Promise.all([
      this.prisma.streetLamp.findMany({
        where,
        include: { region: true, alarms: { where: { status: { in: ['OPEN', 'PROCESSING'] } } } },
        orderBy: { createdAt: 'desc' },
        skip: (page - 1) * pageSize,
        take: pageSize,
      }),
      this.prisma.streetLamp.count({ where }),
      this.prisma.streetLamp.count({
        where: { AND: [where, { onlineStatus: 'ONLINE' }] } as Prisma.StreetLampWhereInput,
      }),
      this.prisma.streetLamp.count({
        where: { AND: [where, { status: { not: 'NORMAL' } }] } as Prisma.StreetLampWhereInput,
      }),
    ]);

    return { items, total, page, pageSize, onlineTotal, abnormalTotal };
  }

  async create(user: AuthUser, dto: any) {
    const longitude = Number(dto.longitude);
    const latitude = Number(dto.latitude);
    const regionId = dto.regionId ?? (await this.regionScope.findNearestRegionId(longitude, latitude));
    if (!regionId) {
      throw new BadRequestException('无法根据经纬度匹配所属区域');
    }

    return this.prisma.streetLamp.create({
      data: {
        code: dto.code,
        name: dto.name,
        tenantId: user.tenantId,
        regionId,
        address: dto.address,
        longitude,
        latitude,
        deviceType: this.normalizeDeviceType(dto.deviceType) ?? '单灯',
        status: dto.status,
        onlineStatus: dto.onlineStatus,
        isPowerOn: dto.isPowerOn,
        lastReportedAt: dto.lastReportedAt ? new Date(dto.lastReportedAt) : undefined,
      },
    });
  }

  async update(user: AuthUser, id: string, dto: any) {
    const existing = await this.prisma.streetLamp.findFirst({ where: await this.regionScope.lampWhere(user, { id }) });
    if (!existing) {
      throw new NotFoundException('路灯不存在或无权限访问');
    }

    const longitude = dto.longitude === undefined ? undefined : Number(dto.longitude);
    const latitude = dto.latitude === undefined ? undefined : Number(dto.latitude);
    const regionId =
      dto.regionId ??
      (longitude !== undefined && latitude !== undefined
        ? await this.regionScope.findNearestRegionId(longitude, latitude)
        : undefined);

    return this.prisma.streetLamp.update({
      where: { id },
      data: {
        code: dto.code,
        name: dto.name,
        regionId,
        address: dto.address,
        longitude,
        latitude,
        deviceType: this.normalizeDeviceType(dto.deviceType),
        status: dto.status,
        onlineStatus: dto.onlineStatus,
        isPowerOn: dto.isPowerOn,
        lastReportedAt: dto.lastReportedAt ? new Date(dto.lastReportedAt) : undefined,
      },
    });
  }

  async remove(user: AuthUser, id: string) {
    const existing = await this.prisma.streetLamp.findFirst({ where: await this.regionScope.lampWhere(user, { id }) });
    if (!existing) {
      throw new NotFoundException('路灯不存在或无权限访问');
    }

    return this.prisma.streetLamp.delete({ where: { id } });
  }

  async syncExternalIlcList(user: AuthUser, options: any = {}) {
    const baseUrlRaw = this.configService.get<string>('EXTERNAL_LIGHTING_BASE_URL');
    const usernameRaw = this.configService.get<string>('EXTERNAL_LIGHTING_USERNAME');
    const passwordRaw = this.configService.get<string>('EXTERNAL_LIGHTING_PASSWORD');
    const tenantNameRaw = this.configService.get<string>('EXTERNAL_LIGHTING_TENANT_NAME');
    const ilcListMethod = this.configService.get<string>('EXTERNAL_LIGHTING_ILC_LIST_METHOD') ?? '/device/getIlcList';
    const rtuListMethod = this.configService.get<string>('EXTERNAL_LIGHTING_RTU_LIST_METHOD') ?? '/lightm/rtu/list';
    const faultListMethod = this.configService.get<string>('EXTERNAL_LIGHTING_FAULT_LIST_METHOD') ?? '/m9z/fault/list';
    const faultSyncBaseUrl = (this.configService.get<string>('EXTERNAL_LIGHTING_FAULT_BASE_URL') ?? 'http://171.208.94.46:7070')
      .trim()
      .replace(/\/$/, '');
    const faultSyncDays = Math.max(
      Number(options.faultDays ?? this.configService.get<string>('EXTERNAL_LIGHTING_FAULT_SYNC_DAYS') ?? 7),
      1,
    );
    const syncAssets =
      options.syncAssets === undefined ||
      options.syncAssets === true ||
      options.syncAssets === 'true' ||
      options.syncAssets === 1 ||
      options.syncAssets === '1';
    const syncRtu =
      this.configService.get<string>('EXTERNAL_LIGHTING_SYNC_RTU') !== '0' &&
      this.configService.get<string>('EXTERNAL_LIGHTING_SYNC_RTU') !== 'false';
    const syncFaults =
      options.syncFaults === undefined
        ? this.configService.get<string>('EXTERNAL_LIGHTING_SYNC_FAULTS') !== '0' &&
          this.configService.get<string>('EXTERNAL_LIGHTING_SYNC_FAULTS') !== 'false'
        : options.syncFaults === true ||
          options.syncFaults === 'true' ||
          options.syncFaults === 1 ||
          options.syncFaults === '1';
    const platformCode = this.configService.get<string>('EXTERNAL_LIGHTING_PLATFORM_CODE') ?? 'lighting-main';
    const defaultRegionCode = this.configService.get<string>('EXTERNAL_LIGHTING_DEFAULT_REGION_CODE') ?? '110101';

    const baseUrls = this.parseExternalLightingBaseUrls(baseUrlRaw);
    if (!baseUrls.length) {
      throw new BadRequestException('未配置智慧照明平台地址');
    }
    if (!syncAssets && !syncFaults) {
      throw new BadRequestException('请至少选择资产同步或报警同步');
    }
    if (!usernameRaw?.trim() || !passwordRaw?.trim()) {
      throw new BadRequestException('未配置智慧照明平台账号或密码');
    }

    const userParts = this.splitEnvCredentialList(usernameRaw);
    const passParts = this.splitEnvCredentialList(passwordRaw);
    const users = this.expandAlignedSegments(userParts, baseUrls.length, 'EXTERNAL_LIGHTING_USERNAME');
    const passwords = this.expandAlignedSegments(passParts, baseUrls.length, 'EXTERNAL_LIGHTING_PASSWORD');

    const tenantParts = tenantNameRaw?.trim() ? this.splitEnvCredentialList(tenantNameRaw) : [];
    const tenantNames = tenantParts.length
      ? this.expandAlignedSegments(tenantParts, baseUrls.length, 'EXTERNAL_LIGHTING_TENANT_NAME')
      : Array.from({ length: baseUrls.length }, () => undefined as string | undefined);

    const requestedBaseUrls = this.parseExternalLightingBaseUrls(
      Array.isArray(options.baseUrls) ? options.baseUrls.join(',') : options.baseUrls,
    );
    const requestedSet = new Set(requestedBaseUrls.map((baseUrl) => baseUrl.toLowerCase()));
    const platformConfigs = baseUrls
      .map((baseUrl, index) => ({
        baseUrl,
        user: users[index],
        password: passwords[index],
        tenantName: tenantNames[index],
      }))
      .filter((item) => !requestedSet.size || requestedSet.has(item.baseUrl.toLowerCase()));

    if (!platformConfigs.length) {
      throw new BadRequestException('未选择有效的智慧照明平台地址');
    }

    for (let i = 0; i < baseUrls.length; i++) {
      if (!users[i] || !passwords[i]) {
        throw new BadRequestException(`第 ${i + 1} 个智慧照明平台的用户名或密码为空`);
      }
    }

    const region = await this.ensureSyncRegion(defaultRegionCode);
    const multiBase = baseUrls.length > 1;

    const targets: Array<{
      baseUrl: string;
      platformCode: string;
      ilc?: SyncPageStats;
      rtu?: SyncPageStats;
      faults?: SyncPageStats & { days: number };
      total?: number;
      created?: number;
      updated?: number;
      skipped?: number;
      error?: string;
    }> = [];

    let created = 0;
    let updated = 0;
    let skipped = 0;
    let total = 0;

    for (const platform of platformConfigs) {
      const baseUrl = platform.baseUrl;
      const effectivePlatform = this.disambiguatedPlatformCode(platformCode, baseUrl, multiBase);
      const tenantName = platform.tenantName?.trim() || undefined;
      try {
        const token = await this.loginExternalPlatform(baseUrl, platform.user, platform.password, tenantName);
        const ilcStats = syncAssets
          ? await this.pullIlcPagesAndUpsert(
              baseUrl,
              ilcListMethod,
              token,
              user.tenantId,
              region.id,
              effectivePlatform,
            )
          : ({ total: 0, created: 0, updated: 0, skipped: 0 } satisfies SyncPageStats);

        const rtuStats =
          syncAssets && syncRtu
            ? await this.pullRtuPagesAndUpsert(
                baseUrl,
                rtuListMethod,
                token,
                user.tenantId,
                region.id,
                effectivePlatform,
              )
            : ({ total: 0, created: 0, updated: 0, skipped: 0 } satisfies SyncPageStats);

        const faultStats =
          syncFaults && this.sameExternalBaseUrl(baseUrl, faultSyncBaseUrl)
            ? await this.pullRecentFaultPagesAndUpsert(
                baseUrl,
                rtuListMethod,
                faultListMethod,
                token,
                user.tenantId,
                effectivePlatform,
                faultSyncDays,
              )
            : undefined;

        total += ilcStats.total + rtuStats.total;
        created += ilcStats.created + rtuStats.created;
        updated += ilcStats.updated + rtuStats.updated;
        skipped += ilcStats.skipped + rtuStats.skipped;

        targets.push({
          baseUrl,
          platformCode: effectivePlatform,
          ilc: ilcStats,
          rtu: rtuStats,
          ...(faultStats ? { faults: { ...faultStats, days: faultSyncDays } } : {}),
          total: ilcStats.total + rtuStats.total,
          created: ilcStats.created + rtuStats.created,
          updated: ilcStats.updated + rtuStats.updated,
          skipped: ilcStats.skipped + rtuStats.skipped,
        });
      } catch (e) {
        const message = e instanceof Error ? e.message : String(e);
        targets.push({ baseUrl, platformCode: effectivePlatform, error: message });
      }
    }

    if (targets.length && targets.every((t) => t.error)) {
      throw new BadGatewayException(
        `所有智慧照明平台地址同步失败：${targets.map((t) => `${t.baseUrl}: ${t.error}`).join('; ')}`,
      );
    }

    return {
      baseUrlCount: platformConfigs.length,
      configuredBaseUrlCount: baseUrls.length,
      targets,
      total,
      created,
      updated,
      skipped,
      platformCode,
      regionCode: defaultRegionCode,
    };
  }

  /** 按逗号拆分账号、密码、租户名等配置段（不去除空段，便于与地址个数严格对齐）。 */
  private splitEnvCredentialList(raw: string): string[] {
    return raw.split(/[,，]/).map((s) => s.trim());
  }

  /**
   * 与 `EXTERNAL_LIGHTING_BASE_URL` 段数对齐：要么只配 1 段（全部地址共用），要么段数与地址个数相同。
   */
  private expandAlignedSegments(parts: string[], targetCount: number, envLabel: string): string[] {
    if (parts.length === targetCount) {
      return parts;
    }
    if (parts.length === 1) {
      return Array.from({ length: targetCount }, () => parts[0]);
    }
    throw new BadRequestException(
      `${envLabel} 须为 1 段（所有平台共用）或与 EXTERNAL_LIGHTING_BASE_URL 相同段数（当前 ${parts.length} 段，地址 ${targetCount} 个）`,
    );
  }

  /** 支持英文逗号或中文逗号分隔多个根地址，依次请求。 */
  private parseExternalLightingBaseUrls(raw?: string): string[] {
    if (!raw?.trim()) {
      return [];
    }
    return raw
      .split(/[,，]/)
      .map((s) => s.trim().replace(/\/$/, ''))
      .filter(Boolean);
  }

  /** 多平台时把主机名拼进业务前缀，避免不同平台上 ILC id 重复导致路灯 code 冲突。单地址时与旧行为一致。 */
  private disambiguatedPlatformCode(platformCode: string, baseUrl: string, multi: boolean): string {
    if (!multi) {
      return platformCode;
    }
    let host = 'node';
    try {
      host =
        new URL(baseUrl).hostname.replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'node';
    } catch {
      /* 非法 URL 时退回占位 */
    }
    return `${platformCode}-${host}`;
  }

  private sameExternalBaseUrl(a: string, b: string) {
    const normalize = (value: string) => value.trim().replace(/\/$/, '').toLowerCase();
    return normalize(a) === normalize(b);
  }

  private async pullIlcPagesAndUpsert(
    baseUrl: string,
    ilcListMethod: string,
    token: string,
    tenantId: string | null | undefined,
    regionId: string,
    effectivePlatformCode: string,
  ): Promise<{ total: number; created: number; updated: number; skipped: number }> {
    const pageSize = 100;
    let page = 1;
    let total = 0;
    let created = 0;
    let updated = 0;
    let skipped = 0;

    while (true) {
      const result = await this.callJsonRpc<GetIlcListResult>(baseUrl, ilcListMethod, {
        params: { page, limit: pageSize },
        session: token,
      });

      const rows = result.rows ?? [];
      total = result.count ?? total;
      if (!rows.length) {
        break;
      }

      for (const item of rows) {
        const syncResult = await this.upsertExternalIlc(item, tenantId, regionId, effectivePlatformCode);
        if (syncResult === 'created') {
          created += 1;
        } else if (syncResult === 'updated') {
          updated += 1;
        } else {
          skipped += 1;
        }
      }

      if (page * pageSize >= total || rows.length < pageSize) {
        break;
      }
      page += 1;
    }

    return { total, created, updated, skipped } satisfies SyncPageStats;
  }

  private async pullRtuPagesAndUpsert(
    baseUrl: string,
    rtuListMethod: string,
    token: string,
    tenantId: string | null | undefined,
    regionId: string,
    effectivePlatformCode: string,
  ): Promise<SyncPageStats> {
    const pageSize = 100;
    let page = 1;
    let total = 0;
    let created = 0;
    let updated = 0;
    let skipped = 0;

    while (true) {
      const result = await this.callJsonRpc<GetIlcListResult>(baseUrl, rtuListMethod, {
        params: { page, limit: pageSize },
        session: token,
      });

      const rows = (result.rows ?? []) as unknown as ExternalRtuItem[];
      total = result.count ?? total;
      if (!rows.length) {
        break;
      }

      for (const item of rows) {
        const syncResult = await this.upsertExternalRtu(item, tenantId, regionId, effectivePlatformCode);
        if (syncResult === 'created') {
          created += 1;
        } else if (syncResult === 'updated') {
          updated += 1;
        } else {
          skipped += 1;
        }
      }

      if (page * pageSize >= total || rows.length < pageSize) {
        break;
      }
      page += 1;
    }

    return { total, created, updated, skipped } satisfies SyncPageStats;
  }

  private async pullRecentFaultPagesAndUpsert(
    baseUrl: string,
    rtuListMethod: string,
    faultListMethod: string,
    token: string,
    tenantId: string | null | undefined,
    effectivePlatformCode: string,
    days: number,
  ): Promise<SyncPageStats> {
    const commUidToLamp = await this.buildRtuCommUidLampMap(baseUrl, rtuListMethod, token, effectivePlatformCode);
    const pageSize = 200;
    const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
    const first = await this.callJsonRpc<GetFaultListResult>(baseUrl, faultListMethod, {
      params: { page: 1, limit: 1 },
      session: token,
    });
    const pageCount = Math.max(Math.ceil((first.count ?? 0) / pageSize), 1);

    let total = 0;
    let created = 0;
    let updated = 0;
    let skipped = 0;

    for (let page = pageCount; page >= 1; page -= 1) {
      const result = await this.callJsonRpc<GetFaultListResult>(baseUrl, faultListMethod, {
        params: { page, limit: pageSize },
        session: token,
      });
      const rows = result.rows ?? [];
      if (!rows.length) {
        continue;
      }

      let pageHasRecent = false;
      for (const item of rows) {
        const occurredAt = this.parseExternalDate(item.fault_time ?? item.faultTime);
        if (!occurredAt || occurredAt < cutoff) {
          continue;
        }
        pageHasRecent = true;
        total += 1;

        const syncResult = await this.upsertExternalFault(item, tenantId, effectivePlatformCode, commUidToLamp);
        if (syncResult === 'created') {
          created += 1;
        } else if (syncResult === 'updated') {
          updated += 1;
        } else {
          skipped += 1;
        }
      }

      if (!pageHasRecent && rows.every((item) => {
        const occurredAt = this.parseExternalDate(item.fault_time ?? item.faultTime);
        return occurredAt && occurredAt < cutoff;
      })) {
        break;
      }
    }

    return { total, created, updated, skipped } satisfies SyncPageStats;
  }

  private async buildRtuCommUidLampMap(
    baseUrl: string,
    rtuListMethod: string,
    token: string,
    effectivePlatformCode: string,
  ) {
    const pageSize = 500;
    let page = 1;
    const commUidToCode = new Map<string, string>();

    while (true) {
      const result = await this.callJsonRpc<GetIlcListResult>(baseUrl, rtuListMethod, {
        params: { page, limit: pageSize },
        session: token,
      });
      const rows = (result.rows ?? []) as unknown as ExternalRtuItem[];
      for (const item of rows) {
        if (item.commUid) {
          commUidToCode.set(String(item.commUid), `${effectivePlatformCode}-RTU-${item.id}`);
        }
      }
      if (!rows.length || page * pageSize >= (result.count ?? 0) || rows.length < pageSize) {
        break;
      }
      page += 1;
    }

    const lamps = await this.prisma.streetLamp.findMany({
      where: { code: { in: [...commUidToCode.values()] } },
      select: { id: true, code: true, regionId: true, tenantId: true },
    });
    const codeToLamp = new Map(lamps.map((lamp) => [lamp.code, lamp]));
    const commUidToLamp = new Map<string, (typeof lamps)[number]>();
    for (const [commUid, code] of commUidToCode.entries()) {
      const lamp = codeToLamp.get(code);
      if (lamp) {
        commUidToLamp.set(commUid, lamp);
      }
    }
    return commUidToLamp;
  }

  private async upsertExternalFault(
    item: ExternalFaultItem,
    tenantId: string | null | undefined,
    platformCode: string,
    commUidToLamp: Map<string, { id: string; regionId: string; tenantId: string | null }>,
  ) {
    const commUid = String(item.comm_uid ?? item.commUid ?? '').trim();
    const occurredAt = this.parseExternalDate(item.fault_time ?? item.faultTime);
    const lamp = commUid ? commUidToLamp.get(commUid) : undefined;
    if (!lamp || !occurredAt) {
      return 'skipped' as const;
    }

    const sourceAlarmId = `${platformCode}-FAULT-${item.id}`;
    const existing = await this.prisma.alarm.findUnique({ where: { sourceAlarmId } });
    const status = (item.operate_status ?? item.operateStatus) === 1 ? 'RESOLVED' : 'OPEN';
    const faultMsg = item.fault_msg ?? item.faultMsg ?? '设备故障';
    const faultCode = item.fault_code ?? item.faultCode ?? 'FAULT';
    const description = [
      `设备CUID：${commUid}`,
      `回路：${item.loop_idx ?? item.loopIdx ?? '-'}`,
      `电压：${item.voltage ?? '-'}`,
      `故障码：${faultCode}`,
    ].join('；');

    await this.prisma.alarm.upsert({
      where: { sourceAlarmId },
      update: {
        status,
        title: faultMsg,
        description,
        occurredAt,
        recoveredAt: status === 'RESOLVED' ? new Date() : null,
      },
      create: {
        sourceAlarmId,
        tenantId: tenantId ?? lamp.tenantId,
        lampId: lamp.id,
        regionId: lamp.regionId,
        type: String(faultCode),
        title: faultMsg,
        description,
        level: this.mapFaultLevel(faultMsg),
        status,
        occurredAt,
        recoveredAt: status === 'RESOLVED' ? new Date() : undefined,
        events: { create: { eventType: 'SYNCED_FROM_LIGHTING_FAULT', payload: item as unknown as Prisma.InputJsonValue } },
      },
    });

    return existing ? ('updated' as const) : ('created' as const);
  }

  private async loginExternalPlatform(baseUrl: string, username: string, password: string, tenantName?: string) {
    const result = await this.callJsonRpc<LightingLoginResult>(baseUrl, '/account/login', {
      params: {
        ...(tenantName ? { tenant_name: tenantName } : {}),
        username,
        password,
      },
    });

    if (!result.token) {
      throw new BadGatewayException('智慧照明平台登录成功但未返回 token');
    }
    return result.token;
  }

  private async ensureSyncRegion(regionCode: string) {
    const existing = await this.prisma.region.findUnique({ where: { code: regionCode } });
    if (existing) {
      return existing;
    }

    const china = await this.prisma.region.upsert({
      where: { code: '100000' },
      update: {},
      create: { code: '100000', name: '全国', level: 0, longitude: 104.195397, latitude: 35.86166 },
    });

    if (regionCode === '321084') {
      const jiangsu = await this.prisma.region.upsert({
        where: { code: '320000' },
        update: {},
        create: { code: '320000', name: '江苏省', level: 1, parentId: china.id, longitude: 118.763232, latitude: 32.061707 },
      });
      const yangzhou = await this.prisma.region.upsert({
        where: { code: '321000' },
        update: {},
        create: { code: '321000', name: '扬州市', level: 2, parentId: jiangsu.id, longitude: 119.412966, latitude: 32.39421 },
      });
      return this.prisma.region.upsert({
        where: { code: '321084' },
        update: {},
        create: { code: '321084', name: '高邮市', level: 3, parentId: yangzhou.id, longitude: 119.459161, latitude: 32.781659 },
      });
    }

    return this.prisma.region.create({
      data: {
        code: regionCode,
        name: `同步区域 ${regionCode}`,
        level: 3,
        parentId: china.id,
      },
    });
  }

  private async callJsonRpc<T>(
    baseUrl: string,
    method: string,
    options: { params?: Record<string, unknown>; session?: string },
  ) {
    const url = `${baseUrl.replace(/\/$/, '')}/jsonrpc`;
    const response = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        jsonrpc: '2.0',
        method,
        params: options.params ?? {},
        ...(options.session ? { session: options.session } : {}),
      }),
    });

    if (!response.ok) {
      throw new BadGatewayException(`智慧照明平台请求失败：HTTP ${response.status}`);
    }

    const payload = (await response.json()) as JsonRpcResponse<T>;
    if (payload.error && payload.error.code !== 200) {
      throw new BadGatewayException(`智慧照明平台返回错误：${payload.error.message}`);
    }
    if (!payload.result) {
      throw new BadGatewayException(`智慧照明平台 ${method} 未返回数据`);
    }

    return payload.result;
  }

  private async upsertExternalRtu(
    item: ExternalRtuItem,
    tenantId: string | null | undefined,
    regionId: string,
    platformCode: string,
  ) {
    const code = `${platformCode}-RTU-${item.id}`;
    const legacyCode = `RTU-${item.id}`;
    const longitude = Number(item.positionX);
    const latitude = Number(item.positionY);

    if (!code || !Number.isFinite(longitude) || !Number.isFinite(latitude)) {
      return 'skipped' as const;
    }

    const existing = await this.prisma.streetLamp.findFirst({ where: { code: { in: [code, legacyCode] } } });
    const status = this.mapWorkState(item.workState);
    const onlineStatus = (item.isOnline ?? Boolean(item.lastOnlineTime)) ? 'ONLINE' : 'OFFLINE';
    const lastReportedAt = this.parseExternalDate(item.lastOnlineTime);
    const address = item.description || item.lampPost || item.lamp_post || undefined;
    const deviceType = '监控终端';
    const matchedRegionId = (await this.regionScope.findNearestRegionId(longitude, latitude)) ?? regionId;

    if (existing && existing.code !== code) {
      await this.prisma.streetLamp.update({
        where: { id: existing.id },
        data: { code },
      });
    }

    if (item.commUid && item.commUid !== code) {
      await this.prisma.streetLamp.deleteMany({
        where: {
          code: { in: [item.commUid, legacyCode] },
          id: existing ? { not: existing.id } : undefined,
          deviceType: { startsWith: '监控终端' },
          alarms: { none: {} },
        },
      });
    }

    await this.prisma.streetLamp.upsert({
      where: { code },
      update: {
        name: item.rtuName || item.commUid || code,
        regionId: matchedRegionId,
        address,
        longitude,
        latitude,
        deviceType,
        status,
        onlineStatus,
        isPowerOn: null,
        lastReportedAt,
      },
      create: {
        code,
        name: item.rtuName || item.commUid || code,
        tenantId,
        regionId: matchedRegionId,
        address,
        longitude,
        latitude,
        deviceType,
        status,
        onlineStatus,
        isPowerOn: null,
        lastReportedAt,
      },
    });

    return existing ? ('updated' as const) : ('created' as const);
  }

  private async upsertExternalIlc(
    item: ExternalIlcItem,
    tenantId: string | null | undefined,
    regionId: string,
    platformCode: string,
  ) {
    const code = `${platformCode}-ILC-${item.id}`;
    const legacyCode = `ILC-${item.id}`;
    const longitude = Number(item.positionX);
    const latitude = Number(item.positionY);

    if (!code || !Number.isFinite(longitude) || !Number.isFinite(latitude)) {
      return 'skipped' as const;
    }

    const existing = await this.prisma.streetLamp.findFirst({ where: { code: { in: [code, legacyCode] } } });
    const status = this.mapWorkState(item.workState);
    const onlineStatus = (item.isOnline ?? Boolean(item.lastOnlineTime)) ? 'ONLINE' : 'OFFLINE';
    const lastReportedAt = this.parseExternalDate(item.lastOnlineTime);
    const address = item.description || item.lampPost || item.lamp_post || undefined;
    const deviceType = '单灯';
    const matchedRegionId = (await this.regionScope.findNearestRegionId(longitude, latitude)) ?? regionId;

    if (existing && existing.code !== code) {
      await this.prisma.streetLamp.update({
        where: { id: existing.id },
        data: { code },
      });
    }

    if (item.commUid && item.commUid !== code) {
      await this.prisma.streetLamp.deleteMany({
        where: {
          code: { in: [item.commUid, legacyCode] },
          id: existing ? { not: existing.id } : undefined,
          deviceType: { not: '监控终端' },
          alarms: { none: {} },
        },
      });
    }

    await this.prisma.streetLamp.upsert({
      where: { code },
      update: {
        name: item.ilcName || item.commUid || code,
        regionId: matchedRegionId,
        address,
        longitude,
        latitude,
        deviceType,
        status,
        onlineStatus,
        isPowerOn: item.isPowerOn,
        lastReportedAt,
      },
      create: {
        code,
        name: item.ilcName || item.commUid || code,
        tenantId,
        regionId: matchedRegionId,
        address,
        longitude,
        latitude,
        deviceType,
        status,
        onlineStatus,
        isPowerOn: item.isPowerOn,
        lastReportedAt,
      },
    });

    return existing ? ('updated' as const) : ('created' as const);
  }

  private mapWorkState(workState?: number) {
    if (workState === 0) {
      return 'NORMAL' as const;
    }
    if (workState === 2) {
      return 'MAINTENANCE' as const;
    }
    if (workState === 1) {
      return 'OFFLINE' as const;
    }
    return 'FAULT' as const;
  }

  private mapFaultLevel(message?: string) {
    if (message && /电源|欠压|过压|断电|离线|短路|漏电/.test(message)) {
      return 'HIGH' as const;
    }
    return 'MEDIUM' as const;
  }

  private normalizeDeviceType(value: unknown) {
    if (value === undefined) {
      return undefined;
    }
    return value === '监控终端' ? '监控终端' : '单灯';
  }

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