import * as plugins from '../../plugins.js';
import { SimpleLocalClient, SimpleLocalMapper, type ISimpleLocalEntitySnapshot, type IServiceCallRequest, type IServiceCallResult, type ISimpleLocalSnapshot } from '../../core/index.js';
import type { IPilightConfig } from './pilight.types.js';
import { pilightDefaultHost, pilightDefaultPort, pilightProfile } from './pilight.types.js';

export type TPilightMessageHandler = (messageArg: Record<string, unknown>) => void;

export class PilightClient extends SimpleLocalClient<IPilightConfig> {
  private cachedSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly configArg: IPilightConfig) {
    super(pilightProfile, configArg);
  }

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

    if (this.shouldUseNativeTcp(forceRefreshArg)) {
      try {
        const response = await this.identify(senderIdentifyOptions);
        this.cachedSnapshot = this.statusSnapshot(true, 'tcp', { identify: response });
      } catch (errorArg) {
        this.cachedSnapshot = this.statusSnapshot(false, 'runtime', undefined, errorMessage(errorArg));
      }
      return SimpleLocalMapper.clone(this.cachedSnapshot);
    }

    const snapshot = await super.getSnapshot(forceRefreshArg);
    this.cachedSnapshot = SimpleLocalMapper.toSnapshot({ profile: pilightProfile, config: this.configArg, rawData: snapshot.rawData ?? snapshot, online: snapshot.online, source: snapshot.source, error: snapshot.error });
    return SimpleLocalMapper.clone(this.cachedSnapshot);
  }

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

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

    if (requestArg.service === 'snapshot' || requestArg.service === 'status') {
      return { success: true, data: await this.getSnapshot() };
    }
    if (requestArg.service === 'refresh' || requestArg.service === 'reload') {
      return this.refresh();
    }

    if (!this.endpoint()) {
      return { success: false, error: 'Pilight native TCP control requires config.host/config.url or an injected client.execute() or commandExecutor.' };
    }

    try {
      const data = requestArg.data || {};
      const code = this.codeForService(requestArg.service, data);
      const acknowledge = boolValue(data.acknowledge) ?? this.configArg.acknowledge ?? true;
      const result = await this.sendCode(code, acknowledge);
      this.cachedSnapshot = undefined;
      return { success: true, data: result };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  public async sendCode(codeArg: Record<string, unknown>, acknowledgeArg = true): Promise<Record<string, unknown>> {
    if (!('protocol' in codeArg)) {
      throw new Error('Pilight send requires code.protocol, matching the pilight-daemon send API.');
    }

    const responses = await this.exchange([{ action: 'send', code: codeArg }], { identifyOptions: senderIdentifyOptions, minResponses: acknowledgeArg ? 1 : 0 });
    const acknowledged = !acknowledgeArg || responses.some((messageArg) => messageArg.status === 'success');
    if (!acknowledged) {
      throw new Error(`Pilight daemon rejected send code: ${JSON.stringify(codeArg)}`);
    }
    return { code: codeArg, responses, acknowledged };
  }

  public async subscribe(handlerArg: TPilightMessageHandler): Promise<() => Promise<void>> {
    const endpoint = this.requireEndpoint();
    const timeoutMs = this.configArg.timeoutMs || 1000;

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

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

      socket.setTimeout(timeoutMs, () => {
        if (!identified) {
          fail(new Error(`Pilight TCP receiver identify to ${endpoint.host}:${endpoint.port} timed out after ${timeoutMs}ms.`));
        }
      });
      socket.on('connect', () => socket.write(JSON.stringify({ action: 'identify', options: receiverIdentifyOptions(this.configArg) })));
      socket.on('data', (chunkArg) => {
        buffer += Buffer.isBuffer(chunkArg) ? chunkArg.toString('utf8') : String(chunkArg);
        const parsed = parseJsonMessages(buffer);
        buffer = parsed.rest;
        for (const message of parsed.messages) {
          if (!identified) {
            if (message.status !== 'success') {
              fail(new Error(`Pilight receiver identify failed: ${JSON.stringify(message)}`));
              return;
            }
            identified = true;
            socket.setTimeout(0);
            if (!settled) {
              settled = true;
              resolve(async () => {
                socket.removeAllListeners();
                socket.destroy();
              });
            }
            continue;
          }
          if (shouldEmitReceivedMessage(message, this.configArg)) {
            handlerArg(message);
          }
        }
      });
      socket.on('error', (errorArg) => identified ? undefined : fail(errorArg));
      socket.on('close', () => identified ? undefined : fail(new Error(`Pilight TCP receiver connection to ${endpoint.host}:${endpoint.port} closed before identify completed.`)));
    });
  }

  private shouldUseNativeTcp(forceRefreshArg: boolean): boolean {
    if (!this.endpoint() || this.configArg.client || this.configArg.snapshotProvider || this.configArg.transport === 'snapshot') {
      return false;
    }
    if (!forceRefreshArg && this.configArg.snapshot) {
      return false;
    }
    return true;
  }

  private async identify(optionsArg: Record<string, unknown>): Promise<Record<string, unknown>> {
    const responses = await this.exchange([], { identifyOptions: optionsArg, minResponses: 0 });
    return responses[0] || { status: 'success' };
  }

  private async exchange(messagesArg: Record<string, unknown>[], optionsArg: IPilightExchangeOptions): Promise<Record<string, unknown>[]> {
    const endpoint = this.requireEndpoint();
    const timeoutMs = this.configArg.timeoutMs || 1000;

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

      const finish = (errorArg?: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        clearTimeout(timer);
        socket.removeAllListeners();
        socket.destroy();
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve(responses);
      };
      const sendMessages = () => {
        for (const message of messagesArg) {
          socket.write(JSON.stringify(message));
        }
        if (optionsArg.minResponses === 0) {
          finish();
        }
      };
      const timer = setTimeout(() => finish(new Error(`Pilight TCP request to ${endpoint.host}:${endpoint.port} timed out after ${timeoutMs}ms.`)), timeoutMs);

      socket.on('connect', () => socket.write(JSON.stringify({ action: 'identify', options: optionsArg.identifyOptions })));
      socket.on('data', (chunkArg) => {
        buffer += Buffer.isBuffer(chunkArg) ? chunkArg.toString('utf8') : String(chunkArg);
        const parsed = parseJsonMessages(buffer);
        buffer = parsed.rest;
        for (const message of parsed.messages) {
          if (!identified) {
            if (message.status !== 'success') {
              finish(new Error(`Pilight identify failed: ${JSON.stringify(message)}`));
              return;
            }
            identified = true;
            if (!messagesArg.length) {
              responses.push(message);
              finish();
              return;
            }
            sendMessages();
            continue;
          }
          responses.push(message);
          if (responses.length >= optionsArg.minResponses) {
            finish();
            return;
          }
        }
      });
      socket.on('error', (errorArg) => finish(errorArg));
      socket.on('close', () => {
        if (!settled) {
          finish(new Error(`Pilight TCP connection to ${endpoint.host}:${endpoint.port} closed before the expected response.`));
        }
      });
    });
  }

  private codeForService(serviceArg: string, dataArg: Record<string, unknown>): Record<string, unknown> {
    if (serviceArg === 'send') {
      return codeFromData(dataArg, ['code', 'message']);
    }
    if (serviceArg === 'turn_on') {
      return codeFromData(dataArg, ['on_code', 'onCode', 'code']);
    }
    if (serviceArg === 'turn_off') {
      return codeFromData(dataArg, ['off_code', 'offCode', 'code']);
    }
    if (serviceArg === 'toggle') {
      return codeFromData(dataArg, ['toggle_code', 'toggleCode', 'code']);
    }
    throw new Error(`Unsupported Pilight native service: ${serviceArg}`);
  }

  private statusSnapshot(onlineArg: boolean, sourceArg: 'tcp' | 'runtime', rawDataArg?: unknown, errorArg?: string): ISimpleLocalSnapshot {
    const endpoint = this.endpoint();
    const entities: ISimpleLocalEntitySnapshot[] = [{
      id: 'daemon',
      name: 'Pilight daemon',
      platform: 'binary_sensor',
      state: onlineArg,
      available: onlineArg,
      attributes: cleanAttributes({ source: sourceArg }),
    }];

    return {
      device: {
        id: this.configArg.uniqueId || this.configArg.deviceId || (endpoint ? `${endpoint.host}:${endpoint.port}` : this.configArg.name) || pilightProfile.domain,
        name: this.configArg.name || pilightProfile.defaultName || pilightProfile.displayName,
        manufacturer: 'Pilight',
        model: 'pilight-daemon TCP API',
        host: endpoint?.host,
        port: endpoint?.port,
        protocol: 'tcp',
        configurationUrl: endpoint ? `tcp://${endpoint.host}:${endpoint.port}` : undefined,
        attributes: cleanAttributes({ ...this.configArg.metadata }),
      },
      entities,
      online: onlineArg,
      updatedAt: new Date().toISOString(),
      source: sourceArg,
      rawData: rawDataArg,
      error: errorArg,
      capabilities: {
        localRead: true,
        localControl: true,
        localPush: true,
      },
    };
  }

  private requireEndpoint(): IPilightEndpoint {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('Pilight TCP API requires config.host/config.url, or config.transport="tcp" with an optional port.');
    }
    return endpoint;
  }

  private endpoint(): IPilightEndpoint | undefined {
    const rawUrl = stringValue(this.configArg.url) || stringValue(this.configArg.metadata?.url);
    if (rawUrl) {
      const parsed = parseUrl(rawUrl);
      if (parsed) {
        return { host: parsed.hostname, port: this.configArg.port || numberValue(parsed.port) || pilightDefaultPort };
      }
    }

    const host = stringValue(this.configArg.host);
    if (host) {
      const parsedHost = parseUrl(host);
      if (parsedHost) {
        return { host: parsedHost.hostname, port: this.configArg.port || numberValue(parsedHost.port) || pilightDefaultPort };
      }
      return { host, port: this.configArg.port || pilightDefaultPort };
    }

    if (this.configArg.transport === 'tcp' || this.configArg.port) {
      return { host: pilightDefaultHost, port: this.configArg.port || pilightDefaultPort };
    }
    return undefined;
  }
}

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

interface IPilightExchangeOptions {
  identifyOptions: Record<string, unknown>;
  minResponses: number;
}

const senderIdentifyOptions = {
  core: 0,
  receiver: 0,
  config: 0,
};

const receiverIdentifyOptions = (configArg: IPilightConfig): Record<string, unknown> => {
  if (configArg.recvIdent && typeof configArg.recvIdent === 'object' && !Array.isArray(configArg.recvIdent)) {
    return configArg.recvIdent as Record<string, unknown>;
  }
  return {
    core: 0,
    receiver: 1,
    config: 0,
    forward: 0,
  };
};

const codeFromData = (dataArg: Record<string, unknown>, wrapperKeysArg: string[]): Record<string, unknown> => {
  for (const key of wrapperKeysArg) {
    const wrapped = objectValue(dataArg[key]);
    if (wrapped) {
      return { ...wrapped };
    }
  }
  return Object.fromEntries(Object.entries(dataArg).filter(([keyArg, valueArg]) => !internalSendKeys.has(keyArg) && valueArg !== undefined));
};

const internalSendKeys = new Set(['acknowledge', 'code', 'message', 'on_code', 'onCode', 'off_code', 'offCode', 'toggle_code', 'toggleCode']);

const shouldEmitReceivedMessage = (messageArg: Record<string, unknown>, configArg: IPilightConfig): boolean => {
  const recvCodesOnly = configArg.recvCodesOnly ?? true;
  const vetoRepeats = configArg.vetoRepeats ?? true;
  if (recvCodesOnly && !originContainsReceiver(messageArg.origin)) {
    return false;
  }
  const repeats = numberValue(messageArg.repeats);
  if (vetoRepeats && repeats !== undefined && repeats !== 1) {
    return false;
  }
  return true;
};

const originContainsReceiver = (originArg: unknown): boolean => {
  if (Array.isArray(originArg)) {
    return originArg.some((valueArg) => String(valueArg).includes('receiver'));
  }
  return String(originArg || '').includes('receiver');
};

const parseJsonMessages = (bufferArg: string): { messages: Record<string, unknown>[]; rest: string } => {
  const messages: Record<string, unknown>[] = [];
  const lines = bufferArg.split(/\r?\n/g);
  if (lines.length > 1) {
    for (const line of lines.slice(0, -1)) {
      const parsed = parseJsonObject(line);
      if (parsed) {
        messages.push(parsed);
      }
    }
    return { messages, rest: lines.at(-1) || '' };
  }

  const parsed = parseJsonObject(bufferArg);
  if (parsed) {
    messages.push(parsed);
    return { messages, rest: '' };
  }
  return { messages, rest: bufferArg };
};

const parseJsonObject = (valueArg: string): Record<string, unknown> | undefined => {
  const trimmed = valueArg.trim();
  if (!trimmed) {
    return undefined;
  }
  try {
    const parsed = JSON.parse(trimmed) as unknown;
    return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : undefined;
  } catch {
    return undefined;
  }
};

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

const objectValue = (valueArg: unknown): Record<string, unknown> | undefined => {
  return valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Record<string, unknown> : 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 Math.round(valueArg);
  }
  if (typeof valueArg === 'string' && valueArg.trim() && Number.isFinite(Number(valueArg))) {
    return Math.round(Number(valueArg));
  }
  return undefined;
};

const boolValue = (valueArg: unknown): boolean | undefined => {
  return typeof valueArg === 'boolean' ? valueArg : undefined;
};

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

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