import * as plugins from '../../plugins.js';
import type {
  IRainbirdCommand,
  IRainbirdCommandResult,
  IRainbirdConfig,
  IRainbirdController,
  IRainbirdEvent,
  IRainbirdHttpLocalCommandShape,
  IRainbirdLocalJsonRpcResponse,
  IRainbirdModelAndVersion,
  IRainbirdSnapshot,
  IRainbirdTunnelSipResponse,
  IRainbirdWifiParams,
  IRainbirdZone,
  TRainbirdProtocol,
} from './rainbird.types.js';

const defaultTimeoutMs = 20000;
const defaultIrrigationDurationMinutes = 6;
const rainbirdManufacturer = 'Rain Bird';

const rainbirdHeaders = {
  'accept-language': 'en',
  'accept-encoding': 'gzip, deflate',
  'user-agent': 'RainBird/2.0 CFNetwork/811.5.4 Darwin/16.7.0',
  accept: '*/*',
  connection: 'keep-alive',
  'content-type': 'application/octet-stream',
};

const modelInfoById: Record<string, Omit<IRainbirdModelAndVersion, 'protocolRevisionMajor' | 'protocolRevisionMinor'>> = {
  '0003': { modelId: '0003', modelCode: 'ESP_RZXe', modelName: 'ESP-RZXe', supportsWaterBudget: false, maxPrograms: 0, maxRunTimes: 6, maxStations: 8 },
  '0007': { modelId: '0007', modelCode: 'ESP_ME', modelName: 'ESP-Me', supportsWaterBudget: true, maxPrograms: 4, maxRunTimes: 6, maxStations: 22 },
  '0006': { modelId: '0006', modelCode: 'ST8X_WF', modelName: 'ST8x-WiFi', supportsWaterBudget: false, maxPrograms: 0, maxRunTimes: 6, maxStations: 8 },
  '0005': { modelId: '0005', modelCode: 'ESP_TM2', modelName: 'ESP-TM2', supportsWaterBudget: true, maxPrograms: 3, maxRunTimes: 4, maxStations: 12 },
  '0008': { modelId: '0008', modelCode: 'ST8X_WF2', modelName: 'ST8x-WiFi2', supportsWaterBudget: false, maxPrograms: 8, maxRunTimes: 6, maxStations: 8 },
  '0009': { modelId: '0009', modelCode: 'ESP_ME3', modelName: 'ESP-ME3', supportsWaterBudget: true, maxPrograms: 4, maxRunTimes: 6, maxStations: 22 },
  '0010': { modelId: '0010', modelCode: 'MOCK_ESP_ME2', modelName: 'ESP=Me2', supportsWaterBudget: true, maxPrograms: 4, maxRunTimes: 6, maxStations: 22 },
  '000a': { modelId: '000a', modelCode: 'ESP_TM2v2', modelName: 'ESP-TM2', supportsWaterBudget: true, maxPrograms: 3, maxRunTimes: 4, maxStations: 12 },
  '010a': { modelId: '010a', modelCode: 'ESP_TM2v3', modelName: 'ESP-TM2', supportsWaterBudget: true, maxPrograms: 3, maxRunTimes: 4, maxStations: 12 },
  '0099': { modelId: '0099', modelCode: 'TBOS_BT', modelName: 'TBOS-BT', supportsWaterBudget: true, maxPrograms: 3, maxRunTimes: 8, maxStations: 8 },
  '0100': { modelId: '0100', modelCode: 'TBOS_BT', modelName: 'TBOS-BT', supportsWaterBudget: true, maxPrograms: 3, maxRunTimes: 8, maxStations: 8 },
  '0107': { modelId: '0107', modelCode: 'ESP_MEv2', modelName: 'ESP-Me', supportsWaterBudget: true, maxPrograms: 4, maxRunTimes: 6, maxStations: 22 },
  '0103': { modelId: '0103', modelCode: 'ESP_RZXe2', modelName: 'ESP-RZXe2', supportsWaterBudget: false, maxPrograms: 8, maxRunTimes: 6, maxStations: 8 },
  '0812': { modelId: '0812', modelCode: 'RC2', modelName: 'RC2', supportsWaterBudget: true, maxPrograms: 3, maxRunTimes: 4, maxStations: 12 },
  '0813': { modelId: '0813', modelCode: 'ARC8', modelName: 'ARC8', supportsWaterBudget: true, maxPrograms: 3, maxRunTimes: 4, maxStations: 12 },
  '0014': { modelId: '0014', modelCode: 'TM2R', modelName: 'TM2R', supportsWaterBudget: true, maxPrograms: 3, maxRunTimes: 4, maxStations: 12 },
  '0015': { modelId: '0015', modelCode: 'TRU', modelName: 'TRU', supportsWaterBudget: true, maxPrograms: 3, maxRunTimes: 4, maxStations: 12 },
  '0011': { modelId: '0011', modelCode: 'ESP_2WIRE', modelName: 'ESP-2WIRE', supportsWaterBudget: true, maxPrograms: 4, maxRunTimes: 6, maxStations: 50 },
  '000c': { modelId: '000c', modelCode: 'LXME2', modelName: 'LXME2', supportsWaterBudget: true, maxPrograms: 40, maxRunTimes: 10, maxStations: 22 },
  '000d': { modelId: '000d', modelCode: 'LX_IVM', modelName: 'LX-IVM', supportsWaterBudget: true, maxPrograms: 10, maxRunTimes: 8, maxStations: 22 },
  '000e': { modelId: '000e', modelCode: 'LX_IVM_PRO', modelName: 'LX-IVM Pro', supportsWaterBudget: true, maxPrograms: 40, maxRunTimes: 8, maxStations: 22 },
};

type TRainbirdEventHandler = (eventArg: IRainbirdEvent) => void;

export class RainbirdApiError extends Error {
  constructor(messageArg: string, public readonly status?: number) {
    super(messageArg);
    this.name = 'RainbirdApiError';
  }
}

export class RainbirdUnsupportedError extends Error {
  constructor(messageArg: string) {
    super(messageArg);
    this.name = 'RainbirdUnsupportedError';
  }
}

export class RainbirdClient {
  private snapshot?: IRainbirdSnapshot;
  private readonly events: IRainbirdEvent[] = [];
  private readonly eventHandlers = new Set<TRainbirdEventHandler>();
  private readonly localClient: RainbirdLocalApiClient;

  constructor(private readonly config: IRainbirdConfig) {
    this.localClient = new RainbirdLocalApiClient(config);
  }

  public async getSnapshot(): Promise<IRainbirdSnapshot> {
    if (this.config.snapshot) {
      this.snapshot = this.normalizeSnapshot(this.cloneSnapshot(this.config.snapshot));
      return this.snapshot;
    }

    if (this.config.host && this.config.password) {
      this.snapshot = this.normalizeSnapshot(await this.fetchSnapshot());
      return this.snapshot;
    }

    if (this.hasManualSnapshotData()) {
      this.snapshot = this.normalizeSnapshot(this.snapshotFromConfig(this.config.connected ?? true));
      return this.snapshot;
    }

    this.snapshot = this.normalizeSnapshot(this.snapshotFromConfig(false));
    return this.snapshot;
  }

  public onEvent(handlerArg: TRainbirdEventHandler): () => void {
    this.eventHandlers.add(handlerArg);
    return () => this.eventHandlers.delete(handlerArg);
  }

  public async sendCommand(commandArg: IRainbirdCommand): Promise<IRainbirdCommandResult> {
    this.emit({ type: 'command_mapped', command: commandArg, zoneId: this.zoneId(commandArg), timestamp: Date.now() });

    try {
      const result = await this.executeCommand(commandArg);
      this.emit({
        type: result.success ? 'command_executed' : 'command_failed',
        command: commandArg,
        zoneId: this.zoneId(commandArg),
        data: result,
        timestamp: Date.now(),
      });
      return result;
    } catch (errorArg) {
      const result = { success: false, error: errorArg instanceof Error ? errorArg.message : String(errorArg), data: { command: commandArg } };
      this.emit({ type: 'command_failed', command: commandArg, zoneId: this.zoneId(commandArg), data: result, timestamp: Date.now() });
      return result;
    }
  }

  public async refresh(): Promise<IRainbirdSnapshot> {
    this.snapshot = undefined;
    const snapshot = await this.getSnapshot();
    this.emit({ type: 'snapshot_refreshed', data: snapshot, timestamp: Date.now() });
    return snapshot;
  }

  public localCommandShape(methodArg: string, paramsArg: Record<string, unknown> = {}): IRainbirdHttpLocalCommandShape {
    return {
      endpoint: '/stick',
      method: 'POST',
      contentType: 'application/octet-stream',
      jsonRpcMethod: methodArg,
      encrypted: Boolean(this.config.password),
      params: paramsArg,
    };
  }

  public async destroy(): Promise<void> {
    this.eventHandlers.clear();
  }

  private async executeCommand(commandArg: IRainbirdCommand): Promise<IRainbirdCommandResult> {
    if (this.config.commandExecutor) {
      return this.commandResult(await this.config.commandExecutor(commandArg), commandArg);
    }

    if (commandArg.type === 'refresh') {
      return { success: true, data: await this.refresh() };
    }

    if (commandArg.type === 'raw_local_rpc') {
      return { success: true, data: await this.localClient.request(commandArg.method, commandArg.params) };
    }

    this.assertLiveLocalControl();
    if (commandArg.type === 'start_zone') {
      await this.localClient.irrigateZone(commandArg.zoneId, commandArg.durationMinutes);
      this.patchZone(commandArg.zoneId, true);
      this.emit({ type: 'zone_started', command: commandArg, zoneId: commandArg.zoneId, timestamp: Date.now() });
      return { success: true };
    }
    if (commandArg.type === 'stop_zone') {
      await this.localClient.stopIrrigation();
      this.patchZone(commandArg.zoneId, false);
      this.emit({ type: 'zone_stopped', command: commandArg, zoneId: commandArg.zoneId, timestamp: Date.now() });
      return { success: true };
    }
    if (commandArg.type === 'set_rain_delay') {
      await this.localClient.setRainDelay(commandArg.days);
      this.patchRainDelay(commandArg.days);
      this.emit({ type: 'rain_delay_set', command: commandArg, data: { days: commandArg.days }, timestamp: Date.now() });
      return { success: true };
    }
    if (commandArg.type === 'start_program') {
      await this.localClient.startProgram(commandArg.programId);
      return { success: true };
    }
    return { success: false, error: `Unsupported Rain Bird command: ${(commandArg as { type: string }).type}` };
  }

  private async fetchSnapshot(): Promise<IRainbirdSnapshot> {
    const updatedAt = new Date().toISOString();
    const model = await this.localClient.getModelAndVersion();
    const serialNumber = await this.optional(() => this.localClient.getSerialNumber());
    const wifiParams = await this.optional(() => this.localClient.getWifiParams());
    const availableZoneIds = await this.localClient.getAvailableStations(model.maxStations);
    const activeZoneIds = await this.localClient.getZoneStates(model.maxStations);
    const rainSensorActive = await this.localClient.getRainSensorState();
    const rainDelayDays = await this.localClient.getRainDelay();
    const currentIrrigation = await this.optional(() => this.localClient.getCurrentIrrigation());
    const firmwareVersion = await this.optional(() => this.localClient.getControllerFirmwareVersion());
    const id = this.config.uniqueId || this.normalizeMac(wifiParams?.macAddress) || serialNumber || this.config.host || 'rainbird-controller';
    const controller: IRainbirdController = {
      id,
      name: this.config.name || `${rainbirdManufacturer} Controller`,
      manufacturer: rainbirdManufacturer,
      modelId: model.modelId,
      modelCode: model.modelCode,
      modelName: this.config.model || model.modelName,
      serialNumber: serialNumber || this.config.serialNumber,
      macAddress: this.normalizeMac(wifiParams?.macAddress || this.config.macAddress) || undefined,
      firmwareVersion,
      protocolRevision: `${model.protocolRevisionMajor}.${model.protocolRevisionMinor}`,
      host: this.config.host,
      port: this.config.port,
      protocol: this.config.protocol || 'auto',
      online: true,
      maxPrograms: model.maxPrograms,
      maxRunTimes: model.maxRunTimes,
      maxStations: model.maxStations,
      supportsWaterBudget: model.supportsWaterBudget,
      rainSensorActive,
      rainDelayDays,
      currentIrrigation,
      rssi: wifiParams?.rssi,
      wifiSsid: wifiParams?.wifiSsid,
      localIpAddress: wifiParams?.localIpAddress,
      localGateway: wifiParams?.localGateway,
      availableZoneIds,
      activeZoneIds,
    };
    const zones = availableZoneIds.map((zoneIdArg) => ({
      id: zoneIdArg,
      name: this.config.zones?.find((zoneArg) => zoneArg.id === zoneIdArg)?.name || `Sprinkler ${zoneIdArg}`,
      available: true,
      active: activeZoneIds.includes(zoneIdArg),
      defaultDurationMinutes: this.config.defaultIrrigationDurationMinutes || defaultIrrigationDurationMinutes,
    }));

    return {
      controller,
      zones,
      programs: this.config.programs || this.config.schedule?.programs || [],
      schedule: this.config.schedule,
      events: [...(this.config.events || []), ...this.events],
      connected: true,
      updatedAt,
      raw: { model, wifiParams },
    };
  }

  private snapshotFromConfig(connectedArg: boolean): IRainbirdSnapshot {
    const updatedAt = new Date().toISOString();
    const controller: IRainbirdController = {
      id: this.config.controller?.id || this.config.uniqueId || this.normalizeMac(this.config.macAddress) || this.config.serialNumber || this.config.host || 'rainbird-controller',
      name: this.config.controller?.name || this.config.name || `${rainbirdManufacturer} Controller`,
      manufacturer: rainbirdManufacturer,
      modelName: this.config.controller?.modelName || this.config.model,
      serialNumber: this.config.controller?.serialNumber || this.config.serialNumber,
      macAddress: this.normalizeMac(this.config.controller?.macAddress || this.config.macAddress) || undefined,
      host: this.config.controller?.host || this.config.host,
      port: this.config.controller?.port || this.config.port,
      protocol: this.config.controller?.protocol || this.config.protocol || 'auto',
      online: connectedArg,
      rainSensorActive: this.config.controller?.rainSensorActive,
      rainDelayDays: this.config.controller?.rainDelayDays,
      availableZoneIds: this.config.controller?.availableZoneIds,
      activeZoneIds: this.config.controller?.activeZoneIds,
      ...this.config.controller,
    };
    const zones = this.zonesFromConfig(controller);
    const programs = this.config.programs || this.config.schedule?.programs || [];
    return {
      controller,
      zones,
      programs,
      schedule: this.config.schedule,
      events: [...(this.config.events || []), ...this.events],
      connected: connectedArg,
      updatedAt,
    };
  }

  private zonesFromConfig(controllerArg: IRainbirdController): IRainbirdZone[] {
    if (this.config.zones?.length) {
      return this.config.zones.map((zoneArg) => ({
        defaultDurationMinutes: this.config.defaultIrrigationDurationMinutes || defaultIrrigationDurationMinutes,
        available: true,
        ...zoneArg,
      }));
    }
    const zoneIds = controllerArg.availableZoneIds || (this.config.zoneCount ? Array.from({ length: this.config.zoneCount }, (_value, indexArg) => indexArg + 1) : []);
    return zoneIds.map((zoneIdArg) => ({
      id: zoneIdArg,
      name: `Sprinkler ${zoneIdArg}`,
      available: true,
      active: controllerArg.activeZoneIds?.includes(zoneIdArg) || false,
      defaultDurationMinutes: this.config.defaultIrrigationDurationMinutes || defaultIrrigationDurationMinutes,
    }));
  }

  private normalizeSnapshot(snapshotArg: IRainbirdSnapshot): IRainbirdSnapshot {
    const controller = {
      manufacturer: rainbirdManufacturer,
      name: `${rainbirdManufacturer} Controller`,
      ...snapshotArg.controller,
    };
    controller.id = controller.id || this.config.uniqueId || this.normalizeMac(controller.macAddress) || controller.serialNumber || controller.host || 'rainbird-controller';
    controller.macAddress = this.normalizeMac(controller.macAddress) || controller.macAddress;
    controller.online = snapshotArg.connected && controller.online !== false;
    const activeZoneIds = controller.activeZoneIds || snapshotArg.zones.filter((zoneArg) => zoneArg.active).map((zoneArg) => zoneArg.id);
    const zones = this.uniqueZones(snapshotArg.zones).map((zoneArg) => ({
      name: `Sprinkler ${zoneArg.id}`,
      available: true,
      defaultDurationMinutes: this.config.defaultIrrigationDurationMinutes || defaultIrrigationDurationMinutes,
      ...zoneArg,
      active: zoneArg.active ?? activeZoneIds.includes(zoneArg.id),
    }));
    controller.availableZoneIds = controller.availableZoneIds || zones.map((zoneArg) => zoneArg.id);
    controller.activeZoneIds = zones.filter((zoneArg) => zoneArg.active).map((zoneArg) => zoneArg.id);
    return {
      ...snapshotArg,
      controller,
      zones,
      programs: snapshotArg.programs || snapshotArg.schedule?.programs || [],
      events: snapshotArg.events || [],
      connected: snapshotArg.connected && controller.online !== false,
      updatedAt: snapshotArg.updatedAt || new Date().toISOString(),
    };
  }

  private uniqueZones(zonesArg: IRainbirdZone[]): IRainbirdZone[] {
    const map = new Map<number, IRainbirdZone>();
    for (const zone of zonesArg) {
      map.set(zone.id, { ...map.get(zone.id), ...zone });
    }
    return [...map.values()].sort((leftArg, rightArg) => leftArg.id - rightArg.id);
  }

  private cloneSnapshot(snapshotArg: IRainbirdSnapshot): IRainbirdSnapshot {
    return JSON.parse(JSON.stringify(snapshotArg)) as IRainbirdSnapshot;
  }

  private hasManualSnapshotData(): boolean {
    return Boolean(this.config.controller || this.config.zones?.length || this.config.programs?.length || this.config.schedule || this.config.events?.length);
  }

  private patchZone(zoneIdArg: number | undefined, activeArg: boolean): void {
    if (!this.snapshot) {
      return;
    }
    for (const zone of this.snapshot.zones) {
      if (zoneIdArg === undefined || zone.id === zoneIdArg) {
        zone.active = zoneIdArg === undefined ? false : activeArg;
        if (activeArg) {
          zone.lastStartedAt = new Date().toISOString();
        } else {
          zone.lastStoppedAt = new Date().toISOString();
        }
      }
    }
    this.snapshot.controller.activeZoneIds = this.snapshot.zones.filter((zoneArg) => zoneArg.active).map((zoneArg) => zoneArg.id);
  }

  private patchRainDelay(daysArg: number): void {
    if (!this.snapshot) {
      return;
    }
    this.snapshot.controller.rainDelayDays = daysArg;
    if (this.snapshot.schedule) {
      this.snapshot.schedule.rainDelayDays = daysArg;
    }
  }

  private assertLiveLocalControl(): void {
    if (!this.config.host || !this.config.password) {
      throw new RainbirdUnsupportedError('Rain Bird live local control requires config.host and config.password, or provide commandExecutor for manual snapshots.');
    }
  }

  private commandResult(resultArg: unknown, commandArg: IRainbirdCommand): IRainbirdCommandResult {
    if (this.isCommandResult(resultArg)) {
      return resultArg;
    }
    return { success: true, data: resultArg ?? { command: commandArg } };
  }

  private isCommandResult(valueArg: unknown): valueArg is IRainbirdCommandResult {
    return typeof valueArg === 'object' && valueArg !== null && 'success' in valueArg;
  }

  private emit(eventArg: IRainbirdEvent): void {
    this.events.push(eventArg);
    for (const handler of this.eventHandlers) {
      handler(eventArg);
    }
  }

  private zoneId(commandArg: IRainbirdCommand): number | undefined {
    return commandArg.type === 'start_zone' || commandArg.type === 'stop_zone' ? commandArg.zoneId : undefined;
  }

  private async optional<TValue>(getterArg: () => Promise<TValue>): Promise<TValue | undefined> {
    try {
      return await getterArg();
    } catch {
      return undefined;
    }
  }

  private normalizeMac(valueArg?: string): string {
    const cleaned = (valueArg || '').replace(/[^a-fA-F0-9]/g, '').toLowerCase();
    return cleaned.length === 12 ? cleaned : '';
  }
}

export class RainbirdLocalApiClient {
  constructor(private readonly config: IRainbirdConfig) {}

  public async request<TResult = unknown>(methodArg: string, paramsArg: Record<string, unknown> = {}): Promise<TResult> {
    if (!this.config.host) {
      throw new RainbirdUnsupportedError('Rain Bird local API requests require config.host.');
    }
    if (!this.config.password) {
      throw new RainbirdUnsupportedError('Rain Bird local API requests require config.password for AES payload encryption.');
    }

    const payload = this.encodeJsonRpc(methodArg, paramsArg);
    const urls = this.candidateUrls();
    let lastError: unknown;
    for (const url of urls) {
      try {
        const response = await this.fetchWithTimeout(url, payload);
        return this.decodeJsonRpc<TResult>(Buffer.from(await response.arrayBuffer()));
      } catch (errorArg) {
        lastError = errorArg;
        if ((this.config.protocol && this.config.protocol !== 'auto') || errorArg instanceof RainbirdApiError) {
          break;
        }
      }
    }
    throw lastError instanceof Error ? lastError : new RainbirdApiError(String(lastError));
  }

  public async getModelAndVersion(): Promise<IRainbirdModelAndVersion> {
    const response = await this.processSipCommand('ModelAndVersionRequest');
    const modelId = response.slice(2, 6).toLowerCase();
    const modelInfo = modelInfoById[modelId] || { modelId, modelCode: 'UNKNOWN', modelName: 'Unknown', supportsWaterBudget: false, maxPrograms: 0, maxRunTimes: 0, maxStations: 0 };
    return {
      ...modelInfo,
      modelId,
      protocolRevisionMajor: parseHex(response.slice(6, 8)),
      protocolRevisionMinor: parseHex(response.slice(8, 10)),
    };
  }

  public async getSerialNumber(): Promise<string> {
    const response = await this.processSipCommand('SerialNumberRequest');
    return response.slice(2, 18);
  }

  public async getControllerFirmwareVersion(): Promise<string> {
    const response = await this.processSipCommand('ControllerFirmwareVersionRequest');
    return `${parseHex(response.slice(2, 4))}.${parseHex(response.slice(4, 6))}.${parseHex(response.slice(6, 10))}`;
  }

  public async getWifiParams(): Promise<IRainbirdWifiParams> {
    const result = await this.request<Record<string, unknown>>('getWifiParams');
    return {
      ...result,
      macAddress: typeof result.macAddress === 'string' ? result.macAddress : undefined,
      localIpAddress: typeof result.localIpAddress === 'string' ? result.localIpAddress : undefined,
      localGateway: typeof result.localGateway === 'string' ? result.localGateway : undefined,
      localNetmask: typeof result.localNetmask === 'string' ? result.localNetmask : undefined,
      rssi: typeof result.rssi === 'number' ? result.rssi : undefined,
      wifiSsid: typeof result.wifiSsid === 'string' ? result.wifiSsid : undefined,
      stickVersion: typeof result.stickVersion === 'string' ? result.stickVersion : undefined,
    };
  }

  public async getAvailableStations(maxStationsArg = 32): Promise<number[]> {
    const pages = Math.max(1, Math.ceil(maxStationsArg / 32));
    let mask = '';
    for (let page = 0; page < pages; page++) {
      const response = await this.processSipCommand('AvailableStationsRequest', page);
      mask += response.slice(4, 12);
    }
    return activeSet(mask, maxStationsArg || mask.length * 4);
  }

  public async getZoneStates(maxStationsArg = 32): Promise<number[]> {
    const pages = Math.max(1, Math.ceil(maxStationsArg / 32));
    let mask = '';
    for (let page = 0; page < pages; page++) {
      const response = await this.processSipCommand('CurrentStationsActiveRequest', page);
      mask += response.slice(4, 12);
    }
    return activeSet(mask, maxStationsArg || mask.length * 4);
  }

  public async getRainSensorState(): Promise<boolean> {
    const response = await this.processSipCommand('CurrentRainSensorStateRequest');
    return parseHex(response.slice(2, 4)) !== 0;
  }

  public async getRainDelay(): Promise<number> {
    const response = await this.processSipCommand('RainDelayGetRequest');
    return parseHex(response.slice(2, 6));
  }

  public async getCurrentIrrigation(): Promise<boolean> {
    const response = await this.processSipCommand('CurrentIrrigationStateRequest');
    return parseHex(response.slice(2, 4)) !== 0;
  }

  public async irrigateZone(zoneIdArg: number, minutesArg: number): Promise<void> {
    await this.processSipCommand('ManuallyRunStationRequest', zoneIdArg, minutesArg);
  }

  public async stopIrrigation(): Promise<void> {
    await this.processSipCommand('StopIrrigationRequest');
  }

  public async setRainDelay(daysArg: number): Promise<void> {
    await this.processSipCommand('RainDelaySetRequest', daysArg);
  }

  public async startProgram(programIdArg: number): Promise<void> {
    await this.processSipCommand('ManuallyRunProgramRequest', programIdArg);
  }

  public async retrieveScheduleRaw(commandCodeArg: string | number): Promise<string> {
    return await this.processSipCommand('RetrieveScheduleRequest', typeof commandCodeArg === 'string' ? parseInt(commandCodeArg, 16) : commandCodeArg);
  }

  private async processSipCommand(commandArg: keyof typeof sipCommands, ...argsArg: number[]): Promise<string> {
    const command = sipCommands[commandArg];
    const data = encodeSipCommand(commandArg, ...argsArg);
    const result = await this.request<IRainbirdTunnelSipResponse>('tunnelSip', { data, length: command.length });
    if (!result.data || typeof result.data !== 'string') {
      throw new RainbirdApiError("Rain Bird tunnelSip response is missing required 'data' field.");
    }
    const responseCode = result.data.slice(0, 2).toUpperCase();
    if (responseCode === '00') {
      throw new RainbirdUnsupportedError(`Rain Bird controller returned NACK for ${commandArg}.`);
    }
    if (responseCode !== command.response) {
      throw new RainbirdApiError(`Unexpected Rain Bird response for ${commandArg}: expected ${command.response}, got ${responseCode}.`);
    }
    return result.data.toUpperCase();
  }

  private candidateUrls(): string[] {
    const host = this.hostWithoutScheme();
    const protocol = this.config.protocol || 'auto';
    if (protocol === 'http') {
      return [this.url('http', host)];
    }
    if (protocol === 'https') {
      return [this.url('https', host)];
    }
    return [this.url('https', host), this.url('http', host)];
  }

  private url(protocolArg: Exclude<TRainbirdProtocol, 'auto'>, hostArg: string): string {
    const hasExplicitPort = /^\[[^\]]+\]:\d+$/.test(hostArg) || (/^[^:]+:\d+$/.test(hostArg));
    const port = this.config.port && !hasExplicitPort ? `:${this.config.port}` : '';
    return `${protocolArg}://${hostArg}${port}/stick`;
  }

  private hostWithoutScheme(): string {
    const value = (this.config.host || '').trim().replace(/\/$/, '');
    try {
      const url = new URL(value);
      return url.host;
    } catch {
      return value.replace(/^https?:\/\//i, '');
    }
  }

  private async fetchWithTimeout(urlArg: string, payloadArg: Buffer): Promise<Response> {
    const abortController = new AbortController();
    const timeout = setTimeout(() => abortController.abort(), this.config.timeoutMs || defaultTimeoutMs);
    try {
      const response = await globalThis.fetch(urlArg, {
        method: 'POST',
        headers: rainbirdHeaders,
        body: payloadArg as BodyInit,
        signal: abortController.signal,
      });
      if (response.status === 503) {
        throw new RainbirdApiError('Rain Bird device is busy; wait and try again.', response.status);
      }
      if (response.status === 403) {
        throw new RainbirdApiError('Rain Bird controller denied authentication; check the local password.', response.status);
      }
      if (!response.ok) {
        throw new RainbirdApiError(`Rain Bird controller responded with HTTP ${response.status}.`, response.status);
      }
      return response;
    } finally {
      clearTimeout(timeout);
    }
  }

  private encodeJsonRpc(methodArg: string, paramsArg: Record<string, unknown>): Buffer {
    const request = JSON.stringify({ id: Date.now() / 1000, jsonrpc: '2.0', method: methodArg, params: paramsArg });
    const password = this.config.password;
    if (!password) {
      return Buffer.from(request, 'utf8');
    }
    const iv = plugins.crypto.randomBytes(16);
    const key = plugins.crypto.createHash('sha256').update(Buffer.from(password, 'utf8')).digest();
    const plaintext = Buffer.from(addPadding(`${request}\x00\x10`), 'utf8');
    const checksum = plugins.crypto.createHash('sha256').update(Buffer.from(request, 'utf8')).digest();
    const cipher = plugins.crypto.createCipheriv('aes-256-cbc', key, iv);
    cipher.setAutoPadding(false);
    return Buffer.concat([checksum, iv, cipher.update(plaintext), cipher.final()]);
  }

  private decodeJsonRpc<TResult>(contentArg: Buffer): TResult {
    const password = this.config.password;
    let content = contentArg.toString('utf8');
    if (password) {
      const iv = contentArg.subarray(32, 48);
      const encrypted = contentArg.subarray(48);
      const key = plugins.crypto.createHash('sha256').update(Buffer.from(password, 'utf8')).digest();
      const decipher = plugins.crypto.createDecipheriv('aes-256-cbc', key, iv);
      decipher.setAutoPadding(false);
      content = Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8').replace(/[\x10\x0a\x00\s]+$/g, '');
    }
    const response = JSON.parse(content) as IRainbirdLocalJsonRpcResponse<TResult>;
    if (response.error) {
      throw new RainbirdApiError(`Rain Bird responded with an error: ${response.error.message || response.error.code || 'unknown error'}`);
    }
    return response.result as TResult;
  }
}

const sipCommands = {
  ModelAndVersionRequest: { command: '02', response: '82', length: 1 },
  AvailableStationsRequest: { command: '03', response: '83', length: 2 },
  SerialNumberRequest: { command: '05', response: '85', length: 1 },
  ControllerFirmwareVersionRequest: { command: '0B', response: '8B', length: 1 },
  RetrieveScheduleRequest: { command: '20', response: 'A0', length: 3 },
  RainDelayGetRequest: { command: '36', response: 'B6', length: 1 },
  RainDelaySetRequest: { command: '37', response: '01', length: 3 },
  ManuallyRunProgramRequest: { command: '38', response: '01', length: 2 },
  ManuallyRunStationRequest: { command: '39', response: '01', length: 4 },
  CurrentRainSensorStateRequest: { command: '3E', response: 'BE', length: 1 },
  CurrentStationsActiveRequest: { command: '3F', response: 'BF', length: 2 },
  StopIrrigationRequest: { command: '40', response: '01', length: 1 },
  CurrentIrrigationStateRequest: { command: '48', response: 'C8', length: 1 },
} as const;

const encodeSipCommand = (commandArg: keyof typeof sipCommands, ...argsArg: number[]): string => {
  const command = sipCommands[commandArg];
  if (!argsArg.length) {
    return command.command;
  }
  if (argsArg.length > command.length) {
    throw new RainbirdApiError(`Too many SIP parameters for ${commandArg}.`);
  }
  const firstWidth = Math.max(0, (command.length - argsArg.length) * 2);
  const encodedArgs = argsArg.map((arg, index) => toHex(arg, index === 0 ? firstWidth || 2 : 2)).join('');
  return `${command.command}${encodedArgs}`;
};

const activeSet = (maskArg: string, maxStationsArg: number): number[] => {
  const active: number[] = [];
  let zone = 1;
  for (let i = 0; i < maskArg.length; i += 2) {
    const byte = parseHex(maskArg.slice(i, i + 2));
    for (let bit = 0; bit < 8 && zone <= maxStationsArg; bit++) {
      if ((byte & (1 << bit)) !== 0) {
        active.push(zone);
      }
      zone++;
    }
  }
  return active;
};

const parseHex = (valueArg: string): number => Number.parseInt(valueArg || '0', 16);

const toHex = (valueArg: number, widthArg: number): string => Math.trunc(valueArg).toString(16).toUpperCase().padStart(widthArg, '0');

const addPadding = (dataArg: string): string => {
  const paddingLength = (16 - (Buffer.byteLength(dataArg, 'utf8') % 16)) % 16;
  return `${dataArg}${'\x10'.repeat(paddingLength)}`;
};
