import * as plugins from '../../plugins.js';
import type { IIntegrationEntity } from '../../core/types.js';
import {
  brotherCounterValues,
  brotherDefaultPort,
  brotherDomain,
  brotherInkMaintenanceValues,
  brotherLaserMaintenanceValues,
  brotherLaserNextcareValues,
  brotherOids,
  brotherPercentSensorKeys,
  brotherSensorDefinitions,
} from './brother.constants.js';
import type { IBrotherConfig, IBrotherPrinterInfo, IBrotherRawData, IBrotherSensorDefinition, IBrotherSensors, IBrotherSnapshot, TBrotherPrinterType, TBrotherSnapshotSource } from './brother.types.js';

interface IBrotherSnapshotOptions {
  config: IBrotherConfig;
  rawData?: IBrotherRawData;
  sensors?: IBrotherSensors;
  printer?: Partial<IBrotherPrinterInfo>;
  online?: boolean;
  source?: TBrotherSnapshotSource;
  error?: string;
}

export class BrotherMapper {
  public static toSnapshot(optionsArg: IBrotherSnapshotOptions): IBrotherSnapshot {
    if (optionsArg.config.snapshot && !optionsArg.rawData && !optionsArg.sensors && !optionsArg.printer) {
      return this.normalizeSnapshot(this.clone(optionsArg.config.snapshot), optionsArg.config, optionsArg.source || 'snapshot');
    }

    const rawData = this.rawData(optionsArg.config, optionsArg.rawData);
    const rawSensors = this.sensorsFromRawData(rawData, this.printerType(optionsArg.config));
    const sensors = this.cleanAttributes({
      ...rawSensors,
      ...optionsArg.config.sensors,
      ...optionsArg.sensors,
    }) as IBrotherSensors;
    const hasData = this.hasRawData(rawData) || this.hasSensorData(sensors);
    const online = optionsArg.online ?? optionsArg.config.online ?? hasData;
    const source = optionsArg.source || (hasData ? 'manual' : 'runtime');
    const updatedAt = new Date().toISOString();
    const snapshot: IBrotherSnapshot = {
      printer: this.printerInfo(optionsArg.config, rawData, optionsArg.printer),
      sensors,
      rawData: this.hasRawData(rawData) ? this.clone(rawData) : undefined,
      online,
      updatedAt,
      source,
      error: optionsArg.error,
    };
    return this.normalizeSnapshot(snapshot, optionsArg.config, source);
  }

  public static toDevices(snapshotArg: IBrotherSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const updatedAt = snapshotArg.updatedAt || new Date().toISOString();
    const features: plugins.shxInterfaces.data.IDeviceFeature[] = [
      { id: 'connection', capability: 'sensor', name: 'Connection', readable: true, writable: false },
    ];
    const state: plugins.shxInterfaces.data.IDeviceState[] = [
      { featureId: 'connection', value: snapshotArg.online ? 'online' : 'offline', updatedAt },
    ];

    for (const definition of brotherSensorDefinitions) {
      const value = this.sensorValue(snapshotArg.sensors, definition.key);
      if (value === undefined || value === null) {
        continue;
      }
      features.push({ id: definition.key, capability: 'sensor', name: definition.name, readable: true, writable: false, unit: definition.unit });
      state.push({ featureId: definition.key, value: this.deviceStateValue(value), updatedAt });
    }

    return [{
      id: this.printerDeviceId(snapshotArg),
      integrationDomain: brotherDomain,
      name: this.printerName(snapshotArg),
      protocol: 'unknown',
      manufacturer: snapshotArg.printer.manufacturer || 'Brother',
      model: snapshotArg.printer.model || 'Brother Printer',
      online: snapshotArg.online,
      features,
      state,
      metadata: this.cleanAttributes({
        serialNumber: snapshotArg.printer.serialNumber || snapshotArg.printer.serial,
        macAddress: snapshotArg.printer.macAddress,
        firmware: snapshotArg.printer.firmware,
        host: snapshotArg.printer.host,
        port: snapshotArg.printer.port,
        printerType: snapshotArg.printer.printerType,
        configurationUrl: snapshotArg.printer.configurationUrl,
        source: snapshotArg.source,
        error: snapshotArg.error,
      }),
    }];
  }

  public static toEntities(snapshotArg: IBrotherSnapshot): IIntegrationEntity[] {
    const deviceId = this.printerDeviceId(snapshotArg);
    const uniqueBase = this.uniqueBase(snapshotArg);
    const baseName = this.printerName(snapshotArg);
    const entities: IIntegrationEntity[] = [];

    for (const definition of brotherSensorDefinitions) {
      const value = this.sensorValue(snapshotArg.sensors, definition.key);
      if (value === undefined || value === null) {
        continue;
      }
      entities.push(this.entity(snapshotArg, definition, `${baseName} ${definition.name}`, value, deviceId, uniqueBase));
    }

    if (!entities.length) {
      entities.push(this.entity(snapshotArg, brotherSensorDefinitions[0], `${baseName} Status`, 'unknown', deviceId, uniqueBase, false));
    }

    return entities;
  }

  public static printerDeviceId(snapshotArg: IBrotherSnapshot): string {
    return `${brotherDomain}.printer.${this.uniqueBase(snapshotArg)}`;
  }

  public static snapshotId(snapshotArg: IBrotherSnapshot): string | undefined {
    return snapshotArg.printer.id || snapshotArg.printer.serialNumber || snapshotArg.printer.serial || snapshotArg.printer.macAddress || snapshotArg.printer.host;
  }

  public static slug(valueArg: string): string {
    return valueArg.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || brotherDomain;
  }

  private static normalizeSnapshot(snapshotArg: IBrotherSnapshot, configArg: IBrotherConfig, sourceArg: TBrotherSnapshotSource): IBrotherSnapshot {
    const rawData = this.rawData(configArg, snapshotArg.rawData);
    const derivedPrinter = this.hasRawData(rawData) ? this.printerInfo(configArg, rawData) : undefined;
    const derivedSensors = this.hasRawData(rawData) ? this.sensorsFromRawData(rawData, this.printerType(configArg, snapshotArg.printer.printerType)) : undefined;
    const printer = {
      ...derivedPrinter,
      ...snapshotArg.printer,
    };
    const parsed = this.parseEndpoint(configArg.host || printer.host);
    printer.host = printer.host || parsed?.host || configArg.host;
    printer.port = printer.port || parsed?.port || (printer.host ? configArg.port || brotherDefaultPort : configArg.port);
    printer.printerType = this.printerType(configArg, printer.printerType);
    printer.manufacturer = printer.manufacturer || configArg.manufacturer || 'Brother';
    printer.model = printer.model || configArg.model || 'Brother Printer';
    printer.name = printer.name || configArg.name || this.defaultPrinterName(printer);
    printer.id = printer.id || configArg.uniqueId || printer.serialNumber || printer.serial || printer.macAddress || (printer.host ? `${printer.host}:${printer.port || brotherDefaultPort}` : undefined) || printer.name;
    printer.configurationUrl = printer.configurationUrl || (printer.host ? `http://${this.hostForUrl(printer.host)}/` : undefined);

    return {
      ...snapshotArg,
      printer,
      sensors: this.cleanAttributes({ ...derivedSensors, ...snapshotArg.sensors }) as IBrotherSensors,
      rawData: this.hasRawData(rawData) ? this.clone(rawData) : snapshotArg.rawData,
      online: snapshotArg.online,
      updatedAt: snapshotArg.updatedAt || new Date().toISOString(),
      source: snapshotArg.source || sourceArg,
    };
  }

  private static printerInfo(configArg: IBrotherConfig, rawDataArg: IBrotherRawData, printerArg: Partial<IBrotherPrinterInfo> = {}): IBrotherPrinterInfo {
    const parsed = this.parseEndpoint(configArg.host);
    const host = printerArg.host || parsed?.host || configArg.host;
    const port = printerArg.port || configArg.port || parsed?.port || (host ? brotherDefaultPort : undefined);
    const modelSource = this.stringValue(this.rawValue(rawDataArg, 'model'));
    const model = printerArg.model || configArg.model || this.deviceIdField(modelSource, 'MDL') || this.stringValue(modelSource);
    const manufacturer = printerArg.manufacturer || configArg.manufacturer || this.deviceIdField(modelSource, 'MFG') || 'Brother';
    const serial = printerArg.serialNumber || printerArg.serial || configArg.serialNumber || configArg.serial || this.stringValue(this.rawValue(rawDataArg, 'serial'));
    const macAddress = printerArg.macAddress || configArg.macAddress || this.macValue(this.rawValue(rawDataArg, 'mac'));
    const firmware = printerArg.firmware || configArg.firmware || this.stringValue(this.rawValue(rawDataArg, 'firmware'));
    const printerType = this.printerType(configArg, printerArg.printerType);
    const name = printerArg.name || configArg.name || this.defaultPrinterName({ model, serialNumber: serial, host });
    return this.cleanAttributes({
      ...printerArg,
      id: printerArg.id || configArg.uniqueId || serial || macAddress || (host ? `${host}:${port || brotherDefaultPort}` : undefined) || name,
      name,
      manufacturer,
      model,
      serial,
      serialNumber: serial,
      macAddress,
      firmware,
      host,
      port,
      printerType,
      configurationUrl: printerArg.configurationUrl || (host ? `http://${this.hostForUrl(host)}/` : undefined),
    }) as IBrotherPrinterInfo;
  }

  private static sensorsFromRawData(rawDataArg: IBrotherRawData, printerTypeArg: TBrotherPrinterType): IBrotherSensors {
    const sensors: IBrotherSensors = {};
    const status = this.stringValue(this.rawValue(rawDataArg, 'status'));
    if (status) {
      sensors.status = this.cleanseStatus(status.toLowerCase());
    }
    const uptime = this.numberValue(this.rawValue(rawDataArg, 'uptime'));
    if (uptime !== undefined) {
      sensors.uptime = new Date(Date.now() - (uptime / 100) * 1000).toISOString();
    }

    this.applyMappedWords(sensors, this.hexWords(this.rawValue(rawDataArg, 'counters'), 14), brotherCounterValues, false);
    const maintenanceWords = this.hexWords(this.rawValue(rawDataArg, 'maintenance'), 14);
    const legacyMaintenanceWords = this.hexWords(this.rawValue(rawDataArg, 'maintenance'), 10);
    if (this.isLegacyMaintenance(legacyMaintenanceWords)) {
      this.applyLegacyMappedWords(sensors, legacyMaintenanceWords, printerTypeArg === 'ink' ? brotherInkMaintenanceValues : brotherLaserMaintenanceValues);
    } else {
      this.applyMappedWords(sensors, maintenanceWords, printerTypeArg === 'ink' ? brotherInkMaintenanceValues : brotherLaserMaintenanceValues, true);
    }
    if (printerTypeArg === 'laser') {
      this.applyMappedWords(sensors, this.hexWords(this.rawValue(rawDataArg, 'nextcare'), 14), brotherLaserNextcareValues, false);
    }

    const legacyPageCounter = this.numberValue(this.rawValue(rawDataArg, 'pageCounter'));
    if (sensors.page_counter === undefined && legacyPageCounter !== undefined) {
      sensors.page_counter = legacyPageCounter;
    }
    return this.cleanAttributes(sensors) as IBrotherSensors;
  }

  private static applyMappedWords(sensorsArg: IBrotherSensors, wordsArg: string[], valuesMapArg: Record<string, string>, percentAwareArg: boolean): void {
    for (const word of wordsArg) {
      const key = valuesMapArg[word.slice(0, 2).toLowerCase()];
      if (!key || word.length < 10) {
        continue;
      }
      const value = Number.parseInt(word.slice(-8), 16);
      if (!Number.isFinite(value)) {
        continue;
      }
      sensorsArg[key] = percentAwareArg && brotherPercentSensorKeys.has(key) ? Math.round(value / 100) : value;
    }
  }

  private static applyLegacyMappedWords(sensorsArg: IBrotherSensors, wordsArg: string[], valuesMapArg: Record<string, string>): void {
    for (const word of wordsArg) {
      const key = valuesMapArg[word.slice(0, 2).toLowerCase()];
      if (!key || word.length < 10) {
        continue;
      }
      const numerator = Number.parseInt(word.slice(6, 8), 16);
      const denominator = Number.parseInt(word.slice(8, 10), 16);
      if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) {
        continue;
      }
      sensorsArg[key] = Math.round((numerator / denominator) * 100);
    }
  }

  private static isLegacyMaintenance(wordsArg: string[]): boolean {
    return Boolean(wordsArg.length && wordsArg.every((wordArg) => wordArg.length === 10 && wordArg.endsWith('14')));
  }

  private static hexWords(valueArg: unknown, chunkSizeArg: number): string[] {
    if (valueArg instanceof Uint8Array) {
      return this.hexStringToWords(this.bytesToHex(valueArg, true), chunkSizeArg);
    }
    if (Array.isArray(valueArg)) {
      if (valueArg.every((itemArg) => typeof itemArg === 'number')) {
        return this.hexStringToWords(this.bytesToHex(new Uint8Array(valueArg as number[]), true), chunkSizeArg);
      }
      return valueArg.map((itemArg) => this.cleanHex(String(itemArg))).filter((itemArg) => itemArg.length >= chunkSizeArg);
    }
    if (typeof valueArg === 'string') {
      return this.hexStringToWords(this.cleanHex(valueArg), chunkSizeArg);
    }
    return [];
  }

  private static hexStringToWords(valueArg: string, chunkSizeArg: number): string[] {
    const result: string[] = [];
    for (let index = 0; index + chunkSizeArg <= valueArg.length; index += chunkSizeArg) {
      result.push(valueArg.slice(index, index + chunkSizeArg));
    }
    return result;
  }

  private static bytesToHex(valueArg: Uint8Array, dropChecksumArg: boolean): string {
    const bytes = dropChecksumArg && valueArg.length > 1 ? valueArg.slice(0, -1) : valueArg;
    return [...bytes].map((byteArg) => byteArg.toString(16).padStart(2, '0')).join('');
  }

  private static cleanHex(valueArg: string): string {
    return valueArg.toLowerCase().replace(/[^0-9a-f]/g, '');
  }

  private static rawData(configArg: IBrotherConfig, rawDataArg?: IBrotherRawData): IBrotherRawData {
    return this.cleanAttributes({ ...configArg.rawData, ...rawDataArg });
  }

  private static rawValue(rawDataArg: IBrotherRawData, keyArg: keyof typeof brotherOids): unknown {
    const oid = brotherOids[keyArg];
    return rawDataArg[oid] ?? rawDataArg[keyArg] ?? rawDataArg[this.snakeKey(keyArg)];
  }

  private static snakeKey(keyArg: string): string {
    return keyArg.replace(/[A-Z]/g, (matchArg) => `_${matchArg.toLowerCase()}`);
  }

  private static entity(snapshotArg: IBrotherSnapshot, definitionArg: IBrotherSensorDefinition, nameArg: string, valueArg: unknown, deviceIdArg: string, uniqueBaseArg: string, availableArg = snapshotArg.online): IIntegrationEntity {
    return {
      id: `sensor.${this.slug(nameArg)}`,
      uniqueId: `${brotherDomain}_${uniqueBaseArg}_${this.slug(definitionArg.key)}`,
      integrationDomain: brotherDomain,
      deviceId: deviceIdArg,
      platform: 'sensor',
      name: nameArg,
      state: this.stateValue(valueArg),
      attributes: this.cleanAttributes({
        key: definitionArg.key,
        unitOfMeasurement: definitionArg.unit,
        deviceClass: definitionArg.deviceClass,
        stateClass: definitionArg.stateClass,
        entityCategory: definitionArg.entityCategory,
        entityRegistryEnabledDefault: definitionArg.enabledDefault,
        source: snapshotArg.source,
        error: definitionArg.key === 'status' ? snapshotArg.error : undefined,
      }),
      available: availableArg && valueArg !== undefined && valueArg !== null,
    };
  }

  private static stateValue(valueArg: unknown): unknown {
    return valueArg instanceof Date ? valueArg.toISOString() : valueArg;
  }

  private static deviceStateValue(valueArg: unknown): plugins.shxInterfaces.data.TDeviceStateValue {
    const value = this.stateValue(valueArg);
    if (value === undefined || value === null) {
      return null;
    }
    if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
      return value;
    }
    if (typeof value === 'object' && !Array.isArray(value)) {
      return value as Record<string, unknown>;
    }
    return String(value);
  }

  private static sensorValue(sensorsArg: IBrotherSensors, keyArg: string): unknown {
    return sensorsArg[keyArg];
  }

  private static printerName(snapshotArg: IBrotherSnapshot): string {
    return snapshotArg.printer.name || this.defaultPrinterName(snapshotArg.printer) || 'Brother Printer';
  }

  private static defaultPrinterName(printerArg: Partial<IBrotherPrinterInfo>): string {
    const model = this.stringValue(printerArg.model);
    const serial = this.stringValue(printerArg.serialNumber || printerArg.serial);
    if (model && serial && model !== 'Brother Printer') {
      return `${model} ${serial}`;
    }
    return model || this.stringValue(printerArg.host) || 'Brother Printer';
  }

  private static uniqueBase(snapshotArg: IBrotherSnapshot): string {
    return this.slug(snapshotArg.printer.serialNumber || snapshotArg.printer.serial || snapshotArg.printer.macAddress || snapshotArg.printer.id || snapshotArg.printer.host || this.printerName(snapshotArg));
  }

  private static printerType(configArg: IBrotherConfig, fallbackArg?: TBrotherPrinterType): TBrotherPrinterType {
    const value = configArg.printerType || configArg.type || fallbackArg;
    return value === 'ink' ? 'ink' : 'laser';
  }

  private static parseEndpoint(valueArg: string | undefined): { host: string; port?: number } | undefined {
    if (!valueArg || !/^[a-z][a-z0-9+.-]*:\/\//i.test(valueArg)) {
      return undefined;
    }
    try {
      const url = new URL(valueArg);
      return { host: url.hostname, port: url.port ? Number(url.port) : undefined };
    } catch {
      return undefined;
    }
  }

  private static hostForUrl(hostArg: string): string {
    return hostArg.includes(':') && !hostArg.startsWith('[') ? `[${hostArg}]` : hostArg;
  }

  private static deviceIdField(valueArg: string | undefined, keyArg: string): string | undefined {
    if (!valueArg) {
      return undefined;
    }
    const pattern = new RegExp(`${keyArg}:([^;]+)`, 'i');
    return valueArg.match(pattern)?.[1]?.trim();
  }

  private static macValue(valueArg: unknown): string | undefined {
    if (typeof valueArg === 'string') {
      return valueArg.trim() || undefined;
    }
    if (valueArg instanceof Uint8Array) {
      return [...valueArg].map((itemArg) => itemArg.toString(16).padStart(2, '0')).join(':');
    }
    if (Array.isArray(valueArg) && valueArg.every((itemArg) => typeof itemArg === 'number')) {
      return (valueArg as number[]).map((itemArg) => itemArg.toString(16).padStart(2, '0')).join(':');
    }
    return undefined;
  }

  private static cleanseStatus(valueArg: string): string {
    return valueArg.split(/\s+/g).join(' ').trim();
  }

  private static stringValue(valueArg: unknown): string | undefined {
    if (typeof valueArg === 'string') {
      return valueArg.trim() || undefined;
    }
    if (valueArg instanceof Date) {
      return valueArg.toISOString();
    }
    if (typeof valueArg === 'number' || typeof valueArg === 'boolean') {
      return String(valueArg);
    }
    return undefined;
  }

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

  private static hasRawData(rawDataArg: IBrotherRawData | undefined): boolean {
    return Boolean(rawDataArg && Object.keys(rawDataArg).length);
  }

  private static hasSensorData(sensorsArg: IBrotherSensors | undefined): boolean {
    return Boolean(sensorsArg && Object.values(sensorsArg).some((valueArg) => valueArg !== undefined && valueArg !== null));
  }

  private static cleanAttributes<TValue extends Record<string, unknown>>(attributesArg: TValue): Partial<TValue> {
    return Object.fromEntries(Object.entries(attributesArg).filter(([, valueArg]) => valueArg !== undefined)) as Partial<TValue>;
  }

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