import { GlancesMapper } from './glances.mapper.js';
import type { IGlancesConfig, IGlancesRawData, IGlancesRefreshResult, IGlancesSnapshot, TGlancesApiVersion } from './glances.types.js';
import { glancesDefaultPort, glancesDefaultTimeoutMs } from './glances.types.js';

export class GlancesApiError extends Error {}
export class GlancesApiConnectionError extends GlancesApiError {}
export class GlancesApiAuthorizationError extends GlancesApiError {}
export class GlancesApiNoDataAvailableError extends GlancesApiError {}

export class GlancesClient {
  private currentSnapshot?: IGlancesSnapshot;

  constructor(private readonly config: IGlancesConfig) {}

  public async getSnapshot(): Promise<IGlancesSnapshot> {
    if (this.hasManualData()) {
      this.currentSnapshot = GlancesMapper.toSnapshot({ config: this.config, source: this.config.snapshot ? 'snapshot' : 'manual', online: this.config.online ?? true });
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (this.config.host) {
      try {
        this.currentSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.currentSnapshot = this.offlineSnapshot(this.errorMessage(errorArg));
      }
      return this.cloneSnapshot(this.currentSnapshot);
    }

    this.currentSnapshot = this.offlineSnapshot('No Glances HTTP endpoint or snapshot data is configured.');
    return this.cloneSnapshot(this.currentSnapshot);
  }

  public async refresh(): Promise<IGlancesRefreshResult> {
    if (this.hasManualData()) {
      const snapshot = await this.getSnapshot();
      return { success: true, snapshot, data: { source: snapshot.source } };
    }

    if (!this.config.host) {
      const snapshot = await this.getSnapshot();
      return {
        success: false,
        snapshot,
        error: 'Glances refresh requires a configured HTTP endpoint or snapshot/manual data.',
      };
    }

    try {
      const snapshot = await this.fetchSnapshot();
      this.currentSnapshot = snapshot;
      return { success: true, snapshot: this.cloneSnapshot(snapshot), data: { source: 'http', apiVersion: snapshot.apiVersion } };
    } catch (errorArg) {
      const error = this.errorMessage(errorArg);
      const snapshot = this.offlineSnapshot(error);
      this.currentSnapshot = snapshot;
      return { success: false, snapshot: this.cloneSnapshot(snapshot), error };
    }
  }

  public async ping(): Promise<boolean> {
    if (this.hasManualData()) {
      return true;
    }
    if (!this.config.host) {
      return false;
    }
    return (await this.refresh()).success;
  }

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

  public async fetchSnapshot(): Promise<IGlancesSnapshot> {
    const versions = this.apiVersions();
    let lastNoDataError: Error | undefined;
    for (const version of versions) {
      try {
        const rawData = await this.requestRawData(version);
        return GlancesMapper.toSnapshot({
          config: this.config,
          rawData,
          online: true,
          source: 'http',
          apiVersion: version,
        });
      } catch (errorArg) {
        if (errorArg instanceof GlancesApiNoDataAvailableError && versions.length > 1) {
          lastNoDataError = errorArg;
          continue;
        }
        throw errorArg;
      }
    }
    throw lastNoDataError || new GlancesApiNoDataAvailableError('Could not connect to Glances API version 3 or 4.');
  }

  private async requestRawData(versionArg: TGlancesApiVersion): Promise<IGlancesRawData> {
    if (this.config.password && !this.config.username) {
      throw new GlancesApiAuthorizationError('Glances username and password must both be provided for HTTP basic authentication.');
    }
    const url = `${this.baseUrl(versionArg)}/all`;
    let response: Response;
    try {
      response = await globalThis.fetch(url, {
        method: 'GET',
        headers: this.headers(),
        signal: AbortSignal.timeout(this.config.timeoutMs || glancesDefaultTimeoutMs),
      });
    } catch (errorArg) {
      throw new GlancesApiConnectionError(`Connection to ${url} failed: ${this.errorMessage(errorArg)}`);
    }

    if (response.status === 401) {
      throw new GlancesApiAuthorizationError('Please check your Glances credentials.');
    }
    if (!response.ok) {
      throw new GlancesApiNoDataAvailableError(`Glances endpoint /api/${versionArg}/all is not valid: HTTP ${response.status}.`);
    }

    const text = await response.text();
    try {
      const parsed = text ? JSON.parse(text) as unknown : {};
      if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
        throw new Error('response is not a JSON object');
      }
      return parsed as IGlancesRawData;
    } catch (errorArg) {
      throw new GlancesApiConnectionError(`Unable to parse Glances data from ${url}: ${this.errorMessage(errorArg)}`);
    }
  }

  private headers(): Record<string, string> {
    const headers: Record<string, string> = { accept: 'application/json' };
    if (this.config.username && this.config.password) {
      headers.authorization = `Basic ${Buffer.from(`${this.config.username}:${this.config.password}`).toString('base64')}`;
    }
    return headers;
  }

  private baseUrl(versionArg: TGlancesApiVersion): string {
    const protocol = this.config.ssl ? 'https' : 'http';
    const host = this.config.host || 'localhost';
    const port = this.config.port || glancesDefaultPort;
    return `${protocol}://${this.hostForUrl(host)}:${port}/api/${versionArg}`;
  }

  private hostForUrl(hostArg: string): string {
    return hostArg.includes(':') && !hostArg.startsWith('[') ? `[${hostArg}]` : hostArg;
  }

  private apiVersions(): TGlancesApiVersion[] {
    const configuredVersion = this.config.apiVersion || this.config.version;
    return configuredVersion ? [configuredVersion] : [4, 3];
  }

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

  private hasManualData(): boolean {
    return Boolean(this.config.snapshot || this.config.rawData || this.config.allData || this.config.haSensorData || this.config.sensorData);
  }

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

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