import { SimpleLocalClient, SimpleLocalMapper, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { OpengarageMapper } from './opengarage.mapper.js';
import type { IOpengarageConfig, IOpengarageRawApiData, IOpengarageSnapshot } from './opengarage.types.js';
import { opengarageDefaultPort, opengarageProfile } from './opengarage.types.js';

export class OpengarageClient extends SimpleLocalClient<IOpengarageConfig> {
  private liveSnapshot?: IOpengarageSnapshot;

  constructor(private readonly configArg: IOpengarageConfig) {
    super(opengarageProfile, configArg);
  }

  public override async getSnapshot(forceRefreshArg = false): Promise<IOpengarageSnapshot> {
    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 = OpengarageMapper.toSnapshot({ config: this.configArg, rawData: await this.getRawData(), online: true, source: 'http' });
    } catch (errorArg) {
      this.liveSnapshot = OpengarageMapper.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);
    }

    try {
      const command = this.commandForRequest(requestArg);
      const response = await this.requestJson(`/cc?${new URLSearchParams({ dkey: this.deviceKey(), [command]: '1' }).toString()}`);
      const result = numberValue(response.result);
      if (result === 1) {
        this.liveSnapshot = undefined;
        return { success: true, data: { command, response } };
      }
      if (result === 2) {
        return { success: false, error: 'OpenGarage command failed: device key is incorrect.', data: response };
      }
      return { success: false, error: `OpenGarage command failed with result ${result ?? 'unknown'}.`, data: response };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  public async getRawData(): Promise<IOpengarageRawApiData> {
    return {
      status: await this.requestJson('/jc'),
      fetchedAt: new Date().toISOString(),
    };
  }

  private commandForRequest(requestArg: IServiceCallRequest): 'click' | 'reboot' | 'apmode' {
    if (requestArg.service === 'open_cover' || requestArg.service === 'close_cover' || requestArg.service === 'stop_cover') {
      return 'click';
    }
    if (requestArg.service === 'press') {
      const key = slug(stringValue(requestArg.data?.key ?? requestArg.data?.action ?? requestArg.target.entityId ?? requestArg.target.deviceId) || 'restart');
      if (key.includes('restart') || key.includes('reboot')) {
        return 'reboot';
      }
      if (key.includes('apmode') || key.includes('ap_mode')) {
        return 'apmode';
      }
      if (key.includes('click') || key.includes('garage')) {
        return 'click';
      }
    }
    throw new Error(`Unsupported OpenGarage service: ${requestArg.service}.`);
  }

  private async requestJson(pathArg: string): Promise<Record<string, unknown>> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('OpenGarage local HTTP requires config.host or config.url.');
    }

    const response = await globalThis.fetch(`${endpoint.origin}${pathArg.startsWith('/') ? pathArg : `/${pathArg}`}`, {
      headers: { accept: 'application/json, text/json;q=0.9, */*;q=0.8' },
      signal: AbortSignal.timeout(this.configArg.timeoutMs || 5000),
    });
    const text = await response.text();
    if (!response.ok) {
      throw new Error(`OpenGarage 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(`OpenGarage request ${pathArg} returned invalid JSON: ${errorMessage(errorArg)}`);
    }
  }

  private deviceKey(): string {
    const key = stringValue(this.configArg.deviceKey) || stringValue(this.configArg.device_key) || stringValue(this.configArg.apiKey) || stringValue(this.configArg.token);
    if (!key) {
      throw new Error('OpenGarage commands require config.deviceKey, device_key, apiKey, or token.');
    }
    return key;
  }

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

  private endpoint(): IOpengarageEndpoint | 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 : opengarageDefaultPort);
      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 : opengarageDefaultPort);
    return { host, port, protocol, origin: `${protocol}://${host}${portPart(protocol, port)}` };
  }
}

interface IOpengarageEndpoint {
  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 slug = (valueArg: string): string => valueArg.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
