import * as plugins from '../../plugins.js';
import { SimpleLocalClient, SimpleLocalMapper, type IServiceCallRequest, type IServiceCallResult, type ISimpleLocalEntitySnapshot } from '../../core/index.js';
import type { IOnewireConfig, IOnewireSnapshot } from './onewire.types.js';
import { onewireDefaultName, onewireDefaultPort, onewireProfile } from './onewire.types.js';

type TOnewirePlatform = 'binary_sensor' | 'select' | 'sensor' | 'switch';

interface IOnewireDeviceDescription {
  id: string;
  family: string;
  path: string;
  type?: string;
  parentId?: string;
}

interface IOnewireEntityDefinition {
  key: string;
  platform: TOnewirePlatform;
  name: string;
  unit?: string;
  deviceClass?: string;
  stateClass?: string;
  writable?: boolean;
  options?: string[];
}

interface IOnewireRawData {
  version?: string;
  devices: Array<IOnewireDeviceDescription & { values: Record<string, unknown> }>;
}

const owserverHeaderSize = 24;
const owserverMaxPayload = 65536;
const owserverDefaultTimeoutMs = 5000;
const owserverOwnetFlag = 0x00000100;
const owserverMessageType = {
  nop: 1,
  read: 2,
  write: 3,
  dirallslash: 9,
} as const;

const deviceSupport: Record<string, string[]> = {
  '01': [],
  '05': [],
  '10': [],
  '12': [],
  '1D': [],
  '1F': [],
  '20': [],
  '22': [],
  '26': [],
  '28': [],
  '29': [],
  '30': [],
  '3A': [],
  '3B': [],
  '42': [],
  '7E': ['EDS0065', 'EDS0066', 'EDS0068'],
  A6: [],
  EF: ['HB_HUB', 'HB_MOISTURE_METER', 'HobbyBoards_EF'],
};

const temperatureSensor = (keyArg = 'temperature'): IOnewireEntityDefinition => ({
  key: keyArg,
  platform: 'sensor',
  name: titleCase(keyArg),
  unit: '°C',
  deviceClass: 'temperature',
  stateClass: 'measurement',
});

const sensorDefinitions: Record<string, IOnewireEntityDefinition[]> = {
  '10': [temperatureSensor()],
  '12': [temperatureSensor('TAI8570/temperature'), sensor('TAI8570/pressure', 'mbar', 'pressure')],
  '1D': ['A', 'B'].map((idArg) => sensor(`counter.${idArg}`, undefined, undefined, 'total_increasing')),
  '20': ['A', 'B', 'C', 'D'].flatMap((idArg) => [sensor(`latestvolt.${idArg}`, 'V', 'voltage'), sensor(`volt.${idArg}`, 'V', 'voltage')]),
  '22': [temperatureSensor()],
  '26': [temperatureSensor(), sensor('humidity', '%', 'humidity'), sensor('HIH3600/humidity', '%', 'humidity'), sensor('HIH4000/humidity', '%', 'humidity'), sensor('HIH5030/humidity', '%', 'humidity'), sensor('HTM1735/humidity', '%', 'humidity'), sensor('B1-R1-A/pressure', 'mbar', 'pressure'), sensor('S3-R1-A/illuminance', 'lx', 'illuminance'), sensor('VAD', 'V', 'voltage'), sensor('VDD', 'V', 'voltage'), sensor('vis', 'V', 'voltage')],
  '28': [temperatureSensor()],
  '30': [temperatureSensor(), temperatureSensor('typeK/temperature'), sensor('volt', 'V', 'voltage'), sensor('vis', 'V', 'voltage')],
  '3B': [temperatureSensor()],
  '42': [temperatureSensor()],
  EDS0065: [temperatureSensor('EDS0065/temperature'), sensor('EDS0065/humidity', '%', 'humidity')],
  EDS0066: [temperatureSensor('EDS0066/temperature'), sensor('EDS0066/pressure', 'mbar', 'pressure')],
  EDS0068: [temperatureSensor('EDS0068/temperature'), sensor('EDS0068/pressure', 'mbar', 'pressure'), sensor('EDS0068/light', 'lx', 'illuminance'), sensor('EDS0068/humidity', '%', 'humidity')],
  HobbyBoards_EF: [sensor('humidity/humidity_corrected', '%', 'humidity'), sensor('humidity/humidity_raw', '%', 'humidity'), temperatureSensor('humidity/temperature')],
  HB_MOISTURE_METER: ['0', '1', '2', '3'].map((idArg) => sensor(`moisture/sensor.${idArg}`, 'cbar', 'pressure')),
};

const binaryDefinitions: Record<string, IOnewireEntityDefinition[]> = {
  '05': [binarySensor('sensed')],
  '12': ['A', 'B'].map((idArg) => binarySensor(`sensed.${idArg}`)),
  '29': range(0, 7).map((idArg) => binarySensor(`sensed.${idArg}`)),
  '3A': ['A', 'B'].map((idArg) => binarySensor(`sensed.${idArg}`)),
  HB_HUB: range(0, 3).map((idArg) => binarySensor(`hub/short.${idArg}`, 'problem')),
};

const switchDefinitions: Record<string, IOnewireEntityDefinition[]> = {
  '05': [switchEntity('PIO')],
  '12': ['A', 'B'].flatMap((idArg) => [switchEntity(`PIO.${idArg}`), switchEntity(`latch.${idArg}`)]),
  '26': [switchEntity('IAD')],
  '29': range(0, 7).flatMap((idArg) => [switchEntity(`PIO.${idArg}`), switchEntity(`latch.${idArg}`)]),
  '3A': ['A', 'B'].map((idArg) => switchEntity(`PIO.${idArg}`)),
  HB_HUB: range(0, 3).map((idArg) => switchEntity(`hub/branch.${idArg}`)),
  HB_MOISTURE_METER: range(0, 3).flatMap((idArg) => [switchEntity(`moisture/is_leaf.${idArg}`), switchEntity(`moisture/is_moisture.${idArg}`)]),
};

const selectDefinitions: Record<string, IOnewireEntityDefinition[]> = {
  '28': [{ key: 'tempres', platform: 'select', name: 'Temperature Resolution', writable: true, options: ['9', '10', '11', '12'] }],
};

export class OnewireClient extends SimpleLocalClient<IOnewireConfig> {
  private currentOnewireSnapshot?: IOnewireSnapshot;

  constructor(private readonly configArg: IOnewireConfig) {
    super(onewireProfile, configArg);
  }

  public async getSnapshot(forceRefreshArg = false): Promise<IOnewireSnapshot> {
    if (!forceRefreshArg && this.currentOnewireSnapshot) {
      return SimpleLocalMapper.clone(this.currentOnewireSnapshot) as IOnewireSnapshot;
    }

    if (!forceRefreshArg && this.configArg.snapshot) {
      this.currentOnewireSnapshot = await super.getSnapshot(false) as IOnewireSnapshot;
      return SimpleLocalMapper.clone(this.currentOnewireSnapshot) as IOnewireSnapshot;
    }

    if (this.configArg.client || this.configArg.snapshotProvider) {
      this.currentOnewireSnapshot = await super.getSnapshot(forceRefreshArg) as IOnewireSnapshot;
      return SimpleLocalMapper.clone(this.currentOnewireSnapshot) as IOnewireSnapshot;
    }

    if (this.configArg.host && this.configArg.transport !== 'snapshot') {
      try {
        this.currentOnewireSnapshot = await this.readOwserverSnapshot();
      } catch (errorArg) {
        this.currentOnewireSnapshot = this.createOfflineSnapshot(errorMessage(errorArg));
      }
      return SimpleLocalMapper.clone(this.currentOnewireSnapshot) as IOnewireSnapshot;
    }

    this.currentOnewireSnapshot = await super.getSnapshot(forceRefreshArg) as IOnewireSnapshot;
    return SimpleLocalMapper.clone(this.currentOnewireSnapshot) as IOnewireSnapshot;
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.currentOnewireSnapshot = undefined;
    const snapshot = await this.getSnapshot(true);
    const success = snapshot.online && !snapshot.error;
    return { success, error: success ? undefined : snapshot.error, data: snapshot };
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (!this.configArg.host || this.configArg.transport === 'snapshot') {
      return super.execute(requestArg);
    }

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

  private async readOwserverSnapshot(): Promise<IOnewireSnapshot> {
    const ownet = this.ownet();
    await ownet.ping();
    const version = await optional(() => ownet.read('/system/configuration/version'));
    const devices = await this.discoverDevices(ownet, '/');
    const rawDevices: IOnewireRawData['devices'] = [];
    const entities: ISimpleLocalEntitySnapshot[] = [];

    for (const device of devices) {
      const deviceValues: Record<string, unknown> = {};
      const deviceEntities = await this.readDeviceEntities(ownet, device, deviceValues);
      rawDevices.push({ ...device, values: deviceValues });
      entities.push(...deviceEntities);
    }

    const rawData: IOnewireRawData = {
      version: version?.trim() || undefined,
      devices: rawDevices,
    };

    return {
      device: {
        id: this.configArg.uniqueId || `${this.host()}:${this.port()}`,
        name: this.configArg.name || onewireDefaultName,
        manufacturer: 'OWFS',
        model: 'OWServer 1-Wire bus',
        host: this.host(),
        port: this.port(),
        protocol: 'tcp',
        configurationUrl: `tcp://${this.host()}:${this.port()}`,
        attributes: {
          version: rawData.version,
          deviceCount: devices.length,
        },
      },
      entities,
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'tcp',
      rawData,
      capabilities: {
        localRead: true,
        localControl: true,
        liveControlRequiresExecutor: false,
      },
    };
  }

  private async discoverDevices(ownetArg: OnewireOwnetClient, pathArg: string, parentIdArg?: string): Promise<IOnewireDeviceDescription[]> {
    const devices: IOnewireDeviceDescription[] = [];
    const entries = await ownetArg.dir(pathArg);

    for (const entry of entries) {
      const devicePath = normalizeDevicePath(entry);
      const deviceId = deviceIdFromPath(devicePath);
      if (!deviceId || !/^[0-9A-Fa-f]{2}[./]/.test(deviceId)) {
        continue;
      }
      const family = (await optional(() => ownetArg.read(`${devicePath}family`)))?.trim().toUpperCase();
      if (!family) {
        continue;
      }
      let type = (await optional(() => ownetArg.read(`${devicePath}type`)))?.trim();
      if (type === 'EDS') {
        type = (await optional(() => ownetArg.read(`${devicePath}device_type`)))?.trim() || type;
      }
      if (!isKnownDevice(family, type)) {
        continue;
      }
      devices.push({ id: deviceId, family, path: devicePath, type, parentId: parentIdArg });
      if (family === '1F') {
        devices.push(...await this.discoverDevices(ownetArg, `${devicePath}aux`, deviceId));
        devices.push(...await this.discoverDevices(ownetArg, `${devicePath}main`, deviceId));
      }
    }

    return devices;
  }

  private async readDeviceEntities(ownetArg: OnewireOwnetClient, deviceArg: IOnewireDeviceDescription, valuesArg: Record<string, unknown>): Promise<ISimpleLocalEntitySnapshot[]> {
    const effectiveFamily = effectiveDeviceFamily(deviceArg.family, deviceArg.type);
    const definitions = [
      ...(sensorDefinitions[effectiveFamily] || []),
      ...(binaryDefinitions[effectiveFamily] || []),
      ...(switchDefinitions[effectiveFamily] || []),
      ...(selectDefinitions[effectiveFamily] || []),
    ];
    const entities: ISimpleLocalEntitySnapshot[] = [];

    for (const definition of definitions) {
      const deviceFile = `${deviceArg.path}${definition.key}`;
      const rawValue = await optional(() => ownetArg.read(deviceFile));
      if (rawValue === undefined) {
        continue;
      }
      const state = stateFromRawValue(definition, rawValue);
      valuesArg[definition.key] = state;
      entities.push({
        id: SimpleLocalMapper.slug(`${deviceArg.id}_${definition.key}`),
        uniqueId: `${onewireProfile.domain}_${SimpleLocalMapper.slug(deviceArg.id)}_${SimpleLocalMapper.slug(definition.key)}`,
        name: `${deviceArg.id} ${definition.name}`,
        platform: definition.platform,
        state,
        available: true,
        writable: definition.writable,
        unit: definition.unit,
        deviceClass: definition.deviceClass,
        stateClass: definition.stateClass,
        attributes: {
          device_file: deviceFile,
          family: deviceArg.family,
          type: deviceArg.type,
          parentId: deviceArg.parentId,
          options: definition.options,
          raw_value: rawValue,
        },
      });
    }

    return entities;
  }

  private async executeOwserverCommand(requestArg: IServiceCallRequest): Promise<unknown> {
    const ownet = this.ownet();
    const path = await this.controlPath(requestArg);
    if (requestArg.service === 'select_option') {
      const option = stringData(requestArg, 'option');
      if (!option) {
        throw new Error('1-Wire select_option requires data.option.');
      }
      await ownet.write(path, option);
      return { path, value: option };
    }
    if (requestArg.service === 'turn_on' || requestArg.service === 'turn_off') {
      const value = requestArg.service === 'turn_on' ? '1' : '0';
      await ownet.write(path, value);
      return { path, value };
    }
    if (requestArg.service === 'toggle') {
      const current = (await ownet.read(path)).trim();
      const value = current === '1' ? '0' : '1';
      await ownet.write(path, value);
      return { path, value };
    }
    throw new Error(`Unsupported 1-Wire service: ${requestArg.domain}.${requestArg.service}`);
  }

  private async controlPath(requestArg: IServiceCallRequest): Promise<string> {
    const directPath = stringData(requestArg, 'path') || stringData(requestArg, 'device_file') || stringData(requestArg, 'deviceFile');
    if (directPath) {
      return directPath;
    }

    const entityId = requestArg.target.entityId;
    if (entityId) {
      const snapshot = await this.getSnapshot();
      const match = snapshot.entities.find((entityArg) => {
        const slug = SimpleLocalMapper.slug(entityArg.id || entityArg.name);
        return entityArg.id === entityId || entityArg.uniqueId === entityId || entityId.endsWith(`.${slug}`) || entityId.endsWith(`_${slug}`);
      });
      const path = match?.attributes?.device_file;
      if (typeof path === 'string' && path) {
        return path;
      }
    }

    throw new Error('1-Wire live control requires data.path, data.device_file, or a target.entityId from the current native snapshot.');
  }

  private ownet(): OnewireOwnetClient {
    return new OnewireOwnetClient(this.host(), this.port(), this.timeoutMs());
  }

  private host(): string {
    if (!this.configArg.host) {
      throw new Error('1-Wire OWServer polling requires config.host.');
    }
    return this.configArg.host;
  }

  private port(): number {
    return this.configArg.port || onewireDefaultPort;
  }

  private timeoutMs(): number {
    return this.configArg.timeoutMs || owserverDefaultTimeoutMs;
  }

  private createOfflineSnapshot(errorArg: string): IOnewireSnapshot {
    return SimpleLocalMapper.toSnapshot({ profile: onewireProfile, config: this.configArg, rawData: this.configArg.rawData, online: false, source: 'runtime', error: errorArg }) as IOnewireSnapshot;
  }
}

class OnewireOwnetClient {
  constructor(private readonly host: string, private readonly port: number, private readonly timeoutMs: number) {}

  public async ping(): Promise<void> {
    await this.request(owserverMessageType.nop, Buffer.alloc(0));
  }

  public async read(pathArg: string): Promise<string> {
    const payload = await this.request(owserverMessageType.read, asciiZero(pathArg), owserverMaxPayload);
    return payload.toString('ascii').trim();
  }

  public async dir(pathArg: string): Promise<string[]> {
    const payload = await this.request(owserverMessageType.dirallslash, asciiZero(pathArg));
    const listing = payload.toString('ascii').trim();
    return listing ? listing.split(',').map((entryArg) => entryArg.trim()).filter(Boolean) : [];
  }

  public async write(pathArg: string, valueArg: string): Promise<void> {
    await this.request(owserverMessageType.write, Buffer.concat([asciiZero(pathArg), Buffer.from(valueArg, 'ascii')]), Buffer.byteLength(valueArg));
  }

  private async request(typeArg: number, payloadArg: Buffer, sizeArg = 0, offsetArg = 0): Promise<Buffer> {
    const header = Buffer.alloc(owserverHeaderSize);
    header.writeInt32BE(0, 0);
    header.writeInt32BE(payloadArg.length, 4);
    header.writeInt32BE(typeArg, 8);
    header.writeInt32BE(owserverOwnetFlag, 12);
    header.writeInt32BE(sizeArg, 16);
    header.writeInt32BE(offsetArg, 20);

    return new Promise<Buffer>((resolve, reject) => {
      const socket = plugins.net.createConnection({ host: this.host, port: this.port });
      let buffer = Buffer.alloc(0);
      let settled = false;

      const finish = (errorArg?: Error, dataArg?: Buffer) => {
        if (settled) {
          return;
        }
        settled = true;
        socket.removeAllListeners();
        socket.destroy();
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve(dataArg || Buffer.alloc(0));
      };

      socket.setTimeout(this.timeoutMs, () => finish(new Error(`1-Wire OWServer request to ${this.host}:${this.port} timed out after ${this.timeoutMs}ms.`)));
      socket.on('connect', () => socket.write(Buffer.concat([header, payloadArg])));
      socket.on('data', (chunkArg) => {
        buffer = Buffer.concat([buffer, Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg)]);
        while (buffer.length >= owserverHeaderSize) {
          const payloadLength = buffer.readInt32BE(4);
          if (payloadLength < 0) {
            buffer = buffer.subarray(owserverHeaderSize);
            continue;
          }
          if (buffer.length < owserverHeaderSize + payloadLength) {
            return;
          }
          const ret = buffer.readInt32BE(8);
          const responseSize = buffer.readInt32BE(16);
          const payload = buffer.subarray(owserverHeaderSize, owserverHeaderSize + payloadLength);
          if (ret < 0) {
            finish(new Error(`OWServer returned error ${-ret}.`));
            return;
          }
          const usableLength = responseSize > 0 ? Math.min(responseSize, payload.length) : payload.length;
          finish(undefined, payload.subarray(0, usableLength));
          return;
        }
      });
      socket.on('end', () => finish(new Error(`1-Wire OWServer connection to ${this.host}:${this.port} closed before a complete response was received.`)));
      socket.on('close', () => finish(new Error(`1-Wire OWServer connection to ${this.host}:${this.port} closed before a complete response was received.`)));
      socket.on('error', (errorArg) => finish(errorArg));
    });
  }
}

function sensor(keyArg: string, unitArg?: string, deviceClassArg?: string, stateClassArg = 'measurement'): IOnewireEntityDefinition {
  return { key: keyArg, platform: 'sensor', name: titleCase(keyArg), unit: unitArg, deviceClass: deviceClassArg, stateClass: stateClassArg };
}

function binarySensor(keyArg: string, deviceClassArg?: string): IOnewireEntityDefinition {
  return { key: keyArg, platform: 'binary_sensor', name: titleCase(keyArg), deviceClass: deviceClassArg };
}

function switchEntity(keyArg: string): IOnewireEntityDefinition {
  return { key: keyArg, platform: 'switch', name: titleCase(keyArg), writable: true };
}

function range(startArg: number, endArg: number): string[] {
  const values: string[] = [];
  for (let value = startArg; value <= endArg; value++) {
    values.push(String(value));
  }
  return values;
}

function isKnownDevice(familyArg: string, typeArg?: string): boolean {
  const supportedTypes = deviceSupport[familyArg];
  if (!supportedTypes) {
    return false;
  }
  return supportedTypes.length === 0 || Boolean(typeArg && supportedTypes.includes(typeArg));
}

function effectiveDeviceFamily(familyArg: string, typeArg?: string): string {
  if (familyArg === 'A6') {
    return '26';
  }
  if ((familyArg === 'EF' || familyArg === '7E') && typeArg) {
    return typeArg;
  }
  return familyArg;
}

function stateFromRawValue(definitionArg: IOnewireEntityDefinition, rawValueArg: string): unknown {
  const raw = rawValueArg.trim();
  if (definitionArg.platform === 'binary_sensor' || definitionArg.platform === 'switch') {
    return raw === '1';
  }
  if (definitionArg.platform === 'sensor' && raw && Number.isFinite(Number(raw))) {
    return Number(raw);
  }
  return raw;
}

function normalizeDevicePath(pathArg: string): string {
  const prefixed = pathArg.startsWith('/') ? pathArg : `/${pathArg}`;
  return prefixed.endsWith('/') ? prefixed : `${prefixed}/`;
}

function deviceIdFromPath(pathArg: string): string | undefined {
  return pathArg.replace(/\/+$/g, '').split('/').filter(Boolean).pop();
}

function asciiZero(valueArg: string): Buffer {
  return Buffer.from(`${valueArg}\0`, 'ascii');
}

async function optional<TValue>(callbackArg: () => Promise<TValue>): Promise<TValue | undefined> {
  try {
    return await callbackArg();
  } catch {
    return undefined;
  }
}

function stringData(requestArg: IServiceCallRequest, keyArg: string): string | undefined {
  const value = requestArg.data?.[keyArg];
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
}

function titleCase(valueArg: string): string {
  return valueArg.replace(/[./_-]+/g, ' ').replace(/\b\w/g, (charArg) => charArg.toUpperCase());
}

function errorMessage(errorArg: unknown): string {
  return errorArg instanceof Error ? errorArg.message : String(errorArg);
}
