import type * as shxInterfaces from '@smarthome.exchange/interfaces';
import { SimpleLocalMapper, type IIntegrationEntity, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot, type ISimpleLocalSnapshotOptions, type TSimpleLocalRawData } from '../../core/index.js';
import type { IOctoprintConfig, IOctoprintRawData } from './octoprint.types.js';
import { octoprintDefaultPort, octoprintProfile } from './octoprint.types.js';

export class OctoprintMapper {
  public static toSnapshot(optionsArg: Omit<ISimpleLocalSnapshotOptions<IOctoprintConfig>, 'profile'>): ISimpleLocalSnapshot {
    const rawData = optionsArg.rawData ?? optionsArg.config.rawData;
    if (this.isNativeRawData(rawData)) {
      return this.toNativeSnapshot(optionsArg.config, rawData, optionsArg.online ?? true, optionsArg.source || 'manual', optionsArg.error);
    }
    return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: octoprintProfile });
  }

  public static toSnapshotFromRaw(configArg: IOctoprintConfig, rawDataArg: TSimpleLocalRawData): ISimpleLocalSnapshot {
    return this.toSnapshot({ config: configArg, rawData: rawDataArg, online: true, source: 'manual' });
  }

  public static toDevices(snapshotArg: ISimpleLocalSnapshot): shxInterfaces.data.IDeviceDefinition[] {
    return SimpleLocalMapper.toDevices(octoprintProfile, snapshotArg);
  }

  public static toEntities(snapshotArg: ISimpleLocalSnapshot): IIntegrationEntity[] {
    return SimpleLocalMapper.toEntities(octoprintProfile, snapshotArg);
  }

  public static slug(valueArg: unknown): string {
    return SimpleLocalMapper.slug(valueArg);
  }

  public static actionForService(requestArg: { service: string; target?: { entityId?: string }; data?: Record<string, unknown> }): string | undefined {
    const explicit = this.stringValue(requestArg.data?.action) || this.stringValue(requestArg.data?.button);
    if (explicit) {
      return explicit;
    }
    const entityId = requestArg.target?.entityId || '';
    if (/pause/i.test(entityId)) {
      return 'pause_job';
    }
    if (/resume/i.test(entityId)) {
      return 'resume_job';
    }
    if (/stop|cancel/i.test(entityId)) {
      return 'stop_job';
    }
    if (/shutdown/i.test(entityId)) {
      return 'shutdown_system';
    }
    if (/reboot/i.test(entityId)) {
      return 'reboot_system';
    }
    if (/restart/i.test(entityId)) {
      return 'restart_octoprint';
    }
    if (requestArg.service === 'pause') {
      return 'pause_job';
    }
    if (requestArg.service === 'resume') {
      return 'resume_job';
    }
    if (requestArg.service === 'stop' || requestArg.service === 'cancel') {
      return 'stop_job';
    }
    return undefined;
  }

  private static toNativeSnapshot(configArg: IOctoprintConfig, rawDataArg: IOctoprintRawData, onlineArg: boolean, sourceArg: ISimpleLocalSnapshot['source'], errorArg?: string): ISimpleLocalSnapshot {
    const endpoint = this.endpointInfo(configArg);
    const job = this.recordValue(rawDataArg.job) || {};
    const printer = this.recordValue(rawDataArg.printer) || undefined;
    const settings = this.recordValue(rawDataArg.settings) || {};
    const discovery = this.recordValue(this.recordValue(settings.plugins)?.discovery) || {};
    const webcam = this.recordValue(settings.webcam) || {};
    const stateText = this.stringValue(this.recordValue(printer?.state)?.text) || this.stringValue(job.state);
    const flags = this.recordValue(this.recordValue(printer?.state)?.flags) || {};
    const progress = this.recordValue(job.progress) || {};
    const jobInfo = this.recordValue(job.job) || {};
    const fileInfo = this.recordValue(jobInfo.file) || {};
    const temperatures = this.recordValue(printer?.temperature) || {};
    const entities: ISimpleLocalEntitySnapshot[] = [];

    this.pushEntity(entities, 'current_state', 'Current State', this.apiStateValue(stateText), { deviceClass: 'enum' });
    this.pushEntity(entities, 'job_percentage', 'Job Percentage', this.round(this.numberValue(progress.completion) ?? 0, 2), { unit: '%', deviceClass: 'battery' });
    this.pushEntity(entities, 'time_remaining', 'Time Remaining', this.numberValue(progress.printTimeLeft), { unit: 's', deviceClass: 'duration' });
    this.pushEntity(entities, 'time_elapsed', 'Time Elapsed', this.numberValue(progress.printTime), { unit: 's', deviceClass: 'duration' });
    this.pushEntity(entities, 'current_file', 'Current File', this.stringValue(fileInfo.name));
    this.pushEntity(entities, 'current_file_size', 'Current File Size', this.numberValue(fileInfo.size), { unit: 'B', deviceClass: 'data_size' });

    entities.push({ id: 'printing', name: 'Printing', platform: 'binary_sensor', state: this.booleanValue(flags.printing) ?? false, available: Boolean(printer), writable: false, attributes: { source: sourceArg } });
    entities.push({ id: 'printing_error', name: 'Printing Error', platform: 'binary_sensor', state: this.booleanValue(flags.error) ?? false, available: Boolean(printer), writable: false, attributes: { source: sourceArg } });

    for (const [tool, rawTemperature] of Object.entries(temperatures)) {
      const temperature = this.recordValue(rawTemperature) || {};
      const slug = this.slug(tool);
      this.pushEntity(entities, `${slug}_actual_temperature`, `${tool} Actual Temperature`, this.numberValue(temperature.actual), { unit: '°C', deviceClass: 'temperature' });
      this.pushEntity(entities, `${slug}_target_temperature`, `${tool} Target Temperature`, this.numberValue(temperature.target), { platform: 'number', unit: '°C', deviceClass: 'temperature', writable: true, attributes: { tool } });
    }

    for (const action of ['resume_job', 'pause_job', 'stop_job', 'shutdown_system', 'reboot_system', 'restart_octoprint']) {
      entities.push({
        id: action,
        name: action.split('_').map((partArg) => partArg[0].toUpperCase() + partArg.slice(1)).join(' '),
        platform: 'button',
        state: false,
        writable: true,
        available: action.includes('system') || action.includes('octoprint') ? onlineArg : Boolean(printer),
        attributes: { action, source: sourceArg },
      });
    }

    return {
      device: {
        id: configArg.uniqueId || configArg.deviceId || this.stringValue(discovery.upnpUuid) || (endpoint.host ? `${endpoint.host}:${endpoint.port || octoprintDefaultPort}` : undefined) || configArg.name || octoprintProfile.domain,
        name: configArg.name || octoprintProfile.defaultName || octoprintProfile.displayName,
        manufacturer: octoprintProfile.manufacturer,
        model: octoprintProfile.model,
        host: endpoint.host,
        port: endpoint.port,
        protocol: endpoint.protocol,
        configurationUrl: endpoint.configurationUrl,
        attributes: this.cleanAttributes({
          state: stateText,
          printerOffline: rawDataArg.printerOffline,
          apiKeyConfigured: Boolean(configArg.apiKey || configArg.token),
          webcamEnabled: this.booleanValue(webcam.webcamEnabled),
          webcamStreamUrl: this.stringValue(webcam.streamUrl),
          fetchedAt: rawDataArg.fetchedAt,
          ...configArg.metadata,
        }),
      },
      entities,
      online: onlineArg,
      updatedAt: new Date().toISOString(),
      source: sourceArg,
      rawData: this.clone(rawDataArg),
      error: errorArg,
      capabilities: {
        localRead: true,
        localControl: true,
        protocol: 'octoprint-rest',
      },
    };
  }

  private static pushEntity(entitiesArg: ISimpleLocalEntitySnapshot[], idArg: string, nameArg: string, valueArg: unknown, optionsArg: { platform?: ISimpleLocalEntitySnapshot['platform']; unit?: string; deviceClass?: string; writable?: boolean; attributes?: Record<string, unknown> } = {}): void {
    if (valueArg === undefined) {
      return;
    }
    entitiesArg.push({
      id: idArg,
      name: nameArg,
      platform: optionsArg.platform || 'sensor',
      state: valueArg,
      unit: optionsArg.unit,
      deviceClass: optionsArg.deviceClass,
      available: true,
      writable: optionsArg.writable ?? optionsArg.platform === 'number',
      attributes: this.cleanAttributes(optionsArg.attributes || {}),
    });
  }

  private static isNativeRawData(valueArg: unknown): valueArg is IOctoprintRawData {
    if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
      return false;
    }
    const value = valueArg as Record<string, unknown>;
    if ('device' in value && 'entities' in value) {
      return false;
    }
    return 'job' in value || 'printer' in value || 'settings' in value || 'printerOffline' in value;
  }

  private static endpointInfo(configArg: IOctoprintConfig): { host?: string; port?: number; protocol: 'http' | 'https'; configurationUrl?: string } {
    const parsed = this.parseUrl(configArg.url);
    const useTls = this.booleanValue(configArg.useTls) || this.booleanValue(configArg.ssl) || parsed?.protocol === 'https:';
    const protocol = useTls ? 'https' : 'http';
    const host = configArg.host || parsed?.hostname;
    const port = configArg.port || (parsed?.port ? Number(parsed.port) : undefined) || (host ? octoprintDefaultPort : undefined);
    const path = configArg.path || parsed?.pathname || '/';
    const portPart = port && !((protocol === 'https' && port === 443) || (protocol === 'http' && port === 80)) ? `:${port}` : '';
    return { host, port, protocol, configurationUrl: host ? `${protocol}://${host}${portPart}${path}` : undefined };
  }

  private static parseUrl(valueArg: string | undefined): URL | undefined {
    if (!valueArg) {
      return undefined;
    }
    try {
      return new URL(valueArg);
    } catch {
      return undefined;
    }
  }

  private static apiStateValue(valueArg: string | undefined): string | undefined {
    const map: Record<string, string> = {
      'Opening serial connection': 'open_serial',
      'Detecting serial connection': 'detect_serial',
      Connecting: 'connecting',
      Operational: 'operational',
      'Starting print from SD': 'starting_sd',
      'Starting to send file to SD': 'starting_streaming',
      Starting: 'starting',
      'Printing from SD': 'printing_sd',
      'Sending file to SD': 'printing_streaming',
      Printing: 'printing',
      Cancelling: 'cancelling',
      Pausing: 'pausing',
      Paused: 'paused',
      Resuming: 'resuming',
      Finishing: 'finishing',
      Offline: 'offline',
      Error: 'error',
      'Offline after error': 'offline_after_error',
      'Transferring file to SD': 'transferring_file',
    };
    return valueArg ? map[valueArg] || valueArg.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') : undefined;
  }

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

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

  private static 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;
  }

  private static booleanValue(valueArg: unknown): boolean | undefined {
    if (typeof valueArg === 'boolean') {
      return valueArg;
    }
    if (typeof valueArg === 'number') {
      return valueArg !== 0;
    }
    if (typeof valueArg === 'string') {
      const normalized = valueArg.trim().toLowerCase();
      if (['true', 'yes', 'on', '1'].includes(normalized)) {
        return true;
      }
      if (['false', 'no', 'off', '0'].includes(normalized)) {
        return false;
      }
    }
    return undefined;
  }

  private static round(valueArg: number, decimalsArg: number): number {
    const factor = 10 ** decimalsArg;
    return Math.round(valueArg * factor) / factor;
  }

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

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