import * as plugins from '../../plugins.js';
import { SimpleLocalClient, SimpleLocalMapper, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult, type TSimpleLocalRawData } from '../../core/index.js';
import { NaswebMapper } from './nasweb.mapper.js';
import type { INaswebConfig, INaswebRawApiData } from './nasweb.types.js';
import { naswebDefaultPort, naswebDefaultTimeoutMs, naswebProfile } from './nasweb.types.js';

export class NaswebClient extends SimpleLocalClient<INaswebConfig> {
  private cachedSnapshot?: ISimpleLocalSnapshot;

  constructor(private readonly configArg: INaswebConfig) {
    super(naswebProfile, configArg);
  }

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

    if (this.hasEndpoint() && !this.configArg.client && !this.configArg.snapshotProvider && (forceRefreshArg || !this.configArg.snapshot)) {
      try {
        this.cachedSnapshot = NaswebMapper.toSnapshot({ config: this.configArg, rawData: await this.fetchRawData(), online: true, source: 'http' });
      } catch (errorArg) {
        this.cachedSnapshot = NaswebMapper.offlineSnapshot(this.configArg, errorMessage(errorArg));
      }
      return SimpleLocalMapper.clone(this.cachedSnapshot);
    }

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

  public async refresh(): Promise<IServiceCallResult> {
    this.cachedSnapshot = undefined;
    const liveCapable = this.hasEndpoint() || Boolean(this.configArg.client || this.configArg.snapshotProvider);
    const snapshot = await this.getSnapshot(liveCapable);
    const success = snapshot.online && !snapshot.error && (liveCapable || this.hasManualData());
    return { success, error: success ? undefined : snapshot.error || 'NASweb refresh requires a local REST host, injected client, snapshotProvider, snapshot, entities, or rawData.', data: snapshot };
  }

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

    try {
      const data = await this.executeNative(requestArg);
      if (data === false) {
        return { success: false, error: `NASweb service ${requestArg.domain}.${requestArg.service} returned a non-OK response.`, data };
      }
      this.cachedSnapshot = undefined;
      return { success: true, data };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

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

  public applyStatusUpdate(statusArg: Record<string, unknown>): ISimpleLocalSnapshot {
    const rawData: INaswebRawApiData = {
      deviceInfo: this.cachedDeviceInfo(),
      status: statusArg,
      fetchedAt: new Date().toISOString(),
    };
    this.cachedSnapshot = NaswebMapper.toSnapshot({ config: this.configArg, rawData, online: true, source: 'runtime' });
    return SimpleLocalMapper.clone(this.cachedSnapshot);
  }

  public async statusSubscription(addressArg: string, subscribeArg: boolean): Promise<boolean> {
    return await this.regularRequest('/rest/integration/hass/statusSubscription', {
      address: addressArg,
      subscribe: subscribeArg,
    });
  }

  private async fetchRawData(): Promise<INaswebRawApiData> {
    const hello = await this.requestText('/rest/hello/world');
    if (hello !== 'restAPI') {
      throw new Error(`NASweb check_connection returned ${hello || 'an empty response'}.`);
    }
    const deviceInfo = await this.requestJson<Record<string, unknown>>('/rest/integration/hass/getDeviceInfo', this.authPayload());
    const status = objectValue(this.configArg.statusUpdate) || objectValue(this.configArg.rawData);
    const subscription = this.configArg.webhookUrl ? await this.statusSubscription(this.configArg.webhookUrl, true) : undefined;
    return {
      deviceInfo: {
        devName: stringValue(deviceInfo.devName),
        devSN: stringValue(deviceInfo.devSN),
      },
      status,
      subscription,
      fetchedAt: new Date().toISOString(),
    };
  }

  private async executeNative(requestArg: IServiceCallRequest): Promise<unknown> {
    const data = requestArg.data || {};
    if (requestArg.service === 'turn_on' || requestArg.service === 'turn_off') {
      const index = this.indexFromRequest(requestArg);
      return await this.regularRequest('/rest/integration/hass/setOutput', { index, status: requestArg.service === 'turn_on' });
    }
    if (requestArg.service === 'toggle') {
      const index = this.indexFromRequest(requestArg);
      const snapshot = await this.getSnapshot();
      const entity = snapshot.entities.find((entityArg) => entityArg.id === `output_${index}`);
      const current = typeof entity?.state === 'boolean' ? entity.state : boolValue(entity?.state);
      return await this.regularRequest('/rest/integration/hass/setOutput', { index, status: !current });
    }
    if (requestArg.service === 'set_hvac_mode') {
      const hvacMode = stringValue(data.hvac_mode ?? data.hvacMode ?? data.mode);
      if (!hvacMode) {
        throw new Error('NASweb set_hvac_mode requires data.hvac_mode.');
      }
      return await this.regularRequest('/rest/integration/hass/thermostat', {
        action: 'set_hvac_mode',
        below: hvacMode.includes('heat'),
        above: hvacMode.includes('cool'),
      });
    }
    if (requestArg.service === 'set_temperature') {
      const tempMin = numberValue(data.target_temp_low ?? data.temp_min ?? data.tempMin);
      const tempMax = numberValue(data.target_temp_high ?? data.temp_max ?? data.tempMax);
      if (tempMin === undefined || tempMax === undefined) {
        throw new Error('NASweb set_temperature requires data.target_temp_low and data.target_temp_high.');
      }
      return await this.regularRequest('/rest/integration/hass/thermostat', {
        action: 'set_temp_range',
        temp_min: tempMin,
        temp_max: tempMax,
      });
    }
    if (requestArg.service === 'alarm_arm_away' || requestArg.service === 'alarm_disarm') {
      const index = this.indexFromRequest(requestArg);
      const passcode = stringValue(data.code ?? data.passcode);
      return await this.regularRequest('/rest/integration/hass/armZone', {
        index,
        status: requestArg.service === 'alarm_arm_away',
        passcode: passcode ? sha1(passcode) : '',
      });
    }
    if (requestArg.service === 'status_subscription') {
      const address = stringValue(data.address ?? data.webhookUrl ?? this.configArg.webhookUrl);
      if (!address) {
        throw new Error('NASweb status_subscription requires data.address or config.webhookUrl.');
      }
      return await this.statusSubscription(address, data.subscribe !== false);
    }
    throw new Error(`Unsupported NASweb service: ${requestArg.domain}.${requestArg.service}`);
  }

  private async regularRequest(pathArg: string, dataArg: Record<string, unknown>): Promise<boolean> {
    const response = await this.requestJson<Record<string, unknown>>(pathArg, { ...this.authPayload(), ...dataArg });
    return response.Answer === 'OK';
  }

  private async requestJson<TValue>(pathArg: string, dataArg?: Record<string, unknown>): Promise<TValue> {
    const text = await this.requestText(pathArg, dataArg);
    try {
      return JSON.parse(text) as TValue;
    } catch (errorArg) {
      throw new Error(`NASweb JSON parse failed for ${pathArg}: ${errorMessage(errorArg)}`);
    }
  }

  private async requestText(pathArg: string, dataArg?: Record<string, unknown>): Promise<string> {
    const response = await this.request(pathArg, dataArg);
    if (response.statusCode === 401 || response.body === 'Not Authorized') {
      throw new Error('NASweb local authentication failed.');
    }
    if (response.statusCode !== 200) {
      throw new Error(`NASweb request ${pathArg} failed with ${response.statusCode}: ${response.body.slice(0, 160)}`);
    }
    return response.body;
  }

  private async request(pathArg: string, dataArg?: Record<string, unknown>): Promise<IHttpResponse> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('NASweb local REST API requires config.host or config.url.');
    }
    const body = dataArg === undefined ? '' : JSON.stringify(dataArg);
    const request = [
      `POST ${pathArg.startsWith('/') ? pathArg : `/${pathArg}`} HTTP/1.1`,
      `Host: ${endpoint.host}`,
      'Connection: close',
      'Accept: application/json, text/plain;q=0.9, */*;q=0.8',
      body ? 'Content-Type: application/json' : undefined,
      `Content-Length: ${Buffer.byteLength(body)}`,
      '',
      body,
    ].filter((lineArg) => lineArg !== undefined).join('\r\n');
    const timeoutMs = this.configArg.timeoutMs || naswebDefaultTimeoutMs;

    return new Promise<IHttpResponse>((resolve, reject) => {
      const chunks: Buffer[] = [];
      let settled = false;
      const socket = endpoint.useTls
        ? plugins.tls.connect({ host: endpoint.host, port: endpoint.port, servername: endpoint.host, rejectUnauthorized: this.configArg.rejectUnauthorized ?? false })
        : 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;
        }
        try {
          resolve(parseHttpResponse(Buffer.concat(chunks).toString('utf8')));
        } catch (parseErrorArg) {
          reject(parseErrorArg);
        }
      };

      socket.setTimeout(timeoutMs, () => finish(new Error(`NASweb request to ${endpoint.host}:${endpoint.port} timed out after ${timeoutMs}ms.`)));
      if (endpoint.useTls) {
        socket.on('secureConnect', () => socket.write(request));
      } else {
        socket.on('connect', () => socket.write(request));
      }
      socket.on('data', (chunkArg) => chunks.push(Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg)));
      socket.on('end', () => finish());
      socket.on('close', () => finish());
      socket.on('error', (errorArg) => finish(errorArg));
    });
  }

  private authPayload(): Record<string, unknown> {
    return {
      login: this.configArg.username || '',
      password: this.configArg.password === undefined ? undefined : sha1(this.configArg.password),
    };
  }

  private indexFromRequest(requestArg: IServiceCallRequest): number {
    const data = requestArg.data || {};
    const index = numberValue(data.index ?? data.output ?? data.zone);
    if (index !== undefined) {
      return index;
    }
    const entityId = stringValue(requestArg.target.entityId);
    const match = entityId?.match(/(?:output|zone|relay_switch)[_.-]?(\d+)$/i);
    if (match) {
      return Number(match[1]);
    }
    throw new Error(`NASweb ${requestArg.service} requires data.index or a target entityId ending with an index.`);
  }

  private endpoint(): INaswebEndpoint | undefined {
    const rawUrl = stringValue(this.configArg.url) || stringValue(this.configArg.host);
    if (!rawUrl) {
      return undefined;
    }
    try {
      const defaultProtocol = this.configArg.useTls === false ? 'http' : 'https';
      const url = new URL(rawUrl.includes('://') ? rawUrl : `${defaultProtocol}://${rawUrl}`);
      const useTls = url.protocol === 'https:';
      return { host: url.hostname, port: this.configArg.port || numberValue(url.port) || (useTls ? naswebDefaultPort : 80), useTls };
    } catch {
      return { host: rawUrl, port: this.configArg.port || (this.configArg.useTls === false ? 80 : naswebDefaultPort), useTls: this.configArg.useTls !== false };
    }
  }

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

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

  private cachedDeviceInfo(): INaswebRawApiData['deviceInfo'] | undefined {
    const rawObject = objectValue(this.cachedSnapshot?.rawData);
    return objectValue(rawObject?.deviceInfo) as INaswebRawApiData['deviceInfo'] | undefined;
  }
}

interface INaswebEndpoint {
  host: string;
  port: number;
  useTls: boolean;
}

interface IHttpResponse {
  statusCode: number;
  headers: Record<string, string>;
  body: string;
}

const parseHttpResponse = (valueArg: string): IHttpResponse => {
  const headerEnd = valueArg.indexOf('\r\n\r\n');
  if (headerEnd < 0) {
    throw new Error('NASweb returned an invalid HTTP response.');
  }
  const headerText = valueArg.slice(0, headerEnd);
  const body = valueArg.slice(headerEnd + 4);
  const lines = headerText.split('\r\n');
  const statusCode = Number(lines[0]?.split(/\s+/)[1]);
  const headers: Record<string, string> = {};
  for (const line of lines.slice(1)) {
    const separator = line.indexOf(':');
    if (separator > 0) {
      headers[line.slice(0, separator).trim().toLowerCase()] = line.slice(separator + 1).trim();
    }
  }
  return { statusCode, headers, body: headers['transfer-encoding'] === 'chunked' ? decodeChunkedBody(body) : body };
};

const decodeChunkedBody = (bodyArg: string): string => {
  let offset = 0;
  let result = '';
  while (offset < bodyArg.length) {
    const lineEnd = bodyArg.indexOf('\r\n', offset);
    if (lineEnd < 0) {
      break;
    }
    const length = Number.parseInt(bodyArg.slice(offset, lineEnd), 16);
    if (!Number.isFinite(length) || length <= 0) {
      break;
    }
    const chunkStart = lineEnd + 2;
    result += bodyArg.slice(chunkStart, chunkStart + length);
    offset = chunkStart + length + 2;
  }
  return result;
};

const sha1 = (valueArg: string): string => {
  return plugins.crypto.createHash('sha1').update(valueArg, 'utf8').digest('hex').toUpperCase();
};

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 valueArg;
  }
  if (typeof valueArg === 'string' && valueArg.trim()) {
    const parsed = Number(valueArg.trim());
    return Number.isFinite(parsed) ? parsed : undefined;
  }
  return undefined;
};

const boolValue = (valueArg: unknown): boolean | undefined => {
  if (typeof valueArg === 'boolean') {
    return valueArg;
  }
  const value = stringValue(valueArg)?.toLowerCase();
  if (value === 'true' || value === '1' || value === 'on') {
    return true;
  }
  if (value === 'false' || value === '0' || value === 'off') {
    return false;
  }
  return undefined;
};

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