import * as plugins from '../../plugins.js';
import { SimpleLocalClient, SimpleLocalMapper, type IServiceCallRequest, type IServiceCallResult, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot } from '../../core/index.js';
import type { INetioConfig } from './netio.types.js';
import { netioProfile } from './netio.types.js';

export class NetioClient extends SimpleLocalClient<INetioConfig> {
  private liveSnapshot?: ISimpleLocalSnapshot;

  constructor(configArg: INetioConfig) {
    super(netioProfile, configArg);
    this.netioConfig = configArg;
  }

  private readonly netioConfig: INetioConfig;

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

    try {
      const states = await this.readStates();
      this.liveSnapshot = this.snapshotFromStates(states);
      return SimpleLocalMapper.clone(this.liveSnapshot);
    } catch (errorArg) {
      this.liveSnapshot = this.buildOfflineSnapshot(errorMessage(errorArg));
      return SimpleLocalMapper.clone(this.liveSnapshot);
    }
  }

  public override async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (!this.netioConfig.host) {
      return super.execute(requestArg);
    }

    const outlet = this.outletFromRequest(requestArg);
    if (outlet === undefined) {
      return { success: false, error: 'Netio control requires request.data.outlet between 1 and 4.' };
    }

    try {
      let value: boolean;
      if (requestArg.service === 'turn_on') {
        value = true;
      } else if (requestArg.service === 'turn_off') {
        value = false;
      } else if (requestArg.service === 'toggle') {
        const states = await this.readStates();
        value = !states[outlet - 1];
      } else {
        return { success: false, error: `Unsupported Netio service: ${requestArg.service}` };
      }

      const command = ['u', 'u', 'u', 'u'];
      command[outlet - 1] = value ? '1' : '0';
      await this.requestCommand(`port list ${command.join('')}`);
      const states = await this.readStates();
      this.liveSnapshot = this.snapshotFromStates(states);
      return { success: true, data: { outlet, state: states[outlet - 1], snapshot: SimpleLocalMapper.clone(this.liveSnapshot) } };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  private async readStates(): Promise<boolean[]> {
    const stateText = await this.requestCommand('port list');
    if (!/^[01]{4}$/.test(stateText)) {
      throw new Error(`Netio returned invalid port list: ${stateText}`);
    }
    return stateText.split('').map((valueArg) => valueArg === '1');
  }

  private async requestCommand(commandArg: string): Promise<string> {
    const username = this.netioConfig.username || 'admin';
    const password = this.netioConfig.password || '';
    const values = await this.runSession([`login ${username} ${password}`, commandArg]);
    return values[values.length - 1] || '';
  }

  private async runSession(commandsArg: string[]): Promise<string[]> {
    const host = this.netioConfig.host;
    const port = this.netioConfig.port || netioProfile.defaultPort || 1234;
    if (!host) {
      throw new Error('Netio TCP client requires config.host.');
    }

    const timeoutMs = this.netioConfig.timeoutMs || 5000;
    return new Promise<string[]>((resolve, reject) => {
      const socket = plugins.net.createConnection({ host, port });
      const values: string[] = [];
      let buffer = '';
      let commandIndex = 0;
      let sawGreeting = false;
      let settled = false;

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

      const sendNext = () => {
        const command = commandsArg[commandIndex++];
        if (!command) {
          finish();
          return;
        }
        socket.write(command.endsWith('\r\n') ? command : `${command}\r\n`);
      };

      const handleLine = (lineArg: string) => {
        const line = lineArg.trim();
        if (!line) {
          return;
        }
        const value = parseNetioResponse(line);
        if (!sawGreeting) {
          sawGreeting = true;
          sendNext();
          return;
        }
        values.push(value);
        if (values.length >= commandsArg.length) {
          finish();
          return;
        }
        sendNext();
      };

      socket.setTimeout(timeoutMs, () => finish(new Error(`Netio TCP request to ${host}:${port} timed out after ${timeoutMs}ms.`)));
      socket.on('data', (chunkArg) => {
        buffer += Buffer.isBuffer(chunkArg) ? chunkArg.toString('utf8') : Buffer.from(chunkArg).toString('utf8');
        let newlineIndex = buffer.search(/\r?\n/);
        while (newlineIndex >= 0) {
          const line = buffer.slice(0, newlineIndex);
          buffer = buffer.slice(buffer[newlineIndex] === '\r' && buffer[newlineIndex + 1] === '\n' ? newlineIndex + 2 : newlineIndex + 1);
          handleLine(line);
          newlineIndex = buffer.search(/\r?\n/);
        }
      });
      socket.on('close', () => {
        if (!settled) {
          finish(new Error('Netio TCP connection closed before a complete response was received.'));
        }
      });
      socket.on('error', (errorArg) => finish(errorArg));
    });
  }

  private snapshotFromStates(statesArg: boolean[]): ISimpleLocalSnapshot {
    const host = this.netioConfig.host;
    const port = this.netioConfig.port || netioProfile.defaultPort || 1234;
    const entities: ISimpleLocalEntitySnapshot[] = statesArg.map((stateArg, indexArg) => {
      const outlet = indexArg + 1;
      return {
        id: `outlet_${outlet}`,
        name: this.outletName(outlet),
        platform: 'switch',
        state: stateArg,
        available: true,
        writable: true,
        attributes: { outlet },
      };
    });

    return {
      device: {
        id: this.netioConfig.uniqueId || (host ? `${host}:${port}` : 'netio'),
        name: this.netioConfig.name || host || netioProfile.defaultName || netioProfile.displayName,
        manufacturer: netioProfile.manufacturer,
        model: netioProfile.model,
        host,
        port,
        protocol: 'telnet',
      },
      entities,
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'tcp',
      rawData: { states: statesArg, stateText: statesArg.map((stateArg) => stateArg ? '1' : '0').join('') },
      capabilities: { localRead: true, localControl: true },
    };
  }

  private buildOfflineSnapshot(errorArg: string): ISimpleLocalSnapshot {
    const snapshot = this.snapshotFromStates([false, false, false, false]);
    return {
      ...snapshot,
      online: false,
      source: 'runtime',
      error: errorArg,
      entities: snapshot.entities.map((entityArg) => ({ ...entityArg, available: false })),
    };
  }

  private outletName(outletArg: number): string {
    const outlets = this.netioConfig.outlets;
    if (Array.isArray(outlets)) {
      return outlets[outletArg - 1] || `Outlet ${outletArg}`;
    }
    if (outlets && typeof outlets === 'object') {
      const outletNames = outlets as Record<string, unknown>;
      return String(outletNames[String(outletArg)] || outletNames[`output${outletArg}`] || `Outlet ${outletArg}`);
    }
    return `Outlet ${outletArg}`;
  }

  private outletFromRequest(requestArg: IServiceCallRequest): number | undefined {
    const value = requestArg.data?.outlet ?? requestArg.data?.port ?? requestArg.data?.output;
    const outlet = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : Number.NaN;
    return Number.isInteger(outlet) && outlet >= 1 && outlet <= 4 ? outlet : undefined;
  }
}

const parseNetioResponse = (lineArg: string): string => {
  const parts = lineArg.trim().split(/\s+/);
  if (!['100', '250'].includes(parts[0])) {
    throw new Error(`Netio command failed: ${lineArg}`);
  }
  return parts[1] || '';
};

const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
