import * as plugins from '../../plugins.js';
import type { IApcupsdConfig, IApcupsdSnapshot, IApcupsdStatusRecord, TApcupsdSnapshotSource } from './apcupsd.types.js';
import { apcupsdDefaultPort, apcupsdDefaultTimeoutMs } from './apcupsd.types.js';

const statusCommand = 'status';
const onlineStatusMask = 0b1000;

export class ApcupsdClient {
  constructor(private readonly config: IApcupsdConfig) {}

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

    if (this.config.rawStatus) {
      return this.normalizeSnapshot(this.snapshotFromStatus(this.config.rawStatus, this.config.online ?? true, 'manual'), 'manual');
    }

    if (this.config.host) {
      try {
        const raw = await this.requestStatusText();
        return this.normalizeSnapshot(this.snapshotFromStatus(raw, true, 'tcp'), 'tcp');
      } catch (errorArg) {
        return this.normalizeSnapshot(this.snapshotFromConfig(false, errorArg instanceof Error ? errorArg.message : String(errorArg)), 'runtime');
      }
    }

    return this.normalizeSnapshot(this.snapshotFromConfig(this.config.online ?? false), 'runtime');
  }

  public async refresh(): Promise<IApcupsdSnapshot> {
    return this.getSnapshot();
  }

  public async ping(): Promise<boolean> {
    if (!this.config.host) {
      return Boolean(this.config.snapshot || this.config.rawStatus);
    }
    const snapshot = this.normalizeSnapshot(this.snapshotFromStatus(await this.requestStatusText(), true, 'tcp'), 'tcp');
    return snapshot.online;
  }

  public async destroy(): Promise<void> {}

  public static parseStatusText(textArg: string): IApcupsdStatusRecord {
    const record: IApcupsdStatusRecord = {};
    for (const line of textArg.split(/\r?\n/)) {
      const match = /^([^:]+?)\s*:\s*(.*)$/.exec(line);
      if (!match) {
        continue;
      }
      const key = match[1].trim().toUpperCase();
      if (!key) {
        continue;
      }
      record[key] = match[2].trim();
    }
    return record;
  }

  public static normalizeStatusRecord(statusArg: string | IApcupsdStatusRecord): IApcupsdStatusRecord {
    if (typeof statusArg === 'string') {
      return this.parseStatusText(statusArg);
    }
    const record: IApcupsdStatusRecord = {};
    for (const [key, value] of Object.entries(statusArg)) {
      if (typeof value === 'string') {
        record[key.trim().toUpperCase()] = value.trim();
      }
    }
    return record;
  }

  public static numberFromValue(valueArg: string | undefined): number | undefined {
    if (!valueArg) {
      return undefined;
    }
    const match = /[-+]?\d+(?:\.\d+)?/.exec(valueArg);
    if (!match) {
      return undefined;
    }
    const parsed = Number(match[0]);
    return Number.isFinite(parsed) ? parsed : undefined;
  }

  public static statusFlag(valueArg: string | undefined): number | undefined {
    if (!valueArg) {
      return undefined;
    }
    const match = /0x[0-9a-f]+|\d+/i.exec(valueArg);
    if (!match) {
      return undefined;
    }
    const parsed = match[0].toLowerCase().startsWith('0x') ? Number.parseInt(match[0], 16) : Number(match[0]);
    return Number.isFinite(parsed) ? parsed : undefined;
  }

  private async requestStatusText(): Promise<string> {
    const host = this.config.host;
    if (!host) {
      throw new Error('APCUPSd TCP status requires config.host.');
    }
    const port = this.config.port || apcupsdDefaultPort;
    const timeoutMs = this.config.timeoutMs || apcupsdDefaultTimeoutMs;

    return new Promise<string>((resolve, reject) => {
      let buffer = Buffer.alloc(0);
      let settled = false;
      const chunks: string[] = [];
      const socket = plugins.net.createConnection({ host, port });

      const finish = (errorArg?: Error, valueArg?: string) => {
        if (settled) {
          return;
        }
        settled = true;
        socket.removeAllListeners();
        socket.destroy();
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve(valueArg || '');
      };

      const handleData = (chunkArg: Buffer) => {
        buffer = Buffer.concat([buffer, chunkArg]);
        while (buffer.length >= 2) {
          const length = buffer.readUInt16BE(0);
          if (length === 0) {
            finish(undefined, chunks.join(''));
            return;
          }
          if (buffer.length < length + 2) {
            return;
          }
          chunks.push(buffer.subarray(2, 2 + length).toString('utf8'));
          buffer = buffer.subarray(2 + length);
        }
      };

      socket.setTimeout(timeoutMs, () => finish(new Error(`APCUPSd TCP status request timed out after ${timeoutMs}ms.`)));
      socket.on('connect', () => socket.write(this.frame(statusCommand)));
      socket.on('error', (errorArg) => finish(errorArg));
      socket.on('close', () => finish(new Error('APCUPSd TCP connection closed before status response completed.')));
      socket.on('data', (chunkArg) => handleData(Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg)));
    });
  }

  private frame(valueArg: string): Buffer {
    const payload = Buffer.from(valueArg, 'utf8');
    const frame = Buffer.alloc(payload.length + 2);
    frame.writeUInt16BE(payload.length, 0);
    payload.copy(frame, 2);
    return frame;
  }

  private snapshotFromStatus(statusArg: string | IApcupsdStatusRecord, onlineArg: boolean, sourceArg: TApcupsdSnapshotSource): IApcupsdSnapshot {
    const status = ApcupsdClient.normalizeStatusRecord(statusArg);
    const statusFlag = ApcupsdClient.statusFlag(status.STATFLAG);
    const serialNumber = status.SERIALNO && status.SERIALNO !== 'Blank' ? status.SERIALNO : undefined;
    const model = status.APCMODEL || status.MODEL;
    const lineOnline = this.lineOnline(status, statusFlag);
    const host = this.config.host;
    const port = this.config.port || (host ? apcupsdDefaultPort : undefined);
    const id = this.config.uniqueId || serialNumber || (host ? `${host}:${port}` : undefined) || status.UPSNAME || model;

    return {
      ups: {
        id,
        name: this.config.name || status.UPSNAME || model || 'APC UPS',
        host,
        port,
        model,
        serialNumber,
        firmware: status.FIRMWARE,
        version: status.VERSION,
        hostname: status.HOSTNAME,
        mode: status.UPSMODE,
        status: status.STATUS,
        statusFlag,
        lineOnline,
      },
      battery: {
        chargePercent: ApcupsdClient.numberFromValue(status.BCHARGE),
        voltage: ApcupsdClient.numberFromValue(status.BATTV),
        nominalVoltage: ApcupsdClient.numberFromValue(status.NOMBATTV),
        timeLeftMinutes: ApcupsdClient.numberFromValue(status.TIMELEFT),
        timeOnBatterySeconds: ApcupsdClient.numberFromValue(status.TONBATT),
        totalTimeOnBatterySeconds: ApcupsdClient.numberFromValue(status.CUMONBATT),
        status: status.BATTSTAT,
        replacementDate: status.BATTDATE,
        lastSelfTest: status.LASTSTEST,
        selfTest: status.SELFTEST,
        badBatteries: ApcupsdClient.numberFromValue(status.BADBATTS),
        externalBatteries: ApcupsdClient.numberFromValue(status.EXTBATTS),
      },
      power: {
        lineVoltage: ApcupsdClient.numberFromValue(status.LINEV),
        outputVoltage: ApcupsdClient.numberFromValue(status.OUTPUTV),
        loadPercent: ApcupsdClient.numberFromValue(status.LOADPCT),
        loadApparentPercent: ApcupsdClient.numberFromValue(status.LOADAPNT),
        lineFrequency: ApcupsdClient.numberFromValue(status.LINEFREQ),
        nominalInputVoltage: ApcupsdClient.numberFromValue(status.NOMINV),
        nominalOutputVoltage: ApcupsdClient.numberFromValue(status.NOMOUTV),
        nominalPowerWatts: ApcupsdClient.numberFromValue(status.NOMPOWER),
        nominalApparentPowerVa: ApcupsdClient.numberFromValue(status.NOMAPNT),
        outputCurrentAmps: ApcupsdClient.numberFromValue(status.OUTCURNT),
        inputVoltageHigh: ApcupsdClient.numberFromValue(status.MAXLINEV),
        inputVoltageLow: ApcupsdClient.numberFromValue(status.MINLINEV),
        transferHighVoltage: ApcupsdClient.numberFromValue(status.HITRANS),
        transferLowVoltage: ApcupsdClient.numberFromValue(status.LOTRANS),
        transferCount: ApcupsdClient.numberFromValue(status.NUMXFERS),
        lastTransfer: status.LASTXFER,
      },
      status,
      online: onlineArg,
      updatedAt: new Date().toISOString(),
      source: sourceArg,
      raw: typeof statusArg === 'string' ? statusArg : undefined,
    };
  }

  private snapshotFromConfig(onlineArg: boolean, errorArg?: string): IApcupsdSnapshot {
    const host = this.config.host;
    const port = this.config.port || (host ? apcupsdDefaultPort : undefined);
    return {
      ups: {
        id: this.config.uniqueId || (host ? `${host}:${port}` : undefined) || this.config.name || 'apc-ups',
        name: this.config.name || 'APC UPS',
        host,
        port,
      },
      battery: {},
      power: {},
      status: {},
      online: onlineArg,
      updatedAt: new Date().toISOString(),
      source: 'runtime',
      error: errorArg,
    };
  }

  private normalizeSnapshot(snapshotArg: IApcupsdSnapshot, sourceArg: TApcupsdSnapshotSource): IApcupsdSnapshot {
    const status = ApcupsdClient.normalizeStatusRecord(snapshotArg.status || {});
    const statusDerived = Object.keys(status).length ? this.snapshotFromStatus(status, snapshotArg.online, sourceArg) : undefined;
    const ups = {
      ...statusDerived?.ups,
      ...snapshotArg.ups,
    };
    ups.host = ups.host || this.config.host;
    ups.port = ups.port || (ups.host ? this.config.port || apcupsdDefaultPort : this.config.port);
    ups.name = ups.name || this.config.name || ups.model || 'APC UPS';
    ups.id = ups.id || this.config.uniqueId || ups.serialNumber || (ups.host ? `${ups.host}:${ups.port || apcupsdDefaultPort}` : undefined) || ups.name;

    return {
      ...snapshotArg,
      ups,
      battery: { ...statusDerived?.battery, ...snapshotArg.battery },
      power: { ...statusDerived?.power, ...snapshotArg.power },
      status,
      online: snapshotArg.online,
      updatedAt: snapshotArg.updatedAt || new Date().toISOString(),
      source: snapshotArg.source || sourceArg,
    };
  }

  private lineOnline(statusArg: IApcupsdStatusRecord, statusFlagArg: number | undefined): boolean | undefined {
    if (statusFlagArg !== undefined) {
      return (statusFlagArg & onlineStatusMask) !== 0;
    }
    const status = statusArg.STATUS?.toUpperCase();
    if (!status) {
      return undefined;
    }
    if (status.includes('ONBATT') || status.includes('ON BATTERY')) {
      return false;
    }
    if (status.includes('ONLINE') || status.includes('ON LINE') || status.includes('BOOST') || status.includes('TRIM')) {
      return true;
    }
    return undefined;
  }

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