import { SimpleLocalClient, type IServiceCallRequest, type IServiceCallResult, type ISimpleLocalSnapshot } from '../../core/index.js';
import { MillMapper } from './mill.mapper.js';
import type { IMillConfig, IMillNativeRawData, TMillOperationMode } from './mill.types.js';
import { millDefaultPort, millDefaultTimeoutMs, millProfile } from './mill.types.js';

export class MillApiError extends Error {}
export class MillApiConnectionError extends MillApiError {}
export class MillApiUnsupportedError extends MillApiError {}
export class MillApiValidationError extends MillApiError {}

export class MillClient extends SimpleLocalClient<IMillConfig> {
  private liveSnapshot?: ISimpleLocalSnapshot;
  private readonly millConfig: IMillConfig;

  constructor(configArg: IMillConfig) {
    super(millProfile, configArg);
    this.millConfig = configArg;
  }

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

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

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

  public async getRawData(): Promise<IMillNativeRawData> {
    if (!this.canUseLiveHttp()) {
      throw new MillApiConnectionError('Mill local HTTP polling requires config.host, config.ipAddress, or config.url.');
    }
    const [status, controlStatus] = await Promise.all([
      this.requestJson<Record<string, unknown>>('status'),
      this.requestJson<Record<string, unknown>>('control-status'),
    ]);
    return { status, controlStatus, fetchedAt: new Date().toISOString() };
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.millConfig.commandExecutor) {
      return normalizeServiceResult(await this.millConfig.commandExecutor.execute(requestArg));
    }
    if (!this.canUseLiveHttp()) {
      return super.execute(requestArg);
    }

    try {
      if (requestArg.service === 'refresh') {
        return this.refresh();
      }
      if (requestArg.service === 'set_temperature' || requestArg.service === 'set_value') {
        const temperature = numberValue(requestArg.data?.temperature ?? requestArg.data?.targetTemperature ?? requestArg.data?.value);
        if (temperature === undefined) {
          throw new MillApiValidationError('Mill set_temperature requires data.temperature or data.value.');
        }
        await this.setTargetTemperature(temperature);
        this.liveSnapshot = undefined;
        return { success: true, data: { temperature } };
      }
      if (requestArg.service === 'set_hvac_mode' || requestArg.service === 'turn_on' || requestArg.service === 'turn_off') {
        const mode = this.operationModeForRequest(requestArg);
        await this.setOperationMode(mode);
        this.liveSnapshot = undefined;
        return { success: true, data: { mode } };
      }
      if (requestArg.service === 'set_room_temperature') {
        throw new MillApiUnsupportedError('Mill set_room_temperature is a cloud millheater room service and is not available through the local Gen 3 HTTP API.');
      }
      return { success: false, error: `Unsupported Mill service: ${requestArg.domain}.${requestArg.service}` };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  public async setTargetTemperature(targetTemperatureArg: number): Promise<void> {
    await this.postJson('set-temperature', { type: 'Normal', value: targetTemperatureArg });
  }

  public async setOperationMode(modeArg: TMillOperationMode): Promise<void> {
    await this.postJson('operation-mode', { mode: modeArg });
  }

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

  private operationModeForRequest(requestArg: IServiceCallRequest): TMillOperationMode {
    if (requestArg.service === 'turn_on') {
      return 'Control individually';
    }
    if (requestArg.service === 'turn_off') {
      return 'OFF';
    }
    const mode = stringValue(requestArg.data?.hvac_mode ?? requestArg.data?.hvacMode ?? requestArg.data?.mode)?.toLowerCase();
    if (mode === 'off') {
      return 'OFF';
    }
    if (mode === 'auto' || mode === 'weekly program') {
      return 'Weekly program';
    }
    if (mode === 'heat' || mode === 'control individually' || mode === 'on') {
      return 'Control individually';
    }
    throw new MillApiValidationError('Mill set_hvac_mode requires hvac_mode heat, auto, or off.');
  }

  private async requestJson<TValue>(pathArg: string, optionsArg: IMillRequestOptions = {}): Promise<TValue> {
    const response = await this.request(pathArg, optionsArg);
    const text = await response.text();
    if (!response.ok) {
      throw new MillApiConnectionError(`Mill endpoint ${pathArg} failed with HTTP ${response.status}${text ? `: ${text}` : ''}`);
    }
    if (!text.trim()) {
      return {} as TValue;
    }
    try {
      return JSON.parse(text) as TValue;
    } catch (errorArg) {
      throw new MillApiConnectionError(`Unable to parse Mill JSON from ${pathArg}: ${errorMessage(errorArg)}`);
    }
  }

  private async postJson<TValue>(pathArg: string, dataArg: unknown): Promise<TValue> {
    return this.requestJson<TValue>(pathArg, { method: 'POST', data: dataArg });
  }

  private async request(pathArg: string, optionsArg: IMillRequestOptions = {}): Promise<Response> {
    const url = this.url(pathArg);
    const headers = new Headers(optionsArg.headers);
    let body: BodyInit | undefined;
    if (optionsArg.data !== undefined) {
      headers.set('content-type', 'application/json');
      body = JSON.stringify(optionsArg.data);
    }
    try {
      return await globalThis.fetch(url, {
        method: optionsArg.method || 'GET',
        headers,
        body,
        signal: AbortSignal.timeout(this.millConfig.timeoutMs || millDefaultTimeoutMs),
      });
    } catch (errorArg) {
      throw new MillApiConnectionError(`Connection to ${url} failed: ${errorMessage(errorArg)}`);
    }
  }

  private url(pathArg: string): string {
    if (/^https?:\/\//i.test(pathArg)) {
      return pathArg;
    }
    const endpoint = this.endpoint();
    return new URL(pathArg.startsWith('/') ? pathArg : `/${pathArg}`, `${endpoint.protocol}://${hostForUrl(endpoint.host)}:${endpoint.port}`).toString();
  }

  private endpoint(): { protocol: 'http' | 'https'; host: string; port: number } {
    const configured = this.millConfig.url || this.millConfig.host || this.millConfig.ipAddress;
    if (!configured) {
      throw new MillApiConnectionError('Mill local HTTP requests require config.host, config.ipAddress, or config.url.');
    }
    if (/^https?:\/\//i.test(configured)) {
      const url = new URL(configured);
      return {
        protocol: url.protocol === 'https:' ? 'https' : 'http',
        host: url.hostname,
        port: this.millConfig.port || (url.port ? Number(url.port) : undefined) || millDefaultPort,
      };
    }
    return {
      protocol: this.millConfig.useTls ? 'https' : 'http',
      host: configured,
      port: this.millConfig.port || millDefaultPort,
    };
  }

  private canUseLiveHttp(): boolean {
    return Boolean(this.millConfig.host || this.millConfig.url || this.millConfig.ipAddress);
  }
}

interface IMillRequestOptions {
  method?: 'GET' | 'POST';
  headers?: HeadersInit;
  data?: unknown;
}

const normalizeServiceResult = (valueArg: unknown): IServiceCallResult => {
  if (valueArg && typeof valueArg === 'object' && 'success' in valueArg && typeof (valueArg as IServiceCallResult).success === 'boolean') {
    return valueArg as IServiceCallResult;
  }
  return { success: true, data: valueArg };
};

const hostForUrl = (hostArg: string): string => hostArg.includes(':') && !hostArg.startsWith('[') ? `[${hostArg}]` : hostArg;
const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
const numberValue = (valueArg: unknown): number | undefined => {
  if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
    return valueArg;
  }
  if (typeof valueArg === 'string' && valueArg.trim()) {
    const parsed = Number(valueArg);
    return Number.isFinite(parsed) ? parsed : undefined;
  }
  return undefined;
};
const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;
