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 { IPencomConfig, IPencomRawApiData, IPencomRelayConfig, IPencomRelayState, TPencomRawData } from './pencom.types.js';
import { pencomDefaultName, pencomProfile, pencomRelaysPerBoard } from './pencom.types.js';

export class PencomMapper {
  public static toSnapshot(optionsArg: Omit<ISimpleLocalSnapshotOptions<IPencomConfig>, 'profile'>): ISimpleLocalSnapshot {
    return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: pencomProfile, rawData: this.normalizeRawData(optionsArg.rawData, optionsArg.config) });
  }

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

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

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

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

  public static normalizeRawData(rawDataArg: unknown, configArg: IPencomConfig): unknown {
    const parsedRawData = parseMaybeJson(rawDataArg);
    const rawObject = objectValue(parsedRawData);
    if (!rawObject || Array.isArray(rawObject.entities) || rawObject.device) {
      return parsedRawData;
    }

    const states = statesValue(rawObject.states);
    const relays = relayStatesValue(rawObject.relays);
    if (!states && !relays) {
      return parsedRawData;
    }

    const host = configArg.host || hostFromUrl(configArg.url);
    const port = configArg.port;
    const relayStates = relays?.length ? relays : relayStatesFromConfig(configArg, states || []);
    const entities = entitiesFromRelays(relayStates);

    return {
      device: {
        id: configArg.uniqueId || configArg.deviceId || (host && port ? `${host}:${port}` : undefined) || pencomDefaultName,
        name: configArg.name || pencomDefaultName,
        manufacturer: 'Pencom',
        model: 'Pencom relay controller',
        host,
        port,
        protocol: 'tcp',
      },
      entities,
      boards: numberValue(rawObject.boards) || configArg.boards || states?.length,
      relays: relayStates,
      states,
      fetchedAt: stringValue(rawObject.fetchedAt) || new Date().toISOString(),
    } satisfies IPencomRawApiData & { device: Record<string, unknown>; entities: ISimpleLocalEntitySnapshot[] };
  }
}

const entitiesFromRelays = (relaysArg: IPencomRelayState[]): ISimpleLocalEntitySnapshot[] => {
  return relaysArg.map((relayArg) => {
    const board = numberValue(relayArg.board) || 0;
    const addr = numberValue(relayArg.addr) || 0;
    return {
      id: relayArg.id || relayId(board, addr),
      name: relayArg.name || `Relay ${board}.${addr}`,
      platform: 'switch',
      state: relayArg.state,
      writable: true,
      attributes: {
        board,
        addr,
        relayNumber: addr + 1,
      },
    };
  });
};

const relayStatesFromConfig = (configArg: IPencomConfig, statesArg: Array<Array<boolean | null>>): IPencomRelayState[] => {
  const relays = Array.isArray(configArg.relays) && configArg.relays.length ? configArg.relays : defaultRelays(configArg.boards || statesArg.length || 1);
  return relays.map((relayArg) => {
    const board = numberValue(relayArg.board) || 0;
    const addr = numberValue(relayArg.addr) || 0;
    return {
      ...relayArg,
      id: relayArg.id || relayId(board, addr),
      board,
      addr,
      state: statesArg[board]?.[addr] ?? null,
    };
  });
};

const defaultRelays = (boardsArg: number): IPencomRelayConfig[] => {
  const relays: IPencomRelayConfig[] = [];
  for (let board = 0; board < Math.max(1, boardsArg); board++) {
    for (let addr = 0; addr < pencomRelaysPerBoard; addr++) {
      relays.push({ id: relayId(board, addr), name: `Relay ${board}.${addr}`, board, addr });
    }
  }
  return relays;
};

const relayId = (boardArg: number, addrArg: number): string => `relay_${boardArg}_${addrArg}`;

const statesValue = (valueArg: unknown): Array<Array<boolean | null>> | undefined => {
  if (!Array.isArray(valueArg)) {
    return undefined;
  }
  return valueArg.map((boardArg) => Array.isArray(boardArg) ? boardArg.map((stateArg) => typeof stateArg === 'boolean' ? stateArg : null) : []);
};

const relayStatesValue = (valueArg: unknown): IPencomRelayState[] | undefined => {
  if (!Array.isArray(valueArg)) {
    return undefined;
  }
  const relays = valueArg.filter((relayArg): relayArg is IPencomRelayState => Boolean(relayArg && typeof relayArg === 'object' && 'addr' in relayArg && 'state' in relayArg));
  return relays.length ? relays : undefined;
};

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

const parseMaybeJson = (valueArg: unknown): unknown => {
  if (typeof valueArg !== 'string') {
    return valueArg;
  }
  try {
    return JSON.parse(valueArg) as IPencomRawApiData;
  } catch {
    return valueArg;
  }
};

const stringValue = (valueArg: unknown): string | undefined => {
  return 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.trim());
    return Number.isFinite(parsed) ? parsed : undefined;
  }
  return undefined;
};

const hostFromUrl = (urlArg: string | undefined): string | undefined => {
  if (!urlArg) {
    return undefined;
  }
  try {
    return new URL(urlArg.includes('://') ? urlArg : `tcp://${urlArg}`).hostname;
  } catch {
    return undefined;
  }
};
