import * as plugins from '../../plugins.js';
import { SimpleLocalClient, SimpleLocalMapper, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { PencomMapper } from './pencom.mapper.js';
import type { IPencomConfig, IPencomRawApiData, IPencomRelayConfig, IPencomRelayState, TPencomRawData } from './pencom.types.js';
import { pencomDefaultTimeoutMs, pencomProfile, pencomRelaysPerBoard } from './pencom.types.js';

export class PencomClient extends SimpleLocalClient<IPencomConfig> {
  private cachedSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly clientConfig: IPencomConfig) {
    super(pencomProfile, clientConfig);
  }

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

    if (!forceRefreshArg && this.clientConfig.snapshot) {
      this.cachedSnapshot = PencomMapper.toSnapshot({ config: this.clientConfig, source: 'snapshot', online: this.clientConfig.snapshot.online });
      return SimpleLocalMapper.clone(this.cachedSnapshot);
    }

    if (this.hasEndpoint() && !this.clientConfig.client && !this.clientConfig.snapshotProvider) {
      try {
        const rawData = await this.fetchRawData();
        this.cachedSnapshot = PencomMapper.toSnapshot({ config: this.clientConfig, rawData, online: true, source: 'tcp' });
      } catch (errorArg) {
        this.cachedSnapshot = this.createOfflineSnapshot(errorMessage(errorArg));
      }
      return SimpleLocalMapper.clone(this.cachedSnapshot);
    }

    if (this.clientConfig.client || this.clientConfig.snapshotProvider) {
      this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
      return SimpleLocalMapper.clone(this.cachedSnapshot);
    }

    if (this.hasManualData()) {
      this.cachedSnapshot = PencomMapper.toSnapshot({ config: this.clientConfig, rawData: this.rawData(), online: this.clientConfig.online ?? true, source: 'manual' });
      return SimpleLocalMapper.clone(this.cachedSnapshot);
    }

    this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
    return SimpleLocalMapper.clone(this.cachedSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.cachedSnapshot = undefined;
    const liveCapable = this.hasEndpoint() || Boolean(this.clientConfig.client || this.clientConfig.snapshotProvider);
    const snapshot = await this.getSnapshot(liveCapable);
    const success = liveCapable && snapshot.online && !snapshot.error && snapshot.source !== 'runtime';
    return { success, error: success ? undefined : snapshot.error || 'Pencom refresh requires a live TCP host/port, injected client, or snapshotProvider.', data: snapshot };
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.clientConfig.client?.execute || this.clientConfig.commandExecutor || !this.endpoint()) {
      return super.execute(requestArg);
    }

    try {
      const relay = await this.relayForRequest(requestArg);
      const nextState = requestArg.service === 'toggle' ? !(relay.state ?? false) : requestArg.service === 'turn_on';
      const rawData = await this.setRelay(relay.board || 0, relay.addr, nextState);
      this.cachedSnapshot = PencomMapper.toSnapshot({ config: this.clientConfig, rawData, online: true, source: 'tcp' });
      return { success: true, data: this.cachedSnapshot };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  public async destroy(): Promise<void> {
    await this.clientConfig.client?.destroy?.();
  }

  private async fetchRawData(): Promise<IPencomRawApiData> {
    const boards = this.boardCount();
    const states: Array<Array<boolean | null>> = [];
    for (let board = 0; board < boards; board++) {
      states[board] = await this.pollBoard(board);
    }
    return this.rawFromStates(states);
  }

  private async setRelay(boardArg: number, addrArg: number, stateArg: boolean): Promise<IPencomRawApiData> {
    this.assertBoardAddr(boardArg, addrArg);
    const command = `${boardName(boardArg)}${stateArg ? 'H' : 'L'}${addrArg + 1}`;
    const response = await this.exchange([command, `${boardName(boardArg)}R0`], 1);
    const states = this.emptyStates();
    states[boardArg] = statesFromBitmask(response[response.length - 1]);
    return this.rawFromStates(states);
  }

  private async pollBoard(boardArg: number): Promise<Array<boolean | null>> {
    if (boardArg < 0 || boardArg >= this.boardCount()) {
      throw new Error(`Pencom board ${boardArg} is outside configured board range.`);
    }
    const response = await this.exchange([`${boardName(boardArg)}R0`], 1);
    return statesFromBitmask(response[response.length - 1]);
  }

  private async relayForRequest(requestArg: IServiceCallRequest): Promise<IPencomRelayState> {
    const data = requestArg.data || {};
    const explicitBoard = numberValue(data.board);
    const explicitAddr = numberValue(data.addr ?? data.address ?? data.relay);
    if (explicitAddr !== undefined) {
      const board = explicitBoard ?? 0;
      this.assertBoardAddr(board, explicitAddr);
      const snapshot = requestArg.service === 'toggle' ? await this.getSnapshot(true) : undefined;
      const state = snapshot?.entities.find((entityArg) => numberValue(entityArg.attributes?.board) === board && numberValue(entityArg.attributes?.addr) === explicitAddr)?.state;
      return { name: `Relay ${board}.${explicitAddr}`, board, addr: explicitAddr, state: typeof state === 'boolean' ? state : null };
    }

    const snapshot = await this.getSnapshot(requestArg.service === 'toggle');
    const entityId = stringValue(requestArg.target.entityId) || stringValue(data.entityId);
    const relayId = stringValue(data.relayId) || stringValue(data.id);
    const relayName = stringValue(data.name);
    const entity = snapshot.entities.find((entityArg) => {
      return Boolean(
        (entityId && (entityArg.id === entityId || entityArg.uniqueId === entityId || entityArg.name === entityId))
        || (relayId && entityArg.id === relayId)
        || (relayName && entityArg.name === relayName)
      );
    }) || (snapshot.entities.length === 1 ? snapshot.entities[0] : undefined);

    if (!entity) {
      throw new Error('Pencom relay command requires data.board and data.addr, data.relayId/name, or a target.entityId matching a configured relay.');
    }

    const board = numberValue(entity.attributes?.board) ?? 0;
    const addr = numberValue(entity.attributes?.addr);
    if (addr === undefined) {
      throw new Error(`Pencom entity ${entity.id || entity.name} is missing relay addr metadata.`);
    }
    this.assertBoardAddr(board, addr);
    return { name: entity.name, board, addr, state: typeof entity.state === 'boolean' ? entity.state : null };
  }

  private rawFromStates(statesArg: Array<Array<boolean | null>>): IPencomRawApiData {
    const relays = this.relayConfigs().map((relayArg) => {
      const board = numberValue(relayArg.board) ?? 0;
      const addr = numberValue(relayArg.addr) ?? 0;
      return {
        ...relayArg,
        board,
        addr,
        id: relayArg.id || `relay_${board}_${addr}`,
        state: statesArg[board]?.[addr] ?? null,
      } satisfies IPencomRelayState;
    });
    return {
      boards: this.boardCount(),
      relays,
      states: statesArg,
      fetchedAt: new Date().toISOString(),
    };
  }

  private relayConfigs(): IPencomRelayConfig[] {
    if (Array.isArray(this.clientConfig.relays) && this.clientConfig.relays.length) {
      return this.clientConfig.relays;
    }
    const relays: IPencomRelayConfig[] = [];
    for (let board = 0; board < this.boardCount(); board++) {
      for (let addr = 0; addr < pencomRelaysPerBoard; addr++) {
        relays.push({ id: `relay_${board}_${addr}`, name: `Relay ${board}.${addr}`, board, addr });
      }
    }
    return relays;
  }

  private emptyStates(): Array<Array<boolean | null>> {
    return Array.from({ length: this.boardCount() }, () => Array.from({ length: pencomRelaysPerBoard }, () => null));
  }

  private boardCount(): number {
    return Math.max(1, Math.floor(numberValue(this.clientConfig.boards) || maxRelayBoard(this.clientConfig.relays) + 1 || 1));
  }

  private assertBoardAddr(boardArg: number, addrArg: number): void {
    if (boardArg < 0 || boardArg >= this.boardCount() || addrArg < 0 || addrArg >= pencomRelaysPerBoard) {
      throw new Error(`Pencom board ${boardArg} addr ${addrArg} is outside configured range.`);
    }
  }

  private async exchange(commandsArg: string[], expectedNumericResponsesArg: number): Promise<number[]> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('Pencom TCP exchange requires config.host/config.url and config.port.');
    }
    const timeoutMs = this.clientConfig.timeoutMs || pencomDefaultTimeoutMs;

    return new Promise<number[]>((resolve, reject) => {
      const responses: number[] = [];
      let buffer = '';
      let settled = false;
      const socket = plugins.net.createConnection({ host: endpoint.host, port: endpoint.port });

      const finish = (errorArg?: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        socket.removeAllListeners();
        socket.destroy();
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve(responses);
      };

      const processMessage = (messageArg: string) => {
        const trimmed = messageArg.trim();
        if (!trimmed) {
          return;
        }
        const parsed = Number(trimmed);
        if (!Number.isInteger(parsed)) {
          throw new Error(`Pencom returned non-numeric relay state: ${trimmed}`);
        }
        responses.push(parsed);
        if (responses.length >= expectedNumericResponsesArg) {
          finish();
        }
      };

      socket.setTimeout(timeoutMs, () => finish(new Error(`Pencom TCP request to ${endpoint.host}:${endpoint.port} timed out after ${timeoutMs}ms.`)));
      socket.on('connect', () => {
        for (const command of commandsArg) {
          socket.write(`${command}\r`);
        }
      });
      socket.on('data', (chunkArg) => {
        try {
          buffer += Buffer.isBuffer(chunkArg) ? chunkArg.toString('utf8') : Buffer.from(chunkArg).toString('utf8');
          const parts = buffer.split('\r');
          buffer = parts.pop() || '';
          for (const part of parts) {
            processMessage(part);
          }
        } catch (errorArg) {
          finish(errorArg instanceof Error ? errorArg : new Error(String(errorArg)));
        }
      });
      socket.on('end', () => responses.length >= expectedNumericResponsesArg ? finish() : finish(new Error('Pencom closed the TCP connection before returning a relay bitmask.')));
      socket.on('close', () => responses.length >= expectedNumericResponsesArg ? finish() : finish(new Error('Pencom closed the TCP connection before returning a relay bitmask.')));
      socket.on('error', (errorArg) => finish(errorArg));
    });
  }

  private endpoint(): IPencomEndpoint | undefined {
    const rawUrl = stringValue(this.clientConfig.url) || stringValue(this.clientConfig.metadata?.url);
    if (rawUrl) {
      const parsed = parseUrl(rawUrl);
      const port = this.clientConfig.port || numberValue(parsed?.port);
      if (parsed && port) {
        return { host: parsed.hostname, port };
      }
    }
    const host = stringValue(this.clientConfig.host);
    if (!host) {
      return undefined;
    }
    const parsedHost = parseUrl(host);
    if (parsedHost) {
      const port = numberValue(this.clientConfig.port) || numberValue(parsedHost.port);
      return port ? { host: parsedHost.hostname, port } : undefined;
    }
    const port = numberValue(this.clientConfig.port);
    if (!port) {
      return undefined;
    }
    return { host, port };
  }

  private hasEndpoint(): boolean {
    return Boolean(this.endpoint());
  }

  private hasManualData(): boolean {
    return this.rawData() !== undefined || Boolean(this.clientConfig.entities?.length || this.clientConfig.snapshot);
  }

  private rawData(): TPencomRawData | undefined {
    return this.clientConfig.rawData as TPencomRawData | undefined ?? this.clientConfig.rawText;
  }

  private createOfflineSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return PencomMapper.toSnapshot({ config: this.clientConfig, online: false, source: 'runtime', error: errorArg });
  }
}

interface IPencomEndpoint {
  host: string;
  port: number;
}

const boardName = (boardArg: number): string => String.fromCharCode('A'.charCodeAt(0) + boardArg);

const statesFromBitmask = (bitsArg: number): Array<boolean | null> => {
  const states: Array<boolean | null> = [];
  for (let index = 0; index < pencomRelaysPerBoard; index++) {
    states.push((bitsArg & (1 << index)) !== 0);
  }
  return states;
};

const maxRelayBoard = (relaysArg: IPencomRelayConfig[] | undefined): number => {
  if (!Array.isArray(relaysArg) || !relaysArg.length) {
    return 0;
  }
  return Math.max(0, ...relaysArg.map((relayArg) => numberValue(relayArg.board) || 0));
};

const parseUrl = (valueArg: string): URL | undefined => {
  try {
    return new URL(valueArg.includes('://') ? valueArg : `tcp://${valueArg}`);
  } catch {
    return undefined;
  }
};

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 errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
