import {
  rflinkDefaultBaudRate,
  rflinkDefaultCoverTypeByProtocol,
  rflinkDefaultLightTypeByProtocol,
  rflinkDefaultSignalRepetitions,
  rflinkDefaultWaitForAck,
  rflinkDomain,
  rflinkPacketFieldAliases,
  rflinkPacketUnits,
} from './rflink.constants.js';
import type {
  IRflinkCommand,
  IRflinkCommandResult,
  IRflinkConfig,
  IRflinkDevice,
  IRflinkEntity,
  IRflinkEntityConfig,
  IRflinkEvent,
  IRflinkGateway,
  IRflinkLineProtocolCommandShape,
  IRflinkPacket,
  IRflinkSnapshot,
  TRflinkEntityCollection,
  TRflinkEntityPlatform,
} from './rflink.types.js';

type TRflinkEventHandler = (eventArg: IRflinkEvent) => void;

const protocolTranslations = new Map<string, string>();
for (const protocol of [
  'Ikea Koppla',
  'Alecto V1',
  'Alecto V2',
  'UPM/Esic',
  'Oregon TempHygro',
  'Oregon BTHR',
  'Oregon Rain',
  'Oregon Rain2',
  'Oregon Wind',
  'Oregon Wind2',
  'Oregon UVN128/138',
  'Plieger York',
  'Byron SX',
  'CAME-TOP432',
]) {
  const serialized = protocol.toLowerCase().replace(/[^a-z0-9_]+/g, '');
  protocolTranslations.set(protocol.toLowerCase(), serialized);
  protocolTranslations.set(serialized, protocol.toLowerCase());
}

export class RflinkClient {
  private snapshot?: IRflinkSnapshot;
  private readonly eventHandlers = new Set<TRflinkEventHandler>();

  constructor(private readonly config: IRflinkConfig) {}

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

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

  public async refresh(): Promise<IRflinkSnapshot> {
    this.snapshot = this.normalizeSnapshot(this.snapshotFromConfig());
    this.emit({ type: 'snapshot_refreshed', data: this.cloneSnapshot(this.snapshot), timestamp: Date.now() });
    return this.cloneSnapshot(this.snapshot);
  }

  public async sendCommand(commandArg: IRflinkCommand): Promise<IRflinkCommandResult> {
    if (commandArg.type === 'refresh') {
      return { success: true, transmitted: false, data: await this.refresh() };
    }

    const command = this.withLineProtocol(commandArg);
    this.emit({ type: command.type === 'learn' ? 'learn' : 'command_mapped', id: command.deviceId, entityId: command.entityId, command, rflinkCommand: command.rflinkCommand, line: command.lineProtocol?.line, timestamp: Date.now() });

    try {
      if (this.config.commandExecutor) {
        const executorResult = await this.config.commandExecutor(command);
        const result = this.commandResult(executorResult, command, true);
        if (result.success) {
          await this.patchSnapshot(command);
        }
        this.emit({ type: result.success ? 'command_executed' : 'command_failed', id: command.deviceId, entityId: command.entityId, command, rflinkCommand: command.rflinkCommand, line: command.lineProtocol?.line, data: result, timestamp: Date.now() });
        return result;
      }

      await this.patchSnapshot(command);
      const result: IRflinkCommandResult = {
        success: true,
        transmitted: false,
        data: {
          lineProtocol: command.lineProtocol,
          reason: 'No RFLink serial/TCP transport executor is configured; command was mapped but not transmitted.',
        },
      };
      this.emit({ type: 'command_executed', id: command.deviceId, entityId: command.entityId, command, rflinkCommand: command.rflinkCommand, line: command.lineProtocol?.line, data: result, timestamp: Date.now() });
      return result;
    } catch (errorArg) {
      const result = { success: false, transmitted: false, error: errorArg instanceof Error ? errorArg.message : String(errorArg), data: { command } };
      this.emit({ type: 'command_failed', id: command.deviceId, entityId: command.entityId, command, rflinkCommand: command.rflinkCommand, line: command.lineProtocol?.line, data: result, timestamp: Date.now() });
      return result;
    }
  }

  public lineProtocolCommandShape(commandArg: IRflinkCommand): IRflinkLineProtocolCommandShape {
    return this.withLineProtocol(commandArg).lineProtocol as IRflinkLineProtocolCommandShape;
  }

  public static commandShape(deviceIdArg: string, rflinkCommandArg: string, optionsArg: { rawLine?: string; waitForAck?: boolean; repetitions?: number } = {}): IRflinkLineProtocolCommandShape {
    if (optionsArg.rawLine) {
      const line = this.normalizeRawLine(optionsArg.rawLine);
      return {
        transport: 'line-protocol',
        line,
        packet: { node: line.startsWith('20;') ? '20' : '10', raw: line },
        waitForAck: optionsArg.waitForAck,
        repetitions: optionsArg.repetitions,
      };
    }

    const packet = this.deserializeDeviceId(deviceIdArg);
    const command = this.cleanSegment(rflinkCommandArg);
    const protocol = this.cleanSegment(packet.protocol || 'unknown');

    if (protocol.toLowerCase() === 'rfdebug' || protocol.toLowerCase() === 'rfudebug' || protocol.toLowerCase() === 'qrfdebug') {
      const debugProtocol = protocol.toUpperCase();
      return {
        transport: 'line-protocol',
        line: `10;${debugProtocol}=${command.toUpperCase()};`,
        packet: { node: '10', protocol: protocol.toLowerCase(), command },
        waitForAck: optionsArg.waitForAck,
        repetitions: optionsArg.repetitions,
      };
    }

    const segments = ['10', protocol];
    if (packet.id) {
      segments.push(this.cleanSegment(packet.id));
    }
    if (packet.switch) {
      segments.push(this.cleanSegment(packet.switch));
    }
    segments.push(command);

    return {
      transport: 'line-protocol',
      line: `${segments.join(';')};`,
      packet: { node: '10', protocol, id: packet.id, switch: packet.switch, command },
      waitForAck: optionsArg.waitForAck,
      repetitions: optionsArg.repetitions,
    };
  }

  public static decodeLine(lineArg: string): IRflinkPacket | undefined {
    const line = this.normalizeRawLine(lineArg);
    const parts = line.split(';').filter((partArg) => partArg !== '');
    if (parts.length < 2) {
      return undefined;
    }
    const [node, sequence, protocolRaw] = parts;
    const packet: IRflinkPacket = { node, sequence, raw: line, values: {} };
    if (!protocolRaw) {
      return packet;
    }

    if (protocolRaw.includes('RFLink Gateway')) {
      packet.protocol = 'unknown';
      packet.values = { ...packet.values, ...this.parseBanner(protocolRaw) };
      return packet;
    }
    if (protocolRaw === 'OK') {
      packet.values = { ...packet.values, ok: true };
      return packet;
    }
    if (protocolRaw === 'CMD UNKNOWN') {
      packet.values = { ...packet.values, ok: false, response: 'command_unknown' };
      return packet;
    }

    packet.protocol = protocolRaw.toLowerCase();
    for (const attr of parts.slice(3)) {
      if (!attr.includes('=')) {
        continue;
      }
      const [rawKey, ...rawValueParts] = attr.split('=');
      const key = rawKey.toLowerCase();
      const rawValue = rawValueParts.join('=').toLowerCase();
      const field = rflinkPacketFieldAliases[key] || key;
      const value = this.decodeValue(key, rawValue);
      if (field === 'id') {
        packet.id = String(value);
      } else if (field === 'switch') {
        packet.switch = String(value);
      } else if (field === 'command') {
        packet.command = String(value).toLowerCase();
      }
      packet.values = { ...packet.values, [field]: value };
      const unit = rflinkPacketUnits[key];
      if (unit) {
        packet.values[`${field}_unit`] = unit;
      }
    }
    if (packet.protocol === 'kaku' && packet.id && packet.id.length !== 6) {
      packet.id = `0000${packet.id}`.slice(-6);
      packet.values = { ...packet.values, id: packet.id };
    }
    return packet;
  }

  public static eventsFromPacket(packetArg: IRflinkPacket): IRflinkEvent[] {
    const timestamp = Date.now();
    const values = packetArg.values || {};
    const id = this.serializePacketId(packetArg);
    if (packetArg.command) {
      return [{ type: 'command', id, rflinkCommand: packetArg.command, packet: packetArg, timestamp }];
    }
    if (values.version || values.firmware || values.revision || id === rflinkDomain) {
      return [{ type: 'gateway', id: rflinkDomain, packet: packetArg, data: values, timestamp }];
    }

    const reverseAliases = this.reversePacketFieldAliases();
    const events: IRflinkEvent[] = [];
    for (const [sensor, abbrev] of Object.entries(reverseAliases)) {
      if (!(sensor in values)) {
        continue;
      }
      events.push({
        type: 'sensor',
        id: `${id}_${abbrev}`,
        sensor,
        value: values[sensor],
        unit: values[`${sensor}_unit`] as string | undefined,
        packet: packetArg,
        timestamp,
      });
    }
    if (events.length && id !== rflinkDomain) {
      events.push({ type: 'sensor', id: `${id}_update_time`, sensor: 'update_time', value: Math.round(timestamp / 1000), unit: 's', packet: packetArg, timestamp });
    }
    return events;
  }

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

  private withLineProtocol(commandArg: IRflinkCommand): IRflinkCommand {
    if (commandArg.lineProtocol) {
      return commandArg;
    }
    const rflinkCommand = commandArg.rflinkCommand || this.defaultRflinkCommand(commandArg);
    const lineProtocol = RflinkClient.commandShape(commandArg.deviceId || this.deviceIdForEntity(commandArg.entityId) || rflinkDomain, rflinkCommand, {
      rawLine: commandArg.rawLine,
      waitForAck: this.config.waitForAck ?? rflinkDefaultWaitForAck,
      repetitions: commandArg.repetitions || rflinkDefaultSignalRepetitions,
    });
    return { ...commandArg, rflinkCommand, lineProtocol };
  }

  private defaultRflinkCommand(commandArg: IRflinkCommand): string {
    if (commandArg.type === 'turn_on') {
      return 'on';
    }
    if (commandArg.type === 'turn_off') {
      return 'off';
    }
    if (commandArg.type === 'set_value') {
      return String(this.brightnessToRflink(commandArg.brightness ?? commandArg.value ?? 255));
    }
    if (commandArg.type === 'open_cover') {
      return 'UP';
    }
    if (commandArg.type === 'close_cover') {
      return 'DOWN';
    }
    if (commandArg.type === 'stop_cover') {
      return 'STOP';
    }
    if (commandArg.type === 'learn') {
      return 'PAIR';
    }
    return 'on';
  }

  private async patchSnapshot(commandArg: IRflinkCommand): Promise<void> {
    const snapshot = this.snapshot || this.normalizeSnapshot(this.snapshotFromConfig());
    this.snapshot = snapshot;
    const entity = this.findEntity(snapshot, commandArg);
    if (!entity || commandArg.type === 'learn' || commandArg.type === 'send_command') {
      return;
    }
    entity.updatedAt = new Date().toISOString();
    entity.command = commandArg.rflinkCommand;
    if (commandArg.type === 'turn_on') {
      entity.state = 'on';
    } else if (commandArg.type === 'turn_off') {
      entity.state = 'off';
    } else if (commandArg.type === 'set_value') {
      entity.state = 'on';
      entity.value = commandArg.value ?? commandArg.brightness;
      entity.brightness = commandArg.brightness ?? this.rflinkToBrightness(Number(commandArg.rflinkCommand));
    } else if (commandArg.type === 'open_cover') {
      entity.state = 'open';
    } else if (commandArg.type === 'close_cover') {
      entity.state = 'closed';
    } else if (commandArg.type === 'stop_cover') {
      entity.state = 'stopped';
    }
  }

  private findEntity(snapshotArg: IRflinkSnapshot, commandArg: IRflinkCommand): IRflinkEntity | undefined {
    return snapshotArg.entities.find((entityArg) => entityArg.id === commandArg.deviceId || entityArg.entityId === commandArg.entityId || entityArg.uniqueId === commandArg.entityId || entityArg.aliases?.includes(commandArg.deviceId || ''));
  }

  private snapshotFromConfig(): IRflinkSnapshot {
    const updatedAt = new Date().toISOString();
    const gateway = this.gatewayFromConfig();
    const events = (this.config.events || []).map((eventArg) => this.cloneEvent(eventArg));
    const entities = this.entitiesFromConfig(events, updatedAt);
    const connected = this.config.connected ?? gateway.online ?? false;
    return {
      gateway: { ...gateway, online: connected },
      devices: entities.map((entityArg) => this.deviceFromEntity(entityArg)),
      entities,
      events,
      connected,
      updatedAt,
    };
  }

  private gatewayFromConfig(): IRflinkGateway {
    const connectionType = this.config.connectionType || (this.config.host ? 'tcp' : this.config.port ? 'serial' : 'manual');
    return {
      id: this.config.gateway?.id || this.config.host || String(this.config.port || rflinkDomain),
      name: this.config.gateway?.name || 'RFLink Gateway',
      connectionType,
      host: this.config.gateway?.host || this.config.host,
      port: this.config.gateway?.port || this.config.port,
      baudRate: this.config.gateway?.baudRate || this.config.baudRate || rflinkDefaultBaudRate,
      hardware: this.config.gateway?.hardware,
      firmware: this.config.gateway?.firmware || 'RFLink Gateway',
      version: this.config.gateway?.version,
      revision: this.config.gateway?.revision,
      online: this.config.connected ?? this.config.gateway?.online ?? false,
      attributes: this.config.gateway?.attributes,
    };
  }

  private entitiesFromConfig(eventsArg: IRflinkEvent[], updatedAtArg: string): IRflinkEntity[] {
    const entities = [
      ...this.entitiesFromCollection(this.config.entities || [], undefined, updatedAtArg),
      ...this.entitiesFromCollection(this.config.devices || [], undefined, updatedAtArg),
      ...this.entitiesFromCollection(this.config.lights || [], 'light', updatedAtArg),
      ...this.entitiesFromCollection(this.config.switches || [], 'switch', updatedAtArg),
      ...this.entitiesFromCollection(this.config.sensors || [], 'sensor', updatedAtArg),
      ...this.entitiesFromCollection(this.config.binarySensors || [], 'binary_sensor', updatedAtArg),
      ...this.entitiesFromCollection(this.config.covers || [], 'cover', updatedAtArg),
    ];
    const knownIds = new Set(entities.flatMap((entityArg) => [entityArg.id, ...(entityArg.aliases || [])]));
    for (const event of eventsArg) {
      if (!event.id || knownIds.has(event.id) || this.ignored(event.id)) {
        continue;
      }
      const entity = this.entityFromEvent(event, updatedAtArg);
      if (entity) {
        entities.push(entity);
        knownIds.add(entity.id);
      }
    }
    return entities.filter((entityArg) => !this.ignored(entityArg.id));
  }

  private entitiesFromCollection(collectionArg: TRflinkEntityCollection | undefined, platformArg: TRflinkEntityPlatform | undefined, updatedAtArg: string): IRflinkEntity[] {
    if (Array.isArray(collectionArg)) {
      return collectionArg.map((entityArg) => this.normalizeEntity(entityArg, platformArg, updatedAtArg));
    }
    return Object.entries(collectionArg || {}).map(([id, configArg]) => {
      const entityConfig = typeof configArg === 'string' ? { id, name: configArg } : { id, ...(configArg || {}) };
      return this.normalizeEntity(entityConfig, platformArg, updatedAtArg);
    });
  }

  private normalizeEntity(entityArg: IRflinkEntityConfig, platformArg: TRflinkEntityPlatform | undefined, updatedAtArg: string): IRflinkEntity {
    const platform = entityArg.platform || platformArg || 'light';
    const protocol = this.protocolFromDeviceId(entityArg.id);
    const type = entityArg.type || (platform === 'light' ? rflinkDefaultLightTypeByProtocol[protocol] || 'switchable' : platform === 'cover' ? rflinkDefaultCoverTypeByProtocol[protocol] || 'standard' : undefined);
    const sensorType = entityArg.sensorType || (platform === 'sensor' ? this.sensorTypeFromId(entityArg.id) : undefined);
    return {
      ...entityArg,
      platform,
      name: entityArg.name || this.nameFromId(entityArg.id, sensorType),
      aliases: [...(entityArg.aliases || [])],
      groupAliases: [...(entityArg.groupAliases || [])],
      noGroupAliases: [...(entityArg.noGroupAliases || [])],
      type,
      sensorType,
      state: entityArg.state || this.stateFromCommand(entityArg.command),
      available: entityArg.available,
      updatedAt: updatedAtArg,
    };
  }

  private entityFromEvent(eventArg: IRflinkEvent, updatedAtArg: string): IRflinkEntity | undefined {
    if (eventArg.type === 'sensor' && eventArg.id) {
      return this.normalizeEntity({ id: eventArg.id, platform: 'sensor', sensorType: eventArg.sensor, unitOfMeasurement: eventArg.unit, value: eventArg.value, available: true }, 'sensor', updatedAtArg);
    }
    if (eventArg.type === 'command' && eventArg.id) {
      return this.normalizeEntity({ id: eventArg.id, platform: 'light', command: eventArg.rflinkCommand, state: this.stateFromCommand(eventArg.rflinkCommand), available: true }, 'light', updatedAtArg);
    }
    return undefined;
  }

  private normalizeSnapshot(snapshotArg: IRflinkSnapshot): IRflinkSnapshot {
    const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
    const connected = snapshotArg.connected ?? snapshotArg.gateway.online ?? this.config.connected ?? false;
    const gateway: IRflinkGateway = { ...this.gatewayFromConfig(), ...snapshotArg.gateway, online: connected };
    const entities = (snapshotArg.entities?.length ? snapshotArg.entities : snapshotArg.devices || []).map((entityArg) => this.normalizeEntity(entityArg, entityArg.platform, updatedAt)).filter((entityArg) => !this.ignored(entityArg.id));
    const devices = (snapshotArg.devices?.length ? snapshotArg.devices : entities.map((entityArg) => this.deviceFromEntity(entityArg))).map((deviceArg) => this.deviceFromEntity(this.normalizeEntity(deviceArg, deviceArg.platform, updatedAt))).filter((deviceArg) => !this.ignored(deviceArg.id));
    return {
      gateway,
      devices,
      entities,
      events: (snapshotArg.events || []).map((eventArg) => this.cloneEvent(eventArg)),
      connected,
      updatedAt,
      raw: snapshotArg.raw ? { ...snapshotArg.raw } : undefined,
    };
  }

  private deviceFromEntity(entityArg: IRflinkEntity): IRflinkDevice {
    const { entityId, uniqueId, updatedAt, ...device } = entityArg;
    void entityId;
    void uniqueId;
    void updatedAt;
    return device;
  }

  private commandResult(valueArg: IRflinkCommandResult | unknown, commandArg: IRflinkCommand, transmittedArg: boolean): IRflinkCommandResult {
    if (valueArg && typeof valueArg === 'object' && 'success' in valueArg) {
      return { transmitted: transmittedArg, ...(valueArg as IRflinkCommandResult) };
    }
    return { success: true, transmitted: transmittedArg, data: valueArg ?? { lineProtocol: commandArg.lineProtocol } };
  }

  private deviceIdForEntity(entityIdArg: string | undefined): string | undefined {
    if (!entityIdArg || !this.snapshot) {
      return undefined;
    }
    return this.snapshot.entities.find((entityArg) => entityArg.entityId === entityIdArg || entityArg.uniqueId === entityIdArg)?.id;
  }

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

  private ignored(deviceIdArg: string): boolean {
    return (this.config.ignoreDevices || []).some((patternArg) => this.wildcardMatch(deviceIdArg, patternArg));
  }

  private wildcardMatch(valueArg: string, patternArg: string): boolean {
    const escaped = patternArg.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.');
    return new RegExp(`^${escaped}$`, 'i').test(valueArg);
  }

  private protocolFromDeviceId(deviceIdArg: string): string {
    const parts = deviceIdArg.split('_');
    return (parts.length > 2 ? parts.slice(0, -2).join('_') : parts[0]).toLowerCase();
  }

  private sensorTypeFromId(deviceIdArg: string): string | undefined {
    const suffix = deviceIdArg.split('_').pop() || '';
    return rflinkPacketFieldAliases[suffix] || suffix || undefined;
  }

  private stateFromCommand(commandArg: string | undefined): string | undefined {
    const command = (commandArg || '').toLowerCase();
    if (command === 'on' || command === 'allon' || command === 'up') {
      return 'on';
    }
    if (command === 'off' || command === 'alloff' || command === 'down') {
      return 'off';
    }
    return undefined;
  }

  private nameFromId(deviceIdArg: string, sensorTypeArg?: string): string {
    const idName = deviceIdArg.replace(/_/g, ' ');
    return sensorTypeArg && !idName.toLowerCase().includes(sensorTypeArg.replace(/_/g, ' ')) ? `${idName} ${sensorTypeArg.replace(/_/g, ' ')}` : idName;
  }

  private brightnessToRflink(valueArg: number): number {
    if (valueArg <= 15) {
      return Math.max(0, Math.min(15, Math.round(valueArg)));
    }
    return Math.max(0, Math.min(15, Math.floor(Math.max(0, Math.min(255, valueArg)) / 17)));
  }

  private rflinkToBrightness(valueArg: number): number {
    return Math.max(0, Math.min(255, Math.round(valueArg) * 17));
  }

  private cloneSnapshot(snapshotArg: IRflinkSnapshot): IRflinkSnapshot {
    return {
      gateway: { ...snapshotArg.gateway, attributes: snapshotArg.gateway.attributes ? { ...snapshotArg.gateway.attributes } : undefined },
      devices: snapshotArg.devices.map((deviceArg) => ({ ...deviceArg, aliases: [...(deviceArg.aliases || [])], attributes: deviceArg.attributes ? { ...deviceArg.attributes } : undefined, metadata: deviceArg.metadata ? { ...deviceArg.metadata } : undefined })),
      entities: snapshotArg.entities.map((entityArg) => ({ ...entityArg, aliases: [...(entityArg.aliases || [])], attributes: entityArg.attributes ? { ...entityArg.attributes } : undefined, metadata: entityArg.metadata ? { ...entityArg.metadata } : undefined })),
      events: snapshotArg.events.map((eventArg) => this.cloneEvent(eventArg)),
      connected: snapshotArg.connected,
      updatedAt: snapshotArg.updatedAt,
      raw: snapshotArg.raw ? { ...snapshotArg.raw } : undefined,
    };
  }

  private cloneEvent(eventArg: IRflinkEvent): IRflinkEvent {
    return { ...eventArg, command: eventArg.command ? { ...eventArg.command } : undefined, packet: eventArg.packet ? { ...eventArg.packet, values: eventArg.packet.values ? { ...eventArg.packet.values } : undefined } : undefined };
  }

  private static deserializeDeviceId(deviceIdArg: string): IRflinkPacket {
    if (deviceIdArg === rflinkDomain) {
      return { protocol: 'unknown' };
    }
    const parts = deviceIdArg.split('_');
    const switchPart = parts.length > 2 ? parts.pop() : undefined;
    const id = parts.length > 1 ? parts.pop() : undefined;
    const protocol = protocolTranslations.get(parts.join('_')) || parts.join('_') || 'unknown';
    return { protocol, id, switch: switchPart };
  }

  private static serializePacketId(packetArg: IRflinkPacket): string {
    const protocol = packetArg.protocol && packetArg.protocol !== 'unknown' ? protocolTranslations.get(packetArg.protocol) || packetArg.protocol.toLowerCase().replace(/[^a-z0-9_]+/g, '') : rflinkDomain;
    return [protocol, packetArg.id, packetArg.switch].filter(Boolean).join('_');
  }

  private static reversePacketFieldAliases(): Record<string, string> {
    const result: Record<string, string> = {};
    for (const [abbrev, field] of Object.entries(rflinkPacketFieldAliases)) {
      if (!result[field]) {
        result[field] = abbrev;
      }
    }
    return result;
  }

  private static normalizeRawLine(lineArg: string): string {
    const line = lineArg.replace(/[\r\n]+/g, '').trim();
    return line.endsWith(';') ? line : `${line};`;
  }

  private static cleanSegment(valueArg: string): string {
    return valueArg.replace(/[;\r\n]/g, '').trim();
  }

  private static parseBanner(valueArg: string): Record<string, string> {
    const match = valueArg.match(/(?<hardware>[a-zA-Z\s]+) - (?<firmware>[a-zA-Z\s]+) V(?<version>[0-9.]+) - R(?<revision>[0-9.]+)/);
    return match?.groups ? { ...match.groups } : {};
  }

  private static decodeValue(keyArg: string, valueArg: string): unknown {
    const hstatus: Record<string, string> = { '0': 'normal', '1': 'comfortable', '2': 'dry', '3': 'wet' };
    const bforecast: Record<string, string> = { '0': 'no_info', '1': 'sunny', '2': 'partly_cloudy', '3': 'cloudy', '4': 'rain' };
    if (keyArg === 'hstatus') {
      return hstatus[valueArg] || 'Unknown';
    }
    if (keyArg === 'bforecast') {
      return bforecast[valueArg] || 'Unknown';
    }
    if (['temp', 'winchl', 'wintmp'].includes(keyArg)) {
      const parsed = Number.parseInt(valueArg, 16);
      return parsed & 0x8000 ? -(parsed & 0x7fff) / 10 : parsed / 10;
    }
    if (['awinsp', 'rain', 'rainrate', 'raintot', 'wings', 'winsp'].includes(keyArg)) {
      return Number.parseInt(valueArg, 16) / 10;
    }
    if (keyArg === 'windir') {
      return Number.parseInt(valueArg, 10) * 22.5;
    }
    if (['baro', 'kwatt', 'lux', 'uv', 'watt'].includes(keyArg)) {
      return Number.parseInt(valueArg, 16);
    }
    if (['chime', 'co2', 'current', 'current2', 'current3', 'dist', 'hum', 'meter', 'sound', 'volt'].includes(keyArg)) {
      return Number.parseInt(valueArg, 10);
    }
    return valueArg;
  }
}
