import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { MystromMapper } from './mystrom.mapper.js';
import type { IMystromConfig } from './mystrom.types.js';
import { mystromDefaultPort, mystromDefaultTimeoutMs, mystromProfile } from './mystrom.types.js';

type TMystromDeviceKind = 'switch' | 'bulb' | 'pir';

interface IMystromEndpoint {
  protocol: 'http' | 'https';
  host: string;
  port?: number;
}

interface IMystromDeviceInfo {
  ip?: string;
  mac?: string;
  type?: number | string;
  version?: string;
  [key: string]: unknown;
}

interface IMystromSwitchReport {
  relay?: boolean | number | string;
  power?: number;
  Ws?: number;
  energy_since_boot?: number;
  time_since_boot?: number;
  boot_id?: string;
  temperature?: number;
  [key: string]: unknown;
}

interface IMystromBulbState {
  type?: string;
  on?: boolean;
  color?: string;
  mode?: string;
  ramp?: number;
  power?: number;
  fw_version?: string;
  [key: string]: unknown;
}

interface IMystromPirLightState {
  intensity?: number;
  day?: boolean;
  raw?: Record<string, unknown>;
  [key: string]: unknown;
}

interface IMystromPirMotionState {
  motion?: boolean;
  [key: string]: unknown;
}

interface IMystromPirTemperatureState {
  measured?: number;
  compensated?: number;
  compensation?: number;
  [key: string]: unknown;
}

const switchDeviceTypes = new Set(['101', '106', '107', '120']);
const bulbDeviceTypes = new Set(['102', '105', 'rgblamp', 'strip']);
const pirDeviceTypes = new Set(['110']);

export class MystromClient extends SimpleLocalClient<IMystromConfig> {
  private cachedSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly configArg: IMystromConfig) {
    super(mystromProfile, configArg);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<ISimpleLocalSnapshot> {
    if (!forceRefreshArg && this.cachedSnapshot) {
      return clone(this.cachedSnapshot);
    }
    if (this.configArg.client || this.configArg.snapshotProvider || (!forceRefreshArg && this.configArg.snapshot)) {
      this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
      return clone(this.cachedSnapshot);
    }
    if (!this.endpoint()) {
      this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
      return clone(this.cachedSnapshot);
    }

    try {
      this.cachedSnapshot = await this.fetchSnapshot();
    } catch (errorArg) {
      this.cachedSnapshot = MystromMapper.toSnapshot({ config: this.configArg, online: false, source: 'runtime', error: errorMessage(errorArg) });
    }
    return clone(this.cachedSnapshot);
  }

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

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.client?.execute || this.configArg.commandExecutor) {
      return super.execute(requestArg);
    }
    if (!this.endpoint()) {
      return { success: false, error: 'myStrom local control requires config.host/config.url, an injected client.execute(), or commandExecutor.' };
    }
    if (!['turn_on', 'turn_off', 'toggle'].includes(requestArg.service)) {
      return { success: false, error: `Unsupported myStrom service: ${requestArg.domain}.${requestArg.service}` };
    }

    try {
      const info = await this.getDeviceInfo();
      const kind = this.deviceKind(info);
      let response: unknown;
      if (kind === 'switch') {
        response = await this.executeSwitch(requestArg);
      } else if (kind === 'bulb') {
        response = await this.executeBulb(info, requestArg);
      } else {
        return { success: false, error: 'myStrom motion sensors do not expose Home Assistant switch/light control through the documented local HTTP API.' };
      }
      this.cachedSnapshot = undefined;
      return { success: true, data: { deviceType: info.type, kind, response } };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

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

  private async fetchSnapshot(): Promise<ISimpleLocalSnapshot> {
    const info = await this.getDeviceInfo();
    const kind = this.deviceKind(info);

    if (kind === 'switch') {
      return this.fetchSwitchSnapshot(info);
    }
    if (kind === 'bulb') {
      return this.fetchBulbSnapshot(info);
    }
    if (kind === 'pir') {
      return this.fetchPirSnapshot(info);
    }
    throw new Error(`Unsupported myStrom device type: ${String(info.type)}`);
  }

  private async fetchSwitchSnapshot(infoArg: IMystromDeviceInfo): Promise<ISimpleLocalSnapshot> {
    const report = await this.requestJson<IMystromSwitchReport>('/report');
    const device = this.deviceInfo(infoArg, 'switch');
    const entities = [
      {
        id: 'relay',
        uniqueId: `${device.id}_relay`,
        name: 'Relay',
        platform: 'switch' as const,
        state: boolState(report.relay),
        available: true,
        writable: true,
        attributes: { source: '/report' },
      },
      sensorEntity(device.id, 'consumption', 'Consumption', report.power, 'W', 'power'),
      sensorEntity(device.id, 'avg_consumption', 'Average Consumption', report.Ws, 'W', 'power'),
      sensorEntity(device.id, 'energy_since_boot', 'Energy Since Boot', report.energy_since_boot, 'Ws', 'energy'),
      sensorEntity(device.id, 'temperature', 'Temperature', report.temperature, 'C', 'temperature'),
      sensorEntity(device.id, 'time_since_boot', 'Time Since Boot', report.time_since_boot, 's', undefined),
    ].filter((entityArg) => entityArg.state !== undefined);

    return MystromMapper.toSnapshot({
      config: this.configArg,
      rawData: { device, info: infoArg, report, entities, fetchedAt: new Date().toISOString() },
      online: true,
      source: 'http',
    });
  }

  private async fetchBulbSnapshot(infoArg: IMystromDeviceInfo): Promise<ISimpleLocalSnapshot> {
    const mac = this.macAddress(infoArg);
    if (!mac) {
      throw new Error('myStrom bulb polling requires a MAC address from /api/v1/info or config.macAddress.');
    }
    const response = await this.requestJson<Record<string, IMystromBulbState>>(`/api/v1/device/${encodeURIComponent(mac)}`);
    const state = response[mac] || response[mac.toUpperCase()] || response[mac.toLowerCase()] || Object.values(response)[0];
    if (!state || typeof state !== 'object') {
      throw new Error('myStrom bulb state response did not contain the MAC keyed payload.');
    }

    const device = this.deviceInfo({ ...infoArg, mac, version: stringValue(infoArg.version) || stringValue(state.fw_version) }, 'bulb');
    const color = parseMystromColor(state.color);
    const entities = [
      {
        id: 'light',
        uniqueId: `${device.id}_light`,
        name: 'Light',
        platform: 'light' as const,
        state: Boolean(state.on),
        available: true,
        writable: true,
        attributes: cleanAttributes({ color: state.color, mode: state.mode, ramp: state.ramp, brightness: color.brightness }),
      },
      sensorEntity(device.id, 'power', 'Power', state.power, 'W', 'power'),
    ].filter((entityArg) => entityArg.state !== undefined);

    return MystromMapper.toSnapshot({
      config: this.configArg,
      rawData: { device, info: infoArg, bulb: state, entities, fetchedAt: new Date().toISOString() },
      online: true,
      source: 'http',
    });
  }

  private async fetchPirSnapshot(infoArg: IMystromDeviceInfo): Promise<ISimpleLocalSnapshot> {
    const [light, motion, temperature] = await Promise.all([
      this.requestJson<IMystromPirLightState>('/api/v1/light'),
      this.requestJson<IMystromPirMotionState>('/api/v1/motion'),
      this.requestJson<IMystromPirTemperatureState>('/temp'),
    ]);
    const device = this.deviceInfo(infoArg, 'pir');
    const entities = [
      {
        id: 'motion',
        uniqueId: `${device.id}_motion`,
        name: 'Motion',
        platform: 'binary_sensor' as const,
        state: Boolean(motion.motion),
        available: true,
        writable: false,
        attributes: { source: '/api/v1/motion' },
      },
      sensorEntity(device.id, 'illuminance', 'Illuminance', light.intensity, 'lx', 'illuminance'),
      sensorEntity(device.id, 'temperature', 'Temperature', temperature.compensated ?? temperature.measured, 'C', 'temperature'),
    ].filter((entityArg) => entityArg.state !== undefined);

    return MystromMapper.toSnapshot({
      config: this.configArg,
      rawData: { device, info: infoArg, light, motion, temperature, entities, fetchedAt: new Date().toISOString() },
      online: true,
      source: 'http',
    });
  }

  private async getDeviceInfo(): Promise<IMystromDeviceInfo> {
    const configuredType = this.configArg.deviceType;
    try {
      const info = await this.requestJson<IMystromDeviceInfo>('/api/v1/info');
      return { ...info, type: configuredType ?? info.type ?? 101 };
    } catch {
      const info = await this.requestJson<IMystromDeviceInfo>('/info.json');
      return { ...info, type: configuredType ?? info.type ?? 101 };
    }
  }

  private async executeSwitch(requestArg: IServiceCallRequest): Promise<unknown> {
    if (requestArg.service === 'toggle') {
      return this.requestMaybeJson('/toggle');
    }
    const state = requestArg.service === 'turn_on' ? '1' : '0';
    return this.requestMaybeJson(`/relay?state=${state}`);
  }

  private async executeBulb(infoArg: IMystromDeviceInfo, requestArg: IServiceCallRequest): Promise<unknown> {
    const mac = this.macAddress(infoArg);
    if (!mac) {
      throw new Error('myStrom bulb control requires a MAC address from /api/v1/info or config.macAddress.');
    }
    const data = requestArg.data || {};
    const body = new URLSearchParams();
    body.set('action', requestArg.service === 'turn_off' ? 'off' : requestArg.service === 'toggle' ? 'toggle' : 'on');

    if (requestArg.service === 'turn_on') {
      const hsv = hsColor(data.hs_color) || hsColor(data.hsColor);
      const brightness = numberValue(data.brightness);
      if (hsv || brightness !== undefined) {
        const hue = Math.round(hsv?.[0] ?? 0);
        const saturation = Math.round(hsv?.[1] ?? 0);
        const value = clamp(Math.round((brightness ?? 255) * 100 / 255), 0, 100);
        body.set('color', `${hue};${saturation};${value}`);
      }
      const ramp = numberValue(data.transitionMs ?? data.ramp);
      if (ramp !== undefined) {
        body.set('ramp', String(Math.max(0, Math.round(ramp))));
      }
    }

    return this.requestMaybeJson(`/api/v1/device/${encodeURIComponent(mac)}`, {
      method: 'POST',
      headers: { 'content-type': 'application/x-www-form-urlencoded' },
      body,
    });
  }

  private deviceInfo(infoArg: IMystromDeviceInfo, kindArg: TMystromDeviceKind): Record<string, unknown> {
    const endpoint = this.endpoint();
    const host = endpoint?.host || this.configArg.host || infoArg.ip;
    const port = endpoint?.port || this.configArg.port || mystromDefaultPort;
    const mac = this.macAddress(infoArg);
    const id = this.configArg.uniqueId || this.configArg.deviceId || mac || (host ? `${host}:${port || ''}` : mystromProfile.domain);
    return {
      id,
      name: this.configArg.name || nameForKind(kindArg),
      manufacturer: mystromProfile.manufacturer,
      model: modelForType(infoArg.type, kindArg),
      serialNumber: mac,
      host,
      port,
      protocol: endpoint?.protocol || 'http',
      configurationUrl: host ? `${endpoint?.protocol || 'http'}://${host}${port && port !== 80 ? `:${port}` : ''}` : undefined,
      attributes: cleanAttributes({ ...infoArg, mac, kind: kindArg }),
    };
  }

  private deviceKind(infoArg: IMystromDeviceInfo): TMystromDeviceKind {
    const rawType = this.configArg.deviceType ?? infoArg.type ?? 101;
    const key = String(rawType).toLowerCase();
    if (bulbDeviceTypes.has(key)) {
      return 'bulb';
    }
    if (pirDeviceTypes.has(key)) {
      return 'pir';
    }
    if (switchDeviceTypes.has(key)) {
      return 'switch';
    }
    throw new Error(`Unsupported myStrom device type: ${String(rawType)}`);
  }

  private macAddress(infoArg: IMystromDeviceInfo): string | undefined {
    return stringValue(this.configArg.macAddress) || stringValue(infoArg.mac);
  }

  private async requestJson<TValue>(pathArg: string, initArg: RequestInit = {}): Promise<TValue> {
    const text = await this.requestText(pathArg, initArg);
    try {
      const parsed = JSON.parse(text) as TValue;
      if (!parsed || typeof parsed !== 'object') {
        throw new Error('response was not a JSON object');
      }
      return parsed;
    } catch (errorArg) {
      throw new Error(`myStrom local JSON parse failed for ${pathArg}: ${errorMessage(errorArg)}`);
    }
  }

  private async requestMaybeJson(pathArg: string, initArg: RequestInit = {}): Promise<unknown> {
    const text = await this.requestText(pathArg, initArg);
    if (!text.trim()) {
      return true;
    }
    try {
      return JSON.parse(text) as unknown;
    } catch {
      return text;
    }
  }

  private async requestText(pathArg: string, initArg: RequestInit = {}): Promise<string> {
    const timeoutMs = this.configArg.timeoutMs || mystromDefaultTimeoutMs;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      const headers: Record<string, string> = { accept: 'application/json, text/plain;q=0.9, */*;q=0.8' };
      if (this.configArg.token) {
        headers.Token = this.configArg.token;
      }
      for (const [key, value] of Object.entries(initArg.headers || {})) {
        headers[key] = String(value);
      }
      const response = await globalThis.fetch(this.url(pathArg), { ...initArg, headers, signal: controller.signal });
      const text = await response.text();
      if (!response.ok) {
        throw new Error(`myStrom local 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('myStrom local HTTP request requires config.host or config.url.');
    }
    const base = `${endpoint.protocol}://${endpoint.host}${endpoint.port && !((endpoint.protocol === 'https' && endpoint.port === 443) || (endpoint.protocol === 'http' && endpoint.port === 80)) ? `:${endpoint.port}` : ''}/`;
    return new URL(pathArg.replace(/^\//, ''), base).toString();
  }

  private endpoint(): IMystromEndpoint | undefined {
    const parsed = parseEndpoint(this.configArg.url);
    if (parsed) {
      return parsed;
    }
    if (!this.configArg.host) {
      return undefined;
    }
    return {
      protocol: this.configArg.useTls ? 'https' : 'http',
      host: this.configArg.host,
      port: this.configArg.port || mystromDefaultPort,
    };
  }
}

function sensorEntity(deviceIdArg: unknown, idArg: string, nameArg: string, stateArg: unknown, unitArg?: string, deviceClassArg?: string) {
  return {
    id: idArg,
    uniqueId: `${String(deviceIdArg || 'mystrom')}_${idArg}`,
    name: nameArg,
    platform: 'sensor' as const,
    state: stateArg,
    available: stateArg !== undefined,
    writable: false,
    unit: unitArg,
    deviceClass: deviceClassArg,
    attributes: { source: '/report' },
  };
}

function parseMystromColor(valueArg: unknown): { brightness?: number } {
  if (typeof valueArg !== 'string') {
    return {};
  }
  const parts = valueArg.split(';').map((partArg) => Number(partArg));
  const brightness = parts.length >= 3 ? parts[2] : parts.length >= 2 ? parts[1] : undefined;
  return Number.isFinite(brightness) ? { brightness } : {};
}

function hsColor(valueArg: unknown): [number, number] | undefined {
  if (!Array.isArray(valueArg) || valueArg.length < 2) {
    return undefined;
  }
  const hue = numberValue(valueArg[0]);
  const saturation = numberValue(valueArg[1]);
  return hue === undefined || saturation === undefined ? undefined : [hue, saturation];
}

function boolState(valueArg: unknown): boolean {
  if (typeof valueArg === 'string') {
    return valueArg === '1' || valueArg.toLowerCase() === 'true' || valueArg.toLowerCase() === 'on';
  }
  return Boolean(valueArg);
}

function modelForType(typeArg: unknown, kindArg: TMystromDeviceKind): string {
  const key = String(typeArg ?? '').toLowerCase();
  const names: Record<string, string> = {
    '101': 'myStrom Switch CH v1',
    '102': 'myStrom Bulb',
    '105': 'myStrom LED Strip',
    '106': 'myStrom Switch CH v2',
    '107': 'myStrom Switch EU',
    '110': 'myStrom Motion Sensor',
    '120': 'myStrom Switch Zero',
    rgblamp: 'myStrom Bulb',
    strip: 'myStrom LED Strip',
  };
  return names[key] || `myStrom ${kindArg}`;
}

function nameForKind(kindArg: TMystromDeviceKind): string {
  if (kindArg === 'bulb') {
    return 'myStrom Bulb';
  }
  if (kindArg === 'pir') {
    return 'myStrom Motion Sensor';
  }
  return 'myStrom Switch';
}

function parseEndpoint(valueArg: unknown): IMystromEndpoint | undefined {
  if (typeof valueArg !== 'string' || !valueArg.trim()) {
    return undefined;
  }
  try {
    const parsed = new URL(valueArg.includes('://') ? valueArg : `http://${valueArg}`);
    return {
      protocol: parsed.protocol === 'https:' ? 'https' : 'http',
      host: parsed.hostname,
      port: parsed.port ? Number(parsed.port) : undefined,
    };
  } catch {
    return undefined;
  }
}

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

function numberValue(valueArg: unknown): number | undefined {
  const value = typeof valueArg === 'number' ? valueArg : typeof valueArg === 'string' && valueArg.trim() ? Number(valueArg) : undefined;
  return value !== undefined && Number.isFinite(value) ? value : undefined;
}

function clamp(valueArg: number, minArg: number, maxArg: number): number {
  return Math.min(maxArg, Math.max(minArg, valueArg));
}

function cleanAttributes<TValue extends Record<string, unknown>>(valueArg: TValue): TValue {
  return Object.fromEntries(Object.entries(valueArg).filter(([, value]) => value !== undefined && value !== null && value !== '')) as TValue;
}

function clone<TValue>(valueArg: TValue): TValue {
  return JSON.parse(JSON.stringify(valueArg)) as TValue;
}

function errorMessage(errorArg: unknown): string {
  return errorArg instanceof Error ? errorArg.message : String(errorArg);
}
