import { SimpleLocalClient, SimpleLocalMapper, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { OpenevseMapper } from './openevse.mapper.js';
import type { IOpenevseConfig, IOpenevseRawApiData, IOpenevseSnapshot } from './openevse.types.js';
import { openevseDefaultPort, openevseProfile } from './openevse.types.js';

export class OpenevseClient extends SimpleLocalClient<IOpenevseConfig> {
  private liveSnapshot?: IOpenevseSnapshot;

  constructor(private readonly configArg: IOpenevseConfig) {
    super(openevseProfile, configArg);
  }

  public override async getSnapshot(forceRefreshArg = false): Promise<IOpenevseSnapshot> {
    if (!forceRefreshArg && this.liveSnapshot) {
      return SimpleLocalMapper.clone(this.liveSnapshot);
    }

    if (!this.hasLiveHttpConfig()) {
      const snapshot = await super.getSnapshot(forceRefreshArg);
      this.liveSnapshot = snapshot;
      return SimpleLocalMapper.clone(this.liveSnapshot);
    }

    try {
      this.liveSnapshot = OpenevseMapper.toSnapshot({ config: this.configArg, rawData: await this.getRawData(), online: true, source: 'http' });
    } catch (errorArg) {
      this.liveSnapshot = OpenevseMapper.toSnapshot({ config: this.configArg, online: false, source: 'http', error: errorMessage(errorArg) });
    }
    return SimpleLocalMapper.clone(this.liveSnapshot);
  }

  public override async refresh(): Promise<IServiceCallResult> {
    this.liveSnapshot = undefined;
    if (!this.hasLiveHttpConfig()) {
      return super.refresh();
    }

    const snapshot = await this.getSnapshot(true);
    const success = snapshot.online && !snapshot.error;
    return { success, error: success ? undefined : snapshot.error, data: snapshot };
  }

  public override async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.client?.execute || this.configArg.commandExecutor || !this.hasLiveHttpConfig()) {
      return super.execute(requestArg);
    }
    if (requestArg.service !== 'set_value') {
      return super.execute(requestArg);
    }

    const value = numberValue(requestArg.data?.value ?? requestArg.data?.nativeValue ?? requestArg.data?.current ?? requestArg.data?.chargeRate);
    if (value === undefined) {
      return { success: false, error: 'OpenEVSE set_value requires numeric data.value for the charge_rate entity.' };
    }

    try {
      const rawData = await this.getRawData();
      await this.setCurrent(Math.round(value), rawData.config);
      this.liveSnapshot = undefined;
      return { success: true, data: { charge_rate: Math.round(value) } };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  public async getRawData(): Promise<IOpenevseRawApiData> {
    const [status, config] = await Promise.all([
      this.requestJson('/status'),
      this.requestJson('/config'),
    ]);
    return {
      status,
      config,
      fetchedAt: new Date().toISOString(),
    };
  }

  private async setCurrent(ampsArg: number, configArg: Record<string, unknown>): Promise<void> {
    const min = numberValue(configArg.min_current_hard) ?? 6;
    const max = numberValue(configArg.max_current_hard) ?? 80;
    if (ampsArg < min || ampsArg > max) {
      throw new Error(`OpenEVSE charge_rate ${ampsArg}A is outside supported range ${min}-${max}A.`);
    }

    if (versionAtLeast(stringValue(configArg.version), '4.1.2')) {
      const currentOverride = await this.requestJson('/override').catch(() => ({}));
      const response = await this.requestJson('/override', 'POST', { ...currentOverride, auto_release: true, charge_current: ampsArg });
      const message = stringValue(response.msg);
      if (message && !['done', 'no change'].includes(message)) {
        throw new Error(`OpenEVSE override current command failed: ${message}`);
      }
      return;
    }

    const rapiSuffix = versionAtLeast(stringValue(configArg.version), '2.9.1') ? 'V' : 'N';
    const response = await this.requestJson('/r', 'POST', undefined, { json: '1', rapi: `$SC ${ampsArg} ${rapiSuffix}` });
    if (!('ret' in response)) {
      throw new Error(`OpenEVSE RAPI current command failed: ${JSON.stringify(response).slice(0, 200)}`);
    }
  }

  private async requestJson(pathArg: string, methodArg = 'GET', jsonBodyArg?: Record<string, unknown>, formBodyArg?: Record<string, string>): Promise<Record<string, unknown>> {
    const response = await this.fetch(pathArg, methodArg, jsonBodyArg, formBodyArg);
    const text = await response.text();
    if (response.status === 401) {
      throw new Error('OpenEVSE authentication failed with HTTP 401.');
    }
    if (!response.ok) {
      throw new Error(`OpenEVSE request ${pathArg} failed with HTTP ${response.status}: ${text.slice(0, 160)}`);
    }
    try {
      const parsed = JSON.parse(text) as unknown;
      if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
        throw new Error('response was not a JSON object');
      }
      return parsed as Record<string, unknown>;
    } catch (errorArg) {
      throw new Error(`OpenEVSE request ${pathArg} returned invalid JSON: ${errorMessage(errorArg)}`);
    }
  }

  private async fetch(pathArg: string, methodArg: string, jsonBodyArg?: Record<string, unknown>, formBodyArg?: Record<string, string>): Promise<Response> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('OpenEVSE local HTTP requires config.host or config.url.');
    }

    const headers: Record<string, string> = { accept: 'application/json, text/json;q=0.9, */*;q=0.8' };
    let body: BodyInit | undefined;
    if (this.configArg.username) {
      headers.authorization = `Basic ${Buffer.from(`${this.configArg.username}:${this.configArg.password || ''}`).toString('base64')}`;
    }
    if (jsonBodyArg) {
      headers['content-type'] = 'application/json';
      body = JSON.stringify(jsonBodyArg);
    } else if (formBodyArg) {
      headers['content-type'] = 'application/x-www-form-urlencoded';
      body = new URLSearchParams(formBodyArg);
    }

    return globalThis.fetch(`${endpoint.origin}${pathArg.startsWith('/') ? pathArg : `/${pathArg}`}`, {
      method: methodArg,
      headers,
      body,
      signal: AbortSignal.timeout(this.configArg.timeoutMs || 5000),
    });
  }

  private hasLiveHttpConfig(): boolean {
    return Boolean(this.endpoint()) && !this.configArg.client && !this.configArg.snapshotProvider;
  }

  private endpoint(): IOpenevseEndpoint | undefined {
    const rawUrl = stringValue(this.configArg.url) || stringValue(this.configArg.metadata?.url);
    const parsedUrl = rawUrl ? parseUrl(rawUrl) : undefined;
    const parsedHost = !parsedUrl && this.configArg.host ? parseUrl(this.configArg.host) : undefined;
    const url = parsedUrl || parsedHost;
    if (url) {
      const protocol = url.protocol === 'https:' ? 'https' : 'http';
      const port = this.configArg.port || numberValue(url.port) || (protocol === 'https' ? 443 : openevseDefaultPort);
      return { host: url.hostname, port, protocol, origin: `${protocol}://${url.hostname}${portPart(protocol, port)}` };
    }

    const host = stringValue(this.configArg.host);
    if (!host) {
      return undefined;
    }
    const protocol = this.configArg.useTls ? 'https' : 'http';
    const port = this.configArg.port || (protocol === 'https' ? 443 : openevseDefaultPort);
    return { host, port, protocol, origin: `${protocol}://${host}${portPart(protocol, port)}` };
  }
}

interface IOpenevseEndpoint {
  host: string;
  port: number;
  protocol: 'http' | 'https';
  origin: string;
}

const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
const numberValue = (valueArg: unknown): number | undefined => typeof valueArg === 'number' && Number.isFinite(valueArg) ? valueArg : stringValue(valueArg) && Number.isFinite(Number(valueArg)) ? Number(valueArg) : undefined;
const parseUrl = (valueArg: string): URL | undefined => {
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(valueArg)) {
    return undefined;
  }
  try {
    return new URL(valueArg);
  } catch {
    return undefined;
  }
};
const portPart = (protocolArg: 'http' | 'https', portArg: number): string => (protocolArg === 'http' && portArg === 80) || (protocolArg === 'https' && portArg === 443) ? '' : `:${portArg}`;
const versionAtLeast = (versionArg: string | undefined, minArg: string): boolean => {
  const version = versionArg?.match(/\d+\.\d+\.\d+/)?.[0];
  if (!version) {
    return false;
  }
  const current = version.split('.').map((partArg) => Number(partArg));
  const minimum = minArg.split('.').map((partArg) => Number(partArg));
  for (let index = 0; index < minimum.length; index++) {
    if ((current[index] || 0) > minimum[index]) {
      return true;
    }
    if ((current[index] || 0) < minimum[index]) {
      return false;
    }
  }
  return true;
};
const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
