import { SimpleLocalClient } from '../../core/index.js';
import type { IServiceCallRequest, IServiceCallResult, ISimpleLocalSnapshot } from '../../core/index.js';
import { LunatoneMapper, lunatoneControlBodyForService } from './lunatone.mapper.js';
import type { ILunatoneConfig, ILunatoneDeviceData, ILunatoneRawData } from './lunatone.types.js';
import { lunatoneProfile } from './lunatone.types.js';

export class LunatoneClient extends SimpleLocalClient<ILunatoneConfig> {
  private snapshot?: ISimpleLocalSnapshot;

  constructor(private readonly configData: ILunatoneConfig) {
    super(lunatoneProfile, configData);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<ISimpleLocalSnapshot> {
    if (!forceRefreshArg && this.snapshot) {
      return clone(this.snapshot);
    }
    if (this.canUseNativeHttp()) {
      try {
        this.snapshot = await this.fetchSnapshot();
        return clone(this.snapshot);
      } catch (errorArg) {
        this.snapshot = LunatoneMapper.toSnapshotFromApi(this.configData, {}, false, 'runtime', errorMessage(errorArg));
        return clone(this.snapshot);
      }
    }
    this.snapshot = await super.getSnapshot(forceRefreshArg);
    return clone(this.snapshot);
  }

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

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configData.commandExecutor || this.configData.client?.execute) {
      return super.execute(requestArg);
    }
    if (!this.canUseNativeHttp()) {
      return super.execute(requestArg);
    }
    if (!['turn_on', 'turn_off', 'toggle'].includes(requestArg.service)) {
      return { success: false, error: `Unsupported Lunatone service: ${requestArg.service}` };
    }
    try {
      const result = await this.executeLive(requestArg);
      this.snapshot = undefined;
      return { success: true, data: result };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  private async fetchSnapshot(): Promise<ISimpleLocalSnapshot> {
    const rawData: ILunatoneRawData = {
      info: await this.getJson('info'),
      devices: await this.getJson('devices'),
    };
    return LunatoneMapper.toSnapshotFromApi(this.configData, rawData, true, 'http');
  }

  private async executeLive(requestArg: IServiceCallRequest): Promise<Record<string, unknown>> {
    const snapshot = await this.getSnapshot(false);
    const rawData = snapshot.rawData && typeof snapshot.rawData === 'object' ? snapshot.rawData as ILunatoneRawData : {};
    const target = this.resolveTarget(requestArg, rawData);
    const service = requestArg.service === 'toggle' ? target.currentState ? 'turn_off' : 'turn_on' : requestArg.service;
    const body = lunatoneControlBodyForService(service, requestArg.data, target.device);
    if (target.kind === 'device') {
      await this.postJson(`device/${target.id}/control`, body);
    } else {
      await this.postJson(`broadcast/control?_line=${encodeURIComponent(String(target.id))}`, body);
    }
    return { transmitted: true, target, body };
  }

  private resolveTarget(requestArg: IServiceCallRequest, rawDataArg: ILunatoneRawData): { kind: 'device' | 'line'; id: number; device?: ILunatoneDeviceData; currentState?: boolean } {
    const targetValue = requestArg.target?.entityId || requestArg.target?.deviceId || stringValue(requestArg.data?.entity_id) || stringValue(requestArg.data?.entityId) || stringValue(requestArg.data?.device_id) || stringValue(requestArg.data?.deviceId) || '';
    const deviceId = numberFromMatch(targetValue, /device_(\d+)/) ?? numberValue(requestArg.data?.id) ?? numberValue(requestArg.data?.device);
    if (deviceId !== undefined) {
      const devices = Array.isArray(rawDataArg.devices) ? rawDataArg.devices : rawDataArg.devices?.devices || [];
      const device = devices.find((deviceArg) => deviceArg.id === deviceId);
      return { kind: 'device', id: deviceId, device, currentState: device ? Boolean(featureStatus(device, 'switchable') ?? ((numberValue(featureStatus(device, 'dimmable')) || 0) > 0)) : undefined };
    }
    const lineId = numberFromMatch(targetValue, /line_(\d+)/) ?? numberValue(requestArg.data?.line);
    if (lineId !== undefined) {
      return { kind: 'line', id: lineId, currentState: true };
    }
    throw new Error('Lunatone control requires a target entityId containing device_<id> or line_<line>, or data.id/data.line.');
  }

  private async getJson<TValue>(pathArg: string): Promise<TValue> {
    const response = await this.fetchWithTimeout(`${this.baseUrl()}/${pathArg.replace(/^\/+/, '')}`, { method: 'GET', headers: this.headers() });
    return await this.responseJson<TValue>(response, pathArg);
  }

  private async postJson(pathArg: string, bodyArg: Record<string, unknown>): Promise<unknown> {
    const response = await this.fetchWithTimeout(`${this.baseUrl()}/${pathArg.replace(/^\/+/, '')}`, { method: 'POST', headers: { ...this.headers(), 'content-type': 'application/json' }, body: JSON.stringify(bodyArg) });
    if (response.status === 204) {
      return undefined;
    }
    return await this.responseJson(response, pathArg);
  }

  private async responseJson<TValue>(responseArg: Response, pathArg: string): Promise<TValue> {
    const text = await responseArg.text();
    if (!responseArg.ok) {
      throw new Error(`Lunatone REST ${pathArg} failed with HTTP ${responseArg.status}: ${text.slice(0, 160)}`);
    }
    if (!text) {
      return {} as TValue;
    }
    const trimmed = text.trim();
    return (trimmed.startsWith('{') || trimmed.startsWith('[') ? JSON.parse(trimmed) : trimmed) as TValue;
  }

  private async fetchWithTimeout(urlArg: string, initArg: RequestInit): Promise<Response> {
    const abortController = new AbortController();
    const timeout = setTimeout(() => abortController.abort(), this.configData.timeoutMs || 5000);
    try {
      return await globalThis.fetch(urlArg, { ...initArg, signal: abortController.signal });
    } finally {
      clearTimeout(timeout);
    }
  }

  private headers(): Record<string, string> {
    const headers: Record<string, string> = { accept: 'application/json' };
    const token = this.configData.accessToken || this.configData.token;
    if (token) {
      headers.authorization = token;
    }
    return headers;
  }

  private canUseNativeHttp(): boolean {
    return this.configData.nativeHttpEnabled !== false && Boolean(this.baseUrl(false));
  }

  private baseUrl(requiredArg = true): string {
    if (this.configData.url) {
      return this.configData.url.replace(/\/+$/, '');
    }
    if (!this.configData.host) {
      if (requiredArg) {
        throw new Error('Lunatone native REST requires config.url or config.host.');
      }
      return '';
    }
    if (/^https?:\/\//i.test(this.configData.host)) {
      return this.configData.host.replace(/\/+$/, '');
    }
    const port = this.configData.port;
    const portPart = port && !this.configData.host.includes(':') && port !== 80 ? `:${port}` : '';
    return `${this.configData.useTls ? 'https' : 'http'}://${this.configData.host}${portPart}`;
  }
}

const featureStatus = (deviceArg: ILunatoneDeviceData, featureArg: string): unknown => {
  const value = deviceArg.features?.[featureArg];
  if (value && typeof value === 'object' && !Array.isArray(value) && 'status' in value) {
    return (value as { status?: unknown }).status;
  }
  return value;
};

const numberFromMatch = (valueArg: string, regexArg: RegExp): number | undefined => {
  const match = valueArg.match(regexArg);
  return match ? Number(match[1]) : undefined;
};

const numberValue = (valueArg: unknown): number | undefined => {
  if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
    return valueArg;
  }
  if (typeof valueArg === 'string' && valueArg.trim() && Number.isFinite(Number(valueArg))) {
    return Number(valueArg);
  }
  return undefined;
};

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);
