import * as plugins from '../../plugins.js';
import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { LcnMapper } from './lcn.mapper.js';
import type { ILcnConfig } from './lcn.types.js';
import { lcnDefaultName, lcnDefaultPort, lcnProfile } from './lcn.types.js';

interface ILcnPchkSession {
  authenticated: boolean;
  lcnConnected: boolean;
  transcript: string[];
  send(lineArg: string): Promise<void>;
  close(): Promise<void>;
}

export class LcnClient extends SimpleLocalClient<ILcnConfig> {
  private lcnCurrentSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly lcnConfig: ILcnConfig) {
    super(lcnProfile, lcnConfig);
  }

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

    if (!this.shouldUseNativePchk(forceRefreshArg)) {
      this.lcnCurrentSnapshot = await super.getSnapshot(forceRefreshArg);
      return cloneSnapshot(this.lcnCurrentSnapshot);
    }

    try {
      const session = await this.openSession();
      this.lcnCurrentSnapshot = LcnMapper.toSnapshot({
        config: this.lcnConfig,
        rawData: this.snapshotRawData(session),
        online: true,
        source: 'tcp',
      });
      await session.close();
    } catch (errorArg) {
      this.lcnCurrentSnapshot = this.offlineLcnSnapshot(errorMessage(errorArg));
    }

    return cloneSnapshot(this.lcnCurrentSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.lcnCurrentSnapshot = undefined;
    if (!this.shouldUseNativePchk(true)) {
      return super.refresh();
    }
    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.lcnConfig.client?.execute || this.lcnConfig.commandExecutor) {
      return super.execute(requestArg);
    }

    if (requestArg.service === 'pck') {
      return this.executePck(requestArg);
    }

    if (this.lcnConfig.host && lcnProfile.controlServices?.includes(requestArg.service)) {
      return {
        success: false,
        error: `Native LCN PCHK TCP support implements live status and the raw pck service only; ${requestArg.service} needs pypck command generation or an injected client.execute().`,
      };
    }

    return super.execute(requestArg);
  }

  private shouldUseNativePchk(forceRefreshArg: boolean): boolean {
    if (!this.lcnConfig.host || this.lcnConfig.client || this.lcnConfig.snapshotProvider) {
      return false;
    }
    if (!forceRefreshArg && (this.lcnConfig.snapshot || this.hasManualData())) {
      return false;
    }
    return true;
  }

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

  private async executePck(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (!this.lcnConfig.host) {
      return super.execute(requestArg);
    }

    const pck = stringValue(requestArg.data?.pck);
    if (!pck) {
      return { success: false, error: 'LCN pck service requires data.pck.' };
    }

    try {
      const session = await this.openSession();
      const command = this.normalizePckCommand(pck, requestArg.data || {});
      await session.send(command);
      await wait(this.lcnConfig.postCommandWaitMs ?? 20);
      await session.close();
      this.lcnCurrentSnapshot = undefined;
      return { success: true, data: { command, host: this.lcnConfig.host, port: this.lcnConfig.port || lcnDefaultPort } };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  private normalizePckCommand(pckArg: string, dataArg: Record<string, unknown>): string {
    const pck = pckArg.trim();
    if (!pck) {
      throw new Error('LCN pck service requires a non-empty PCK command.');
    }
    if (booleanValue(dataArg.rawHostCommand) || booleanValue(dataArg.toHost) || pck.startsWith('>') || pck.startsWith('!') || pck.startsWith('^')) {
      return pck;
    }
    return `${this.addressHeader(dataArg)}${pck}`;
  }

  private addressHeader(dataArg: Record<string, unknown>): string {
    const segmentId = numberValue(dataArg.segmentId) ?? numberValue(dataArg.segment) ?? this.lcnConfig.segmentId ?? 0;
    const moduleId = numberValue(dataArg.moduleId) ?? numberValue(dataArg.addressId) ?? numberValue(dataArg.addrId) ?? this.lcnConfig.moduleId;
    if (moduleId === undefined) {
      throw new Error('LCN addressed PCK commands require data.moduleId or config.moduleId.');
    }
    if (!validPckId(segmentId, 0, 128) || !validPckId(moduleId, 1, 255)) {
      throw new Error('LCN address segmentId must be 0..128 and moduleId/addressId must be 1..255.');
    }
    const isGroup = booleanValue(dataArg.group) ?? booleanValue(dataArg.isGroup) ?? this.lcnConfig.group ?? false;
    const acknowledge = booleanValue(dataArg.acknowledge) ?? this.lcnConfig.acknowledge ?? false;
    const localSegmentId = this.lcnConfig.localSegmentId ?? 0;
    const physicalSegmentId = segmentId === localSegmentId ? 0 : segmentId;
    return `>${isGroup ? 'G' : 'M'}${pad3(physicalSegmentId)}${pad3(moduleId)}${acknowledge ? '!' : '.'}`;
  }

  private async openSession(): Promise<ILcnPchkSession> {
    const host = this.lcnConfig.host;
    if (!host) {
      throw new Error('LCN PCHK TCP connection requires config.host.');
    }
    const port = this.lcnConfig.port || lcnDefaultPort;
    const timeoutMs = this.lcnConfig.timeoutMs || 5000;
    const username = this.lcnConfig.username || '';
    const password = this.lcnConfig.password || '';
    const dimMode = this.lcnConfig.dimMode === 'STEPS50' ? '0' : '1';

    return new Promise<ILcnPchkSession>((resolve, reject) => {
      const socket: plugins.net.Socket = this.lcnConfig.useTls
        ? plugins.tls.connect({ host, port })
        : plugins.net.createConnection({ host, port });
      const transcript: string[] = [];
      let buffer = '';
      let settled = false;
      let authenticated = false;
      let lcnConnected = true;

      const timer = setTimeout(() => fail(new Error(`LCN PCHK TCP connection to ${host}:${port} timed out after ${timeoutMs}ms.`)), timeoutMs);

      const send = (lineArg: string): Promise<void> => new Promise<void>((sendResolve, sendReject) => {
        const line = lineArg.endsWith('\n') ? lineArg.slice(0, -1) : lineArg;
        transcript.push(`> ${line}`);
        socket.write(`${line}\n`, 'utf8', (errorArg) => {
          if (errorArg) {
            sendReject(errorArg);
            return;
          }
          sendResolve();
        });
      });

      const ready = () => {
        if (settled) {
          return;
        }
        if (!lcnConnected) {
          fail(new Error('LCN PCHK reports that the LCN bus is disconnected.'));
          return;
        }
        settled = true;
        clearTimeout(timer);
        resolve({
          get authenticated() {
            return authenticated;
          },
          get lcnConnected() {
            return lcnConnected;
          },
          transcript,
          send,
          close: () => closeSocket(socket),
        });
      };

      const handleLine = (lineArg: string) => {
        const line = lineArg.replace(/\r$/, '');
        if (!line) {
          return;
        }
        transcript.push(`< ${line}`);
        if (line === 'Username:') {
          void send(username).catch(fail);
          return;
        }
        if (line === 'Password:') {
          void send(password).catch(fail);
          return;
        }
        if (line === 'OK') {
          authenticated = true;
          void send('!CHD').catch(fail);
          return;
        }
        if (line === 'Authentification failed.') {
          fail(new Error('LCN PCHK authentication failed.'));
          return;
        }
        if (line === '$err:(license?)') {
          fail(new Error('LCN PCHK license limit reached.'));
          return;
        }
        if (line === '$io:#LCN:connected') {
          lcnConnected = true;
          return;
        }
        if (line === '$io:#LCN:disconnected') {
          lcnConnected = false;
          return;
        }
        if (line === '(dec-mode)') {
          void send(`!OM${dimMode}P`).then(ready).catch(fail);
        }
      };

      socket.setTimeout(timeoutMs, () => fail(new Error(`LCN PCHK TCP connection to ${host}:${port} timed out after ${timeoutMs}ms.`)));
      socket.on('data', (chunkArg) => {
        buffer += chunkArg.toString('utf8');
        const lines = buffer.split('\n');
        buffer = lines.pop() || '';
        for (const line of lines) {
          handleLine(line);
        }
      });
      socket.on('error', (errorArg) => fail(errorArg));
      socket.on('close', () => {
        if (!settled) {
          fail(new Error(`LCN PCHK TCP connection to ${host}:${port} closed before setup completed.`));
        }
      });

      function fail(errorArg: Error): void {
        if (settled) {
          return;
        }
        settled = true;
        clearTimeout(timer);
        socket.removeAllListeners();
        socket.destroy();
        reject(errorArg);
      }
    });
  }

  private snapshotRawData(sessionArg: ILcnPchkSession): Record<string, unknown> {
    const host = this.lcnConfig.host;
    const port = this.lcnConfig.port || lcnDefaultPort;
    return {
      device: {
        id: this.lcnConfig.uniqueId || (host ? `${host}:${port}` : lcnDefaultName),
        name: this.lcnConfig.name || lcnDefaultName,
        manufacturer: 'Issendorff',
        model: 'LCN-PCHK',
        host,
        port,
        protocol: 'tcp',
      },
      entities: [
        {
          id: 'pchk_connection',
          name: 'PCHK Connection',
          platform: 'binary_sensor',
          state: sessionArg.authenticated && sessionArg.lcnConnected,
          available: true,
          attributes: {
            authenticated: sessionArg.authenticated,
            lcnConnected: sessionArg.lcnConnected,
            protocol: 'LCN-PCK over PCHK TCP',
            transcript: sessionArg.transcript,
          },
        },
      ],
      authenticated: sessionArg.authenticated,
      lcnConnected: sessionArg.lcnConnected,
    };
  }

  private offlineLcnSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return LcnMapper.toSnapshot({ config: this.lcnConfig, online: false, source: 'runtime', error: errorArg });
  }
}

function closeSocket(socketArg: plugins.net.Socket): Promise<void> {
  return new Promise((resolve) => {
    if (socketArg.destroyed) {
      resolve();
      return;
    }
    socketArg.once('close', () => resolve());
    socketArg.end();
    setTimeout(() => {
      if (!socketArg.destroyed) {
        socketArg.destroy();
      }
      resolve();
    }, 25);
  });
}

function cloneSnapshot(snapshotArg: ISimpleLocalSnapshot): ISimpleLocalSnapshot {
  return JSON.parse(JSON.stringify(snapshotArg)) as ISimpleLocalSnapshot;
}

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

function stringValue(valueArg: unknown): string | undefined {
  return typeof valueArg === 'string' && valueArg.trim() ? valueArg : undefined;
}

function 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;
}

function booleanValue(valueArg: unknown): boolean | undefined {
  if (typeof valueArg === 'boolean') {
    return valueArg;
  }
  if (typeof valueArg === 'string') {
    if (['true', '1', 'yes', 'on'].includes(valueArg.toLowerCase())) {
      return true;
    }
    if (['false', '0', 'no', 'off'].includes(valueArg.toLowerCase())) {
      return false;
    }
  }
  return undefined;
}

function validPckId(valueArg: number, minArg: number, maxArg: number): boolean {
  return Number.isInteger(valueArg) && valueArg >= minArg && valueArg <= maxArg;
}

function pad3(valueArg: number): string {
  return String(valueArg).padStart(3, '0');
}

function wait(msArg: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, msArg));
}
