import { SimpleLocalClient, type ISimpleLocalEntitySnapshot, type IServiceCallRequest, type IServiceCallResult, type TEntityPlatform } from '../../core/index.js';
import { OverkizMapper } from './overkiz.mapper.js';
import type { IOverkizConfig, IOverkizSnapshot } from './overkiz.types.js';
import { overkizDefaultName, overkizProfile } from './overkiz.types.js';

interface IOverkizEndpoint {
  apiBase: string;
  origin: string;
  host: string;
  port?: number;
  protocol: 'http' | 'https';
}

interface IOverkizRequestOptions {
  method?: 'GET' | 'POST' | 'DELETE';
  body?: unknown;
}

interface IOverkizGateway {
  id?: string;
  type?: string | Record<string, unknown>;
  subType?: string;
  sub_type?: string;
  connectivity?: Record<string, unknown>;
}

interface IOverkizDevice {
  deviceURL?: string;
  deviceUrl?: string;
  device_url?: string;
  label?: string;
  controllableName?: string;
  controllable_name?: string;
  widget?: string;
  uiClass?: string;
  ui_class?: string;
  protocol?: string;
  available?: boolean;
  enabled?: boolean;
  states?: Array<Record<string, unknown>> | Record<string, Record<string, unknown>>;
  attributes?: Record<string, Record<string, unknown>>;
  definition?: Record<string, unknown>;
}

export class OverkizClient extends SimpleLocalClient<IOverkizConfig> {
  private currentOverkizSnapshot?: IOverkizSnapshot;

  constructor(private readonly configArg: IOverkizConfig) {
    super(overkizProfile, configArg);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<IOverkizSnapshot> {
    if (!forceRefreshArg && this.currentOverkizSnapshot) {
      return clone(this.currentOverkizSnapshot);
    }

    if (this.configArg.apiType === 'cloud' && !this.hasOfflineSource()) {
      this.currentOverkizSnapshot = this.offlineOverkizSnapshot('Overkiz cloud authentication is not implemented natively; configure a local Developer Mode host and token or use an injected client/snapshot.');
      return clone(this.currentOverkizSnapshot);
    }

    if (this.shouldUseLocalApi()) {
      try {
        this.currentOverkizSnapshot = await this.fetchLocalSnapshot();
      } catch (errorArg) {
        this.currentOverkizSnapshot = this.offlineOverkizSnapshot(errorMessage(errorArg));
      }
      return clone(this.currentOverkizSnapshot);
    }

    if (this.endpoint() && !this.accessToken() && !this.hasOfflineSource()) {
      this.currentOverkizSnapshot = this.offlineOverkizSnapshot('Overkiz local API polling requires config.token from Somfy Developer Mode for Bearer authentication.');
      return clone(this.currentOverkizSnapshot);
    }

    return await super.getSnapshot(forceRefreshArg) as IOverkizSnapshot;
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.configArg.commandExecutor || this.configArg.client?.execute) {
      return super.execute(requestArg);
    }

    if (!this.shouldUseLocalApi()) {
      return {
        success: false,
        error: 'Overkiz live commands require config.host/config.url plus a local Developer Mode config.token, an injected client.execute, or commandExecutor.',
      };
    }

    try {
      const result = await this.executeLocalCommand(requestArg);
      this.currentOverkizSnapshot = undefined;
      return { success: true, data: result };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.currentOverkizSnapshot = undefined;
    const liveCapable = Boolean(this.hasOfflineSource() || this.shouldUseLocalApi());
    const snapshot = await this.getSnapshot(liveCapable);
    const success = liveCapable && snapshot.online && !snapshot.error && snapshot.source !== 'runtime';
    return { success, error: success ? undefined : snapshot.error || 'Overkiz refresh requires a live local API endpoint, injected client, or snapshotProvider.', data: snapshot };
  }

  public async fetchLocalSnapshot(): Promise<IOverkizSnapshot> {
    const setup = await this.requestJson<Record<string, unknown>>('setup');
    const devices = arrayValue<IOverkizDevice>(setup.devices);
    const gateways = arrayValue<IOverkizGateway>(setup.gateways);
    const gateway = gateways[0];
    const endpoint = this.requireEndpoint();
    const rawData = { setup, devices, gateways };
    return OverkizMapper.toSnapshot({
      config: this.configArg,
      rawData: {
        device: {
          id: this.configArg.uniqueId || gateway?.id || endpoint.host || overkizProfile.domain,
          name: this.configArg.name || gatewayName(gateway) || overkizDefaultName,
          manufacturer: 'Somfy',
          model: gatewayModel(gateway) || 'Somfy TaHoma local API',
          serialNumber: gateway?.id,
          host: endpoint.host,
          port: endpoint.port,
          protocol: endpoint.protocol,
          configurationUrl: endpoint.origin,
          attributes: cleanAttributes({ gatewayType: gatewayType(gateway), protocolVersion: gateway?.connectivity?.protocolVersion }),
        },
        entities: this.entitiesFromDevices(devices),
        online: true,
        updatedAt: new Date().toISOString(),
        source: 'http',
        rawData,
        capabilities: {
          localRead: true,
          localControl: true,
          services: ['execute_command'],
        },
      },
      online: true,
      source: 'http',
    }) as IOverkizSnapshot;
  }

  public async executeCommand(deviceUrlArg: string, commandNameArg: string, parametersArg: unknown[] = [], labelArg = 'smarthome.exchange'): Promise<Record<string, unknown>> {
    if (!deviceUrlArg) {
      throw new Error('Overkiz execute_command requires data.deviceUrl or data.device_url.');
    }
    if (!commandNameArg) {
      throw new Error('Overkiz execute_command requires data.command or data.name.');
    }
    const payload = {
      label: labelArg,
      actions: [{
        deviceURL: deviceUrlArg,
        commands: [{ name: commandNameArg, parameters: parametersArg }],
      }],
    };
    const response = await this.requestJson<Record<string, unknown>>('exec/apply', { method: 'POST', body: payload });
    return { ...response, request: payload };
  }

  private async executeLocalCommand(requestArg: IServiceCallRequest): Promise<Record<string, unknown>> {
    const data = requestArg.data || {};
    const deviceUrl = requiredString(data.deviceUrl ?? data.device_url ?? data.deviceURL, 'Overkiz command requires data.deviceUrl.');
    const command = stringValue(data.command) || stringValue(data.name) || commandForService(requestArg.service);
    if (!command) {
      throw new Error(`Overkiz service ${requestArg.service} requires data.command for native local execution.`);
    }
    const parameters = arrayValue<unknown>(data.parameters) || arrayValue<unknown>(data.args) || parametersForService(requestArg.service, data);
    const label = stringValue(data.label) || 'smarthome.exchange';
    return this.executeCommand(deviceUrl, command, parameters, label);
  }

  private entitiesFromDevices(devicesArg: IOverkizDevice[]): ISimpleLocalEntitySnapshot[] {
    const entities: ISimpleLocalEntitySnapshot[] = [];
    for (const device of devicesArg) {
      const deviceUrl = device.deviceURL || device.deviceUrl || device.device_url;
      const label = device.label || device.controllableName || device.controllable_name || deviceUrl || 'Overkiz device';
      for (const state of statesArray(device.states)) {
        const stateName = stringValue(state.name) || stringValue(state.qualifiedName) || 'state';
        const platform = platformForState(device, stateName, state.value);
        entities.push({
          id: slug(`${deviceUrl || label}_${stateName}`),
          uniqueId: deviceUrl ? `overkiz_${slug(deviceUrl)}_${slug(stateName)}` : undefined,
          name: `${label} ${stateLabel(stateName)}`,
          platform,
          state: state.value,
          available: device.available !== false && device.enabled !== false,
          writable: isWritable(platform),
          attributes: cleanAttributes({
            deviceUrl,
            stateName,
            widget: device.widget,
            uiClass: device.uiClass || device.ui_class,
            protocol: device.protocol,
          }),
        });
      }
    }
    return entities.length ? entities : [{ id: 'devices', name: 'Devices', platform: 'sensor', state: devicesArg.length, available: true }];
  }

  private async requestJson<TValue = unknown>(pathArg: string, optionsArg: IOverkizRequestOptions = {}): Promise<TValue> {
    const endpoint = this.requireEndpoint();
    const token = this.accessToken();
    if (!token) {
      throw new Error('Overkiz local API requests require config.token from Somfy Developer Mode.');
    }
    const url = new URL(`${endpoint.apiBase.replace(/\/+$/, '')}/${pathArg.replace(/^\/+/, '')}`);
    const headers: Record<string, string> = {
      accept: 'application/json',
      authorization: `Bearer ${token}`,
      'user-agent': 'SmarthomeExchange-Overkiz/1.0',
    };
    let body: string | undefined;
    if (optionsArg.body !== undefined) {
      headers['content-type'] = 'application/json';
      body = JSON.stringify(optionsArg.body);
    }

    const timeoutMs = this.timeoutMs();
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      const response = await globalThis.fetch(url, {
        method: optionsArg.method || 'GET',
        headers,
        body,
        signal: controller.signal,
      });
      const text = await response.text();
      if (response.status === 401 || response.status === 403) {
        throw new Error('Invalid Overkiz local API token.');
      }
      if (!response.ok) {
        throw new Error(`Overkiz HTTP ${optionsArg.method || 'GET'} ${url.pathname} failed with ${response.status}: ${text.slice(0, 200)}`);
      }
      return parseJson<TValue>(text);
    } finally {
      clearTimeout(timer);
    }
  }

  private shouldUseLocalApi(): boolean {
    return Boolean(this.endpoint() && this.accessToken() && !this.hasOfflineSource() && this.configArg.apiType !== 'cloud');
  }

  private hasOfflineSource(): boolean {
    return Boolean(this.configArg.client || this.configArg.snapshotProvider || this.configArg.snapshot || this.configArg.rawData !== undefined || this.configArg.rawText !== undefined || this.configArg.entities?.length);
  }

  private requireEndpoint(): IOverkizEndpoint {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('Overkiz local API requires config.host or config.url.');
    }
    return endpoint;
  }

  private endpoint(): IOverkizEndpoint | undefined {
    const value = stringValue(this.configArg.url) || stringValue(this.configArg.host);
    if (!value) {
      return undefined;
    }
    const protocol = this.configArg.useTls === false || this.configArg.protocol === 'http' ? 'http' : 'https';
    try {
      const url = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `${protocol}://${value}`);
      if (this.configArg.port) {
        url.port = String(this.configArg.port);
      }
      if (!url.port && !stringValue(this.configArg.url)) {
        url.port = String(overkizProfile.defaultPort || 8443);
      }
      const configuredPath = stringValue(this.configArg.path);
      const path = (configuredPath || url.pathname || '').replace(/\/+$/, '');
      const localApiPath = '/enduser-mobile-web/1/enduserAPI';
      const apiPath = path.endsWith(localApiPath) ? path : `${path || ''}${localApiPath}`;
      return {
        apiBase: `${url.origin}${apiPath}`,
        origin: url.origin,
        host: url.hostname,
        port: this.configArg.port || (url.port ? Number(url.port) : overkizProfile.defaultPort),
        protocol: url.protocol === 'http:' ? 'http' : 'https',
      };
    } catch {
      return undefined;
    }
  }

  private accessToken(): string | undefined {
    return stringValue(this.configArg.token) || stringValue(this.configArg.accessToken) || stringValue(this.configArg.apiKey);
  }

  private timeoutMs(): number {
    return typeof this.configArg.timeoutMs === 'number' && this.configArg.timeoutMs > 0 ? this.configArg.timeoutMs : 10000;
  }

  private offlineOverkizSnapshot(errorArg: string): IOverkizSnapshot {
    const endpoint = this.endpoint();
    return OverkizMapper.toSnapshot({
      config: this.configArg,
      rawData: {
        device: {
          id: this.configArg.uniqueId || endpoint?.host || overkizProfile.domain,
          name: this.configArg.name || overkizDefaultName,
          manufacturer: 'Somfy',
          model: 'Somfy TaHoma local API',
          host: endpoint?.host,
          port: endpoint?.port,
          protocol: endpoint?.protocol,
          configurationUrl: endpoint?.origin,
        },
        entities: [{ id: 'online', name: 'Online', platform: 'binary_sensor', state: false, available: true, attributes: { error: errorArg } }],
        online: false,
        updatedAt: new Date().toISOString(),
        source: 'runtime',
        error: errorArg,
      },
      online: false,
      source: 'runtime',
      error: errorArg,
    }) as IOverkizSnapshot;
  }
}

const commandForService = (serviceArg: string): string | undefined => {
  const commands: Record<string, string> = {
    turn_on: 'on',
    turn_off: 'off',
    toggle: 'toggle',
    open_cover: 'open',
    close_cover: 'close',
    stop_cover: 'stop',
    press: 'identify',
  };
  return commands[serviceArg];
};

const parametersForService = (serviceArg: string, dataArg: Record<string, unknown>): unknown[] => {
  if (serviceArg === 'set_value') {
    return dataArg.value !== undefined ? [dataArg.value] : [];
  }
  if (serviceArg === 'select_option') {
    return dataArg.option !== undefined ? [dataArg.option] : [];
  }
  if (serviceArg === 'set_cover_position_and_tilt') {
    const position = dataArg.position ?? dataArg.current_position;
    const tilt = dataArg.tilt ?? dataArg.current_tilt_position;
    return [position, tilt].filter((valueArg) => valueArg !== undefined);
  }
  return [];
};

const platformForState = (deviceArg: IOverkizDevice, stateNameArg: string, valueArg: unknown): TEntityPlatform => {
  const widget = String(deviceArg.widget || deviceArg.uiClass || deviceArg.ui_class || '').toLowerCase();
  const state = stateNameArg.toLowerCase();
  if (state.includes('temperature') || state.includes('rssi') || state.includes('battery') || state.includes('luminance') || state.includes('humidity') || typeof valueArg === 'number') {
    return 'sensor';
  }
  if (state.includes('onoff') || state.includes('on_off') || widget.includes('onoff') || widget.includes('light')) {
    return widget.includes('light') ? 'light' : 'switch';
  }
  if (state.includes('closure') || widget.includes('shutter') || widget.includes('awning') || widget.includes('screen') || widget.includes('cover')) {
    return 'cover';
  }
  if (typeof valueArg === 'boolean') {
    return 'binary_sensor';
  }
  return 'sensor';
};

const isWritable = (platformArg: TEntityPlatform): boolean => ['button', 'climate', 'cover', 'fan', 'light', 'media_player', 'number', 'select', 'switch', 'text'].includes(platformArg);

const statesArray = (statesArg: IOverkizDevice['states']): Array<Record<string, unknown>> => {
  if (Array.isArray(statesArg)) {
    return statesArg;
  }
  if (statesArg && typeof statesArg === 'object') {
    return Object.entries(statesArg).map(([name, value]) => ({ name, ...(recordValue(value) || { value }) }));
  }
  return [];
};

const gatewayType = (gatewayArg: IOverkizGateway | undefined): string | undefined => {
  const type = gatewayArg?.type;
  if (typeof type === 'string') {
    return type;
  }
  return stringValue(type?.name) || stringValue(type?.value) || stringValue(type?.label);
};

const gatewayModel = (gatewayArg: IOverkizGateway | undefined): string | undefined => {
  const type = gatewayType(gatewayArg);
  return type ? titleCase(type) : undefined;
};

const gatewayName = (gatewayArg: IOverkizGateway | undefined): string | undefined => {
  const model = gatewayModel(gatewayArg);
  return model ? `Overkiz ${model}` : undefined;
};

const stateLabel = (stateNameArg: string): string => titleCase(stateNameArg.split(':').pop() || stateNameArg);

const titleCase = (valueArg: string): string => valueArg.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/[_:-]+/g, ' ').replace(/\s+/g, ' ').trim().replace(/\b\w/g, (charArg) => charArg.toUpperCase());

const parseJson = <TValue>(textArg: string): TValue => textArg.trim() ? JSON.parse(textArg) as TValue : undefined as TValue;

const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;

const cleanAttributes = (attributesArg: Record<string, unknown> | undefined): Record<string, unknown> | undefined => {
  if (!attributesArg) {
    return undefined;
  }
  const result = Object.fromEntries(Object.entries(attributesArg).filter(([, valueArg]) => valueArg !== undefined));
  return Object.keys(result).length ? result : undefined;
};

const arrayValue = <TValue = unknown>(valueArg: unknown): TValue[] => Array.isArray(valueArg) ? valueArg as TValue[] : [];

const recordValue = (valueArg: unknown): Record<string, unknown> | undefined => valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Record<string, unknown> : undefined;

const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;

const requiredString = (valueArg: unknown, messageArg: string): string => {
  const value = stringValue(valueArg);
  if (!value) {
    throw new Error(messageArg);
  }
  return value;
};

const slug = (valueArg: unknown): string => String(valueArg || '').toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'device';

const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
