import { SimpleLocalClient, SimpleLocalMapper, type IServiceCallRequest, type IServiceCallResult, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot } from '../../core/index.js';
import type { INetgearLteConfig } from './netgear_lte.types.js';
import { netgearLteProfile } from './netgear_lte.types.js';

const failoverModes: Record<string, string> = { auto: 'Auto', wire: 'WAN', mobile: 'LTE' };
const autoconnectModes: Record<string, string> = { never: 'Never', home: 'HomeNetwork', always: 'Always' };
const sensitiveFlattenedKeys = new Set(['webd.adminpassword', 'session.sectoken', 'wifi.guest.passphrase', 'wifi.passphrase']);

export class NetgearLteClient extends SimpleLocalClient<INetgearLteConfig> {
  private liveSnapshot?: ISimpleLocalSnapshot;
  private token?: string;
  private cookies: Record<string, string> = {};

  constructor(configArg: INetgearLteConfig) {
    super(netgearLteProfile, configArg);
    this.lteConfig = configArg;
  }

  private readonly lteConfig: INetgearLteConfig;

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

    try {
      const data = await this.readModelData();
      this.liveSnapshot = this.snapshotFromModelData(data, true);
      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.baseUrl()) {
      return super.execute(requestArg);
    }

    try {
      if (requestArg.service === 'connect_lte') {
        await this.configCall('wwan.connect', 'DefaultProfile');
        return { success: true, data: { connected: true } };
      }
      if (requestArg.service === 'disconnect_lte') {
        await this.configCall('wwan.connect', 'Disconnect');
        return { success: true, data: { connected: false } };
      }
      if (requestArg.service === 'delete_sms') {
        const ids = smsIds(requestArg.data?.sms_id ?? requestArg.data?.smsId);
        if (!ids.length) {
          return { success: false, error: 'NETGEAR LTE delete_sms requires request.data.sms_id.' };
        }
        for (const id of ids) {
          await this.configCall('sms.deleteId', String(id));
        }
        return { success: true, data: { deletedSmsIds: ids } };
      }
      if (requestArg.service === 'set_option') {
        const changed: Record<string, string> = {};
        const failover = stringValue(requestArg.data?.failover);
        if (failover) {
          if (!(failover in failoverModes)) {
            return { success: false, error: `Unsupported NETGEAR LTE failover mode: ${failover}` };
          }
          await this.configCall('failover.mode', failoverModes[failover]);
          changed.failover = failover;
        }
        const autoconnect = stringValue(requestArg.data?.autoconnect);
        if (autoconnect) {
          if (!(autoconnect in autoconnectModes)) {
            return { success: false, error: `Unsupported NETGEAR LTE autoconnect mode: ${autoconnect}` };
          }
          await this.configCall('wwan.autoconnect', autoconnectModes[autoconnect]);
          changed.autoconnect = autoconnect;
        }
        if (!Object.keys(changed).length) {
          return { success: false, error: 'NETGEAR LTE set_option requires request.data.failover or request.data.autoconnect.' };
        }
        return { success: true, data: changed };
      }
      if (['send_message', 'sms'].includes(requestArg.service)) {
        const message = stringValue(requestArg.data?.message);
        const targets = recipientValues(requestArg.data?.target ?? requestArg.data?.targets ?? requestArg.data?.recipient);
        if (!message || !targets.length) {
          return { success: false, error: 'NETGEAR LTE send_message requires request.data.message and request.data.target.' };
        }
        for (const target of targets) {
          await this.sendSms(target, message);
        }
        return { success: true, data: { targets } };
      }
      return { success: false, error: `Unsupported NETGEAR LTE service: ${requestArg.service}` };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  private async readModelData(): Promise<Record<string, unknown>> {
    if (this.lteConfig.password) {
      await this.ensureLoggedIn();
    }
    return this.requestJson('model.json');
  }

  private async ensureLoggedIn(): Promise<void> {
    if (this.token) {
      return;
    }
    await this.login();
  }

  private async login(): Promise<void> {
    const password = this.lteConfig.password;
    if (!password) {
      throw new Error('NETGEAR LTE login requires config.password.');
    }
    this.token = undefined;
    this.cookies = {};
    const data = await this.requestJson('model.json');
    const token = stringValue(record(data.session).secToken);
    if (!token) {
      throw new Error('NETGEAR LTE login did not return a session token.');
    }
    this.token = token;
    await this.postForm('Forms/config', {
      'session.password': password,
      token,
    });
  }

  private async configCall(keyArg: string, valueArg: string): Promise<void> {
    await this.ensureLoggedIn();
    const response = await this.postForm('Forms/config', {
      [keyArg]: valueArg,
      err_redirect: '/error.json',
      ok_redirect: '/success.json',
      token: this.token || '',
    });
    const text = await response.text().catch(() => '');
    if (text.toLowerCase().includes('error')) {
      throw new Error(`NETGEAR LTE config call ${keyArg} returned an error response.`);
    }
  }

  private async sendSms(phoneArg: string, messageArg: string): Promise<void> {
    await this.ensureLoggedIn();
    await this.postForm('Forms/smsSendMsg', {
      'sms.sendMsg.receiver': phoneArg,
      'sms.sendMsg.text': messageArg,
      'sms.sendMsg.clientId': 'smarthome.exchange',
      action: 'send',
      token: this.token || '',
    });
  }

  private async requestJson(pathArg: string): Promise<Record<string, unknown>> {
    const response = await this.fetchModem(pathArg, { method: 'GET' });
    const text = await response.text();
    try {
      const parsed = JSON.parse(text) as unknown;
      if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
        return parsed as Record<string, unknown>;
      }
    } catch {}
    throw new Error(`NETGEAR LTE returned invalid JSON from ${pathArg}.`);
  }

  private async postForm(pathArg: string, dataArg: Record<string, string>): Promise<Response> {
    return this.fetchModem(pathArg, {
      method: 'POST',
      body: new URLSearchParams(dataArg) as unknown as BodyInit,
    });
  }

  private async fetchModem(pathArg: string, initArg: RequestInit): Promise<Response> {
    const baseUrl = this.baseUrl();
    if (!baseUrl) {
      throw new Error('NETGEAR LTE HTTP client requires config.host or config.url.');
    }
    const headers = new Headers(initArg.headers);
    const cookie = Object.entries(this.cookies).map(([key, value]) => `${key}=${value}`).join('; ');
    if (cookie) {
      headers.set('cookie', cookie);
    }
    const response = await this.fetchWithTimeout(`${baseUrl}/${pathArg.replace(/^\/+/, '')}`, { ...initArg, headers });
    this.storeCookies(response.headers);
    if (!response.ok) {
      const text = await response.text().catch(() => '');
      throw new Error(`NETGEAR LTE request ${pathArg} failed with HTTP ${response.status}${text ? `: ${text}` : ''}`);
    }
    return response;
  }

  private async fetchWithTimeout(urlArg: string, initArg: RequestInit): Promise<Response> {
    const abortController = new AbortController();
    const timeout = setTimeout(() => abortController.abort(), this.lteConfig.timeoutMs || 5000);
    try {
      return await globalThis.fetch(urlArg, { ...initArg, signal: abortController.signal });
    } finally {
      clearTimeout(timeout);
    }
  }

  private storeCookies(headersArg: Headers): void {
    const headersWithCookies = headersArg as Headers & { getSetCookie?: () => string[] };
    const values = headersWithCookies.getSetCookie?.() || splitSetCookie(headersArg.get('set-cookie'));
    for (const value of values) {
      const pair = value.split(';')[0];
      const equalsIndex = pair.indexOf('=');
      if (equalsIndex > 0) {
        this.cookies[pair.slice(0, equalsIndex)] = pair.slice(equalsIndex + 1);
      }
    }
  }

  private snapshotFromModelData(dataArg: Record<string, unknown>, onlineArg: boolean): ISimpleLocalSnapshot {
    const general = record(dataArg.general);
    const wwan = record(dataArg.wwan);
    const wwanDataUsage = record(record(wwan.dataUsage).generic);
    const wwanAdv = record(dataArg.wwanadv);
    const failover = record(dataArg.failover);
    const smsMessages = smsMessagesFromData(dataArg);
    const unreadSms = smsMessages.filter((messageArg) => messageArg.unread).length;
    const host = this.lteConfig.host;
    const port = this.lteConfig.port || netgearLteProfile.defaultPort || 80;
    const items = flatten(dataArg);

    const sensorValues: Array<[string, string, unknown, string | undefined, string | undefined]> = [
      ['sms', 'Unread SMS', unreadSms, 'unread', undefined],
      ['sms_total', 'SMS total', smsMessages.length, 'messages', undefined],
      ['usage', 'Usage', numberValue(wwanDataUsage.dataTransferred), 'B', 'data_size'],
      ['radio_quality', 'Radio quality', numberValue(wwanAdv.radioQuality), '%', undefined],
      ['rx_level', 'RX level', numberValue(wwanAdv.rxLevel), 'dBm', 'signal_strength'],
      ['tx_level', 'TX level', numberValue(wwanAdv.txLevel), 'dBm', 'signal_strength'],
      ['upstream', 'Upstream', failover.backhaul, undefined, undefined],
      ['connection_text', 'Connection text', wwan.connectionText, undefined, undefined],
      ['connection_type', 'Connection type', wwan.connectionType, undefined, undefined],
      ['current_ps_service_type', 'Service type', wwan.currentPSserviceType, undefined, undefined],
      ['register_network_display', 'Register network display', wwan.registerNetworkDisplay, undefined, undefined],
      ['current_band', 'Band', wwanAdv.curBand, undefined, undefined],
      ['cell_id', 'Cell ID', wwanAdv.cellId, undefined, undefined],
    ];

    const entities: ISimpleLocalEntitySnapshot[] = sensorValues.map(([id, name, state, unit, deviceClass]) => ({
      id,
      name,
      platform: 'sensor',
      state,
      unit,
      deviceClass,
      available: onlineArg,
      attributes: { source: 'model.json' },
    }));

    entities.push(
      {
        id: 'roaming',
        name: 'Roaming',
        platform: 'binary_sensor',
        state: booleanValue(wwan.roaming),
        available: onlineArg,
        attributes: { source: 'model.json' },
      },
      {
        id: 'wire_connected',
        name: 'Wire connected',
        platform: 'binary_sensor',
        state: booleanValue(failover.wanConnected),
        available: onlineArg,
        deviceClass: 'connectivity',
        attributes: { source: 'model.json' },
      },
      {
        id: 'mobile_connected',
        name: 'Mobile connected',
        platform: 'binary_sensor',
        state: wwan.connection === 'Connected',
        available: onlineArg,
        deviceClass: 'connectivity',
        attributes: { source: 'model.json' },
      },
    );

    return {
      device: {
        id: this.lteConfig.uniqueId || stringValue(general.FSN) || (host ? `${host}:${port}` : netgearLteProfile.domain),
        name: this.lteConfig.name || stringValue(general.deviceName) || stringValue(general.devicename) || netgearLteProfile.defaultName || netgearLteProfile.displayName,
        manufacturer: 'Netgear',
        model: stringValue(general.model) || netgearLteProfile.model,
        serialNumber: stringValue(general.FSN),
        host,
        port,
        protocol: this.lteConfig.useTls ? 'https' : 'http',
        configurationUrl: this.baseUrl(),
        attributes: { items },
      },
      entities,
      online: onlineArg,
      updatedAt: new Date().toISOString(),
      source: 'http',
      rawData: { items, sms: smsMessages },
      capabilities: { localRead: true, localControl: true },
    };
  }

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

  private baseUrl(): string | undefined {
    if (this.lteConfig.url) {
      return this.lteConfig.url.replace(/\/+$/, '');
    }
    const host = this.lteConfig.host;
    if (!host) {
      return undefined;
    }
    const protocol = this.lteConfig.useTls ? 'https' : 'http';
    const port = this.lteConfig.port || netgearLteProfile.defaultPort || 80;
    const portPart = (port === 80 && protocol === 'http') || (port === 443 && protocol === 'https') ? '' : `:${port}`;
    return `${protocol}://${host}${portPart}`;
  }
}

const record = (valueArg: unknown): Record<string, unknown> => valueArg && typeof valueArg === 'object' && !Array.isArray(valueArg) ? valueArg as Record<string, unknown> : {};

const stringValue = (valueArg: unknown): string | undefined => 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() && Number.isFinite(Number(valueArg))) {
    return Number(valueArg);
  }
  return undefined;
};

const booleanValue = (valueArg: unknown): boolean => valueArg === true || valueArg === 1 || valueArg === '1' || valueArg === 'true' || valueArg === 'True' || valueArg === 'Connected';

const smsIds = (valueArg: unknown): number[] => {
  const values = Array.isArray(valueArg) ? valueArg : [valueArg];
  return values
    .map((itemArg) => typeof itemArg === 'number' ? itemArg : typeof itemArg === 'string' ? Number(itemArg) : Number.NaN)
    .filter((itemArg) => Number.isInteger(itemArg) && itemArg > 0);
};

const recipientValues = (valueArg: unknown): string[] => {
  const values = Array.isArray(valueArg) ? valueArg : [valueArg];
  return values.map((itemArg) => stringValue(itemArg)).filter((itemArg): itemArg is string => Boolean(itemArg));
};

const smsMessagesFromData = (dataArg: Record<string, unknown>): Array<{ id?: number; timestamp?: string; unread: boolean; sender?: string; message?: string }> => {
  const rawMessages = record(dataArg.sms).msgs;
  const list = Array.isArray(rawMessages) ? rawMessages : [];
  return list
    .map((itemArg) => record(itemArg))
    .filter((itemArg) => 'text' in itemArg)
    .map((itemArg) => ({
      id: numberValue(itemArg.id),
      timestamp: stringValue(itemArg.rxTime),
      unread: itemArg.read === false,
      sender: stringValue(itemArg.sender),
      message: stringValue(itemArg.text),
    }));
};

const flatten = (valueArg: unknown, pathArg = ''): Record<string, unknown> => {
  const result: Record<string, unknown> = {};
  if (Array.isArray(valueArg)) {
    valueArg.forEach((itemArg, indexArg) => Object.assign(result, flatten(itemArg, `${pathArg}.${indexArg}`.replace(/^\./, ''))));
    return result;
  }
  if (valueArg && typeof valueArg === 'object') {
    for (const [key, value] of Object.entries(valueArg)) {
      const path = `${pathArg}.${key.toLowerCase()}`.replace(/^\./, '');
      Object.assign(result, flatten(value, path));
    }
    return result;
  }
  if (pathArg && (valueArg === null || ['string', 'number', 'boolean'].includes(typeof valueArg)) && !sensitiveFlattenedKeys.has(pathArg)) {
    result[pathArg] = valueArg;
  }
  return result;
};

const splitSetCookie = (valueArg: string | null): string[] => valueArg ? [valueArg] : [];

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