import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallResult } from '../../core/index.js';
import { IometerMapper } from './iometer.mapper.js';
import type { IIometerConfig, IIometerRawApiData, IIometerReadingResponse, IIometerStatusResponse, TIometerRawData } from './iometer.types.js';
import { iometerDefaultPort, iometerDefaultTimeoutMs, iometerProfile } from './iometer.types.js';

export class IometerClient extends SimpleLocalClient<IIometerConfig> {
  private cachedSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly clientConfig: IIometerConfig) {
    super(iometerProfile, clientConfig);
  }

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

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

    if (this.hasEndpoint()) {
      try {
        const rawData = await this.fetchRawData();
        this.cachedSnapshot = IometerMapper.toSnapshot({ config: this.clientConfig, rawData, online: true, source: 'http' });
      } catch (errorArg) {
        this.cachedSnapshot = this.createOfflineSnapshot(errorMessage(errorArg));
      }
      return clone(this.cachedSnapshot);
    }

    if (this.clientConfig.client || this.clientConfig.snapshotProvider) {
      this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
      return clone(this.cachedSnapshot);
    }

    if (this.hasManualData()) {
      this.cachedSnapshot = IometerMapper.toSnapshot({ config: this.clientConfig, rawData: this.rawData(), online: this.clientConfig.online ?? true, source: 'manual' });
      return clone(this.cachedSnapshot);
    }

    this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
    return clone(this.cachedSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.cachedSnapshot = undefined;
    const liveCapable = this.hasEndpoint() || Boolean(this.clientConfig.client || this.clientConfig.snapshotProvider);
    const snapshot = await this.getSnapshot(liveCapable);
    const success = liveCapable && snapshot.online && !snapshot.error && snapshot.source !== 'runtime';
    return { success, error: success ? undefined : snapshot.error || 'IOmeter refresh requires a live host/url, injected client, or snapshotProvider.', data: snapshot };
  }

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

  private async fetchRawData(): Promise<IIometerRawApiData> {
    const [reading, status] = await Promise.all([
      this.requestJson<IIometerReadingResponse>('/v1/reading'),
      this.requestJson<IIometerStatusResponse>('/v1/status'),
    ]);
    return { reading, status, fetchedAt: new Date().toISOString() };
  }

  private async requestJson<TValue>(pathArg: string): Promise<TValue> {
    const response = await this.requestText(pathArg);
    try {
      return JSON.parse(response) as TValue;
    } catch (errorArg) {
      throw new Error(`IOmeter JSON parse failed for ${pathArg}: ${errorMessage(errorArg)}`);
    }
  }

  private async requestText(pathArg: string): Promise<string> {
    const url = this.url(pathArg);
    const timeoutMs = this.clientConfig.timeoutMs || iometerDefaultTimeoutMs;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      const response = await globalThis.fetch(url, {
        headers: {
          'user-agent': 'PythonIOmeter/0.1',
          accept: 'application/json',
        },
        signal: controller.signal,
      });
      const text = await response.text();
      if (!response.ok) {
        if (response.status === 404 && pathArg.includes('/reading')) {
          throw new Error('No readings available from IOmeter bridge.');
        }
        if (response.status === 404 && pathArg.includes('/status')) {
          throw new Error('No status available from IOmeter bridge.');
        }
        throw new Error(`IOmeter request ${pathArg} failed with ${response.status}: ${text.slice(0, 160)}`);
      }
      return text;
    } finally {
      clearTimeout(timer);
    }
  }

  private url(pathArg: string): string {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('IOmeter local API requires config.host or config.url.');
    }
    const path = pathArg.startsWith('/') ? pathArg : `/${pathArg}`;
    return new URL(path, `${endpoint.origin}/`).toString();
  }

  private endpoint(): URL | undefined {
    const configuredUrl = stringValue(this.clientConfig.url);
    if (configuredUrl) {
      return normalizeBaseUrl(configuredUrl, this.clientConfig.port);
    }
    const host = stringValue(this.clientConfig.host);
    if (!host) {
      return undefined;
    }
    return normalizeBaseUrl(host, this.clientConfig.port || iometerDefaultPort);
  }

  private hasEndpoint(): boolean {
    return Boolean(this.endpoint());
  }

  private hasManualData(): boolean {
    return this.rawData() !== undefined || Boolean(this.clientConfig.entities?.length || this.clientConfig.snapshot);
  }

  private rawData(): TIometerRawData | undefined {
    return this.clientConfig.rawData as TIometerRawData | undefined ?? this.clientConfig.rawText;
  }

  private createOfflineSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return IometerMapper.toSnapshot({ config: this.clientConfig, online: false, source: 'runtime', error: errorArg });
  }
}

const normalizeBaseUrl = (valueArg: string, portArg?: number): URL => {
  const withProtocol = valueArg.includes('://') ? valueArg : `http://${valueArg}`;
  const url = new URL(withProtocol);
  url.pathname = '';
  url.search = '';
  url.hash = '';
  if (portArg) {
    url.port = String(portArg);
  }
  return url;
};

const stringValue = (valueArg: unknown): string | undefined => {
  return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
};

const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;

const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
