import * as plugins from '../../plugins.js';
import { HddtempMapper } from './hddtemp.mapper.js';
import type { IHddtempConfig, IHddtempRefreshResult, IHddtempSnapshot, THddtempRawData } from './hddtemp.types.js';
import { hddtempDefaultPort, hddtempDefaultTimeoutMs } from './hddtemp.types.js';

export class HddtempClient {
  private currentSnapshot?: IHddtempSnapshot;

  constructor(private readonly config: IHddtempConfig) {}

  public async getSnapshot(forceRefreshArg = false): Promise<IHddtempSnapshot> {
    if (!forceRefreshArg && this.currentSnapshot) {
      return HddtempMapper.clone(this.currentSnapshot);
    }

    if (!forceRefreshArg && this.config.snapshot) {
      this.currentSnapshot = HddtempMapper.toSnapshot({ config: this.config, source: 'snapshot', online: this.config.snapshot.online });
      return HddtempMapper.clone(this.currentSnapshot);
    }

    if (this.config.client) {
      try {
        this.currentSnapshot = await this.snapshotFromClient();
      } catch (errorArg) {
        this.currentSnapshot = this.offlineSnapshot(this.errorMessage(errorArg));
      }
      return HddtempMapper.clone(this.currentSnapshot);
    }

    if (this.config.host) {
      try {
        const rawData = await this.requestRawData();
        this.currentSnapshot = HddtempMapper.toSnapshot({ config: this.config, rawData, online: true, source: 'tcp' });
      } catch (errorArg) {
        this.currentSnapshot = this.offlineSnapshot(this.errorMessage(errorArg));
      }
      return HddtempMapper.clone(this.currentSnapshot);
    }

    const rawData = this.rawData();
    if (rawData !== undefined) {
      this.currentSnapshot = HddtempMapper.toSnapshot({ config: this.config, rawData, online: this.config.online ?? true, source: 'manual' });
      return HddtempMapper.clone(this.currentSnapshot);
    }

    if (this.config.snapshot) {
      this.currentSnapshot = HddtempMapper.toSnapshot({ config: this.config, source: 'snapshot', online: this.config.snapshot.online });
      return HddtempMapper.clone(this.currentSnapshot);
    }

    this.currentSnapshot = this.offlineSnapshot('HDDTemp snapshots require config.host, an injected client, snapshot, or raw data.');
    return HddtempMapper.clone(this.currentSnapshot);
  }

  public async refresh(): Promise<IHddtempRefreshResult> {
    this.currentSnapshot = undefined;
    const snapshot = await this.getSnapshot(true);
    const success = snapshot.online && !snapshot.error;
    return { success, snapshot, error: success ? undefined : snapshot.error, data: { source: snapshot.source } };
  }

  public async ping(): Promise<boolean> {
    const snapshot = await this.getSnapshot(true);
    return snapshot.online && !snapshot.error;
  }

  public async destroy(): Promise<void> {
    await this.config.client?.destroy?.();
  }

  private async snapshotFromClient(): Promise<IHddtempSnapshot> {
    const client = this.config.client;
    if (!client) {
      throw new Error('No HDDTemp client configured.');
    }
    if (client.getSnapshot) {
      const snapshot = await client.getSnapshot();
      if (this.isSnapshot(snapshot)) {
        return HddtempMapper.toSnapshot({ config: { ...this.config, snapshot }, source: 'client', online: snapshot.online });
      }
      return HddtempMapper.toSnapshot({ config: this.config, rawData: snapshot, online: true, source: 'client' });
    }
    if (client.getRawData) {
      return HddtempMapper.toSnapshot({ config: this.config, rawData: await client.getRawData(), online: true, source: 'client' });
    }
    throw new Error('HDDTemp client must expose getSnapshot() or getRawData().');
  }

  private async requestRawData(): Promise<string> {
    const host = this.config.host;
    if (!host) {
      throw new Error('HDDTemp TCP snapshot requires config.host.');
    }
    const port = this.config.port || hddtempDefaultPort;
    const timeoutMs = this.config.timeoutMs || hddtempDefaultTimeoutMs;

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

      const finish = (errorArg?: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        socket.removeAllListeners();
        socket.destroy();
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve(Buffer.concat(chunks).toString('ascii'));
      };

      socket.setTimeout(timeoutMs, () => finish(new Error(`HDDTemp TCP snapshot request to ${host}:${port} timed out after ${timeoutMs}ms.`)));
      socket.on('data', (chunkArg) => chunks.push(Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg)));
      socket.on('end', () => finish());
      socket.on('close', () => finish());
      socket.on('error', (errorArg) => finish(errorArg));
    });
  }

  private rawData(): THddtempRawData | undefined {
    return this.config.rawData ?? this.config.rawText ?? this.config.rawDisks;
  }

  private offlineSnapshot(errorArg: string): IHddtempSnapshot {
    return HddtempMapper.toSnapshot({ config: this.config, online: false, source: 'runtime', error: errorArg });
  }

  private isSnapshot(valueArg: unknown): valueArg is IHddtempSnapshot {
    return Boolean(valueArg && typeof valueArg === 'object' && 'daemon' in valueArg && 'disks' in valueArg);
  }

  private errorMessage(errorArg: unknown): string {
    return errorArg instanceof Error ? errorArg.message : String(errorArg);
  }
}
