import type * as shxInterfaces from '@smarthome.exchange/interfaces';
import { SimpleLocalMapper, type IIntegrationEntity, type ISimpleLocalDeviceInfo, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot, type ISimpleLocalSnapshotOptions, type TSimpleLocalRawData, type TSimpleLocalSnapshotSource } from '../../core/index.js';
import type { IOpengarageConfig, IOpengarageRawApiData } from './opengarage.types.js';
import { opengarageDefaultPort, opengarageProfile } from './opengarage.types.js';

export class OpengarageMapper {
  public static toSnapshot(optionsArg: Omit<ISimpleLocalSnapshotOptions<IOpengarageConfig>, 'profile'>): ISimpleLocalSnapshot {
    if (isOpengarageRawApiData(optionsArg.rawData)) {
      return opengarageApiDataToSnapshot(optionsArg.config, optionsArg.rawData, optionsArg.online ?? true, optionsArg.source || 'http', optionsArg.error);
    }
    return SimpleLocalMapper.toSnapshot({ ...optionsArg, profile: opengarageProfile });
  }

  public static toSnapshotFromRaw(configArg: IOpengarageConfig, 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(opengarageProfile, snapshotArg);
  }

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

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

export const opengarageApiDataToSnapshot = (configArg: IOpengarageConfig, rawDataArg: IOpengarageRawApiData, onlineArg: boolean, sourceArg: TSimpleLocalSnapshotSource, errorArg?: string): ISimpleLocalSnapshot => {
  const endpoint = endpointInfo(configArg);
  const status = rawDataArg.status || {};
  const macAddress = stringValue(status.mac) || configArg.macAddress;
  const device: ISimpleLocalDeviceInfo = {
    id: configArg.uniqueId || configArg.deviceId || macAddress || endpoint?.id || opengarageProfile.domain,
    name: configArg.name || stringValue(status.name) || opengarageProfile.defaultName || opengarageProfile.displayName,
    manufacturer: 'Open Garage',
    model: opengarageProfile.model,
    serialNumber: macAddress,
    host: endpoint?.host,
    port: endpoint?.port,
    protocol: endpoint?.protocol || opengarageProfile.defaultProtocol,
    configurationUrl: endpoint?.origin,
    attributes: cleanAttributes({
      firmwareVersion: stringValue(status.fwv),
      suggestedArea: 'Garage',
      rawDoorState: numberValue(status.door),
    }),
  };

  return {
    device,
    entities: rawDataArg.entities?.length ? rawDataArg.entities : opengarageEntities(status),
    online: onlineArg,
    updatedAt: rawDataArg.fetchedAt || new Date().toISOString(),
    source: sourceArg,
    rawData: rawDataArg,
    error: errorArg,
    capabilities: {
      localRead: true,
      localControl: true,
      dataPath: '/jc',
      controlPath: '/cc',
    },
  };
};

export const isOpengarageRawApiData = (valueArg: unknown): valueArg is IOpengarageRawApiData => {
  if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
    return false;
  }
  const record = valueArg as Record<string, unknown>;
  return isRecord(record.status) && ('door' in record.status || 'mac' in record.status || 'fwv' in record.status);
};

const opengarageEntities = (statusArg: Record<string, unknown>): ISimpleLocalEntitySnapshot[] => {
  const entities = [
    coverEntity(statusArg),
    binarySensor('vehicle', 'Vehicle', vehicleState(statusArg.vehicle), { available: vehicleState(statusArg.vehicle) !== undefined }),
    sensor('dist', 'Distance', numberValue(statusArg.dist), { unit: 'cm', deviceClass: 'distance', stateClass: 'measurement' }),
    sensor('rssi', 'RSSI', numberValue(statusArg.rssi), { unit: 'dBm', deviceClass: 'signal_strength', stateClass: 'measurement' }),
    sensor('temp', 'Temperature', numberValue(statusArg.temp), { unit: 'C', deviceClass: 'temperature', stateClass: 'measurement' }),
    sensor('humid', 'Humidity', numberValue(statusArg.humid), { unit: '%', deviceClass: 'humidity', stateClass: 'measurement' }),
    button('restart', 'Restart'),
  ];
  return entities.filter((entityArg): entityArg is ISimpleLocalEntitySnapshot => Boolean(entityArg));
};

const coverEntity = (statusArg: Record<string, unknown>): ISimpleLocalEntitySnapshot => ({
  id: 'garage_door',
  uniqueId: `${opengarageProfile.domain}_garage_door`,
  name: 'Garage Door',
  platform: 'cover',
  state: doorState(statusArg.door),
  available: doorState(statusArg.door) !== undefined,
  writable: true,
  deviceClass: 'garage',
  attributes: cleanAttributes({ rawDoorState: numberValue(statusArg.door) }),
});

const sensor = (idArg: string, nameArg: string, stateArg: unknown, optionsArg: Partial<ISimpleLocalEntitySnapshot> = {}): ISimpleLocalEntitySnapshot | undefined => stateArg === undefined ? undefined : ({
  id: idArg,
  uniqueId: `${opengarageProfile.domain}_${idArg}`,
  name: nameArg,
  platform: 'sensor',
  state: stateArg,
  available: true,
  writable: false,
  ...optionsArg,
});

const binarySensor = (idArg: string, nameArg: string, stateArg: unknown, optionsArg: Partial<ISimpleLocalEntitySnapshot> = {}): ISimpleLocalEntitySnapshot => ({
  id: idArg,
  uniqueId: `${opengarageProfile.domain}_${idArg}`,
  name: nameArg,
  platform: 'binary_sensor',
  state: stateArg ?? false,
  available: true,
  writable: false,
  ...optionsArg,
});

const button = (idArg: string, nameArg: string): ISimpleLocalEntitySnapshot => ({
  id: idArg,
  uniqueId: `${opengarageProfile.domain}_${idArg}`,
  name: nameArg,
  platform: 'button',
  state: 'available',
  available: true,
  writable: true,
});

const doorState = (valueArg: unknown): string | undefined => numberValue(valueArg) === 0 ? 'closed' : numberValue(valueArg) === 1 ? 'open' : undefined;
const vehicleState = (valueArg: unknown): boolean | undefined => numberValue(valueArg) === 1 ? true : numberValue(valueArg) === 0 ? false : undefined;

const endpointInfo = (configArg: IOpengarageConfig): { id: string; host: string; port: number; protocol: 'http' | 'https'; origin: string } | undefined => {
  const rawUrl = stringValue(configArg.url) || stringValue(configArg.metadata?.url);
  const parsedUrl = rawUrl ? parseUrl(rawUrl) : undefined;
  const parsedHost = !parsedUrl && configArg.host ? parseUrl(configArg.host) : undefined;
  const url = parsedUrl || parsedHost;
  if (url) {
    const protocol = url.protocol === 'https:' ? 'https' : 'http';
    const port = configArg.port || numberValue(url.port) || (protocol === 'https' ? 443 : opengarageDefaultPort);
    const origin = `${protocol}://${url.hostname}${portPart(protocol, port)}`;
    return { id: `${url.hostname}:${port}`, host: url.hostname, port, protocol, origin };
  }
  const host = stringValue(configArg.host);
  if (!host) {
    return undefined;
  }
  const protocol = configArg.useTls ? 'https' : 'http';
  const port = configArg.port || (protocol === 'https' ? 443 : opengarageDefaultPort);
  const origin = `${protocol}://${host}${portPart(protocol, port)}`;
  return { id: `${host}:${port}`, host, port, protocol, origin };
};

const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> => Boolean(valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg));
const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
const numberValue = (valueArg: unknown): number | undefined => typeof valueArg === 'number' && Number.isFinite(valueArg) ? valueArg : stringValue(valueArg) && Number.isFinite(Number(valueArg)) ? Number(valueArg) : undefined;
const parseUrl = (valueArg: string): URL | undefined => {
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(valueArg)) {
    return undefined;
  }
  try {
    return new URL(valueArg);
  } catch {
    return undefined;
  }
};
const portPart = (protocolArg: 'http' | 'https', portArg: number): string => (protocolArg === 'http' && portArg === 80) || (protocolArg === 'https' && portArg === 443) ? '' : `:${portArg}`;
const 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;
};
