import * as plugins from '../../plugins.js';
import { SimpleLocalClient, type ISimpleLocalEntitySnapshot, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult, type TSimpleLocalRawData } from '../../core/index.js';
import { KostalPlenticoreMapper } from './kostal_plenticore.mapper.js';
import type { IKostalPlenticoreConfig, IKostalPlenticoreProcessDataItem, IKostalPlenticoreRawData, IKostalPlenticoreVersionData } from './kostal_plenticore.types.js';
import { kostalPlenticoreDefaultPort, kostalPlenticoreDefaultTimeoutMs, kostalPlenticoreProfile } from './kostal_plenticore.types.js';

export class KostalPlenticoreApiError extends Error {}
export class KostalPlenticoreAuthenticationError extends KostalPlenticoreApiError {}
export class KostalPlenticoreConnectionError extends KostalPlenticoreApiError {}
export class KostalPlenticoreCommandError extends KostalPlenticoreApiError {}

const defaultProcessDataRequest: Record<string, string[]> = {
  'devices:local': ['Inverter:State', 'Dc_P', 'Grid_P', 'HomeBat_P', 'HomeGrid_P', 'HomeOwn_P', 'HomePv_P', 'Home_P', 'PV2Bat_P', 'EM_State'],
  'devices:local:ac': ['P'],
  'devices:local:pv1': ['P', 'U', 'I'],
  'devices:local:pv2': ['P', 'U', 'I'],
  'devices:local:pv3': ['P', 'U', 'I'],
  'devices:local:battery': ['Cycles', 'P', 'SoC'],
  'scb:event': ['Event:ActiveErrorCnt'],
};

const defaultSettingsRequest: Record<string, string[]> = {
  'devices:local': [
    'Properties:SerialNo',
    'Branding:ProductName1',
    'Branding:ProductName2',
    'Properties:VersionIOC',
    'Properties:VersionMC',
    'Battery:Strategy',
    'Battery:ManualCharge',
    'Battery:MinSoc',
    'Battery:MinHomeComsumption',
    'Inverter:ActivePowerLimitation',
    'Battery:SmartBatteryControl:Enable',
    'Battery:TimeControl:Enable',
  ],
  'scb:network': ['Network:Hostname', 'Hostname'],
};

export class KostalPlenticoreClient extends SimpleLocalClient<IKostalPlenticoreConfig> {
  private currentKostalSnapshot?: ISimpleLocalSnapshot;
  private sessionId?: string;

  constructor(private readonly kostalConfig: IKostalPlenticoreConfig) {
    super(kostalPlenticoreProfile, kostalConfig);
    this.sessionId = kostalConfig.sessionId;
  }

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

    if (!forceRefreshArg && this.kostalConfig.snapshot) {
      this.currentKostalSnapshot = KostalPlenticoreMapper.toSnapshot({ config: this.kostalConfig, source: 'snapshot', online: this.kostalConfig.snapshot.online });
      return clone(this.currentKostalSnapshot);
    }

    if (this.kostalConfig.client) {
      try {
        this.currentKostalSnapshot = await this.snapshotFromInjectedClient();
      } catch (errorArg) {
        this.currentKostalSnapshot = KostalPlenticoreMapper.offlineSnapshot(this.kostalConfig, errorMessage(errorArg));
      }
      return clone(this.currentKostalSnapshot);
    }

    if (this.kostalConfig.snapshotProvider) {
      try {
        const value = await this.kostalConfig.snapshotProvider();
        if (value !== undefined) {
          this.currentKostalSnapshot = isSimpleLocalSnapshot(value)
            ? KostalPlenticoreMapper.toSnapshot({ config: { ...this.kostalConfig, snapshot: value }, source: 'provider', online: value.online })
            : KostalPlenticoreMapper.toSnapshot({ config: this.kostalConfig, rawData: value as TSimpleLocalRawData, source: 'provider', online: true });
          return clone(this.currentKostalSnapshot);
        }
      } catch (errorArg) {
        this.currentKostalSnapshot = KostalPlenticoreMapper.offlineSnapshot(this.kostalConfig, errorMessage(errorArg));
        return clone(this.currentKostalSnapshot);
      }
    }

    if (this.hasLiveTarget()) {
      try {
        this.currentKostalSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.currentKostalSnapshot = KostalPlenticoreMapper.offlineSnapshot(this.kostalConfig, errorMessage(errorArg));
      }
      return clone(this.currentKostalSnapshot);
    }

    if (this.endpointHost() && !this.authKey() && !this.sessionId && !this.hasManualData()) {
      this.currentKostalSnapshot = KostalPlenticoreMapper.offlineSnapshot(this.kostalConfig, 'Kostal Plenticore live REST polling requires config.host/config.url plus password or sessionId for the inverter-local /api/v1 API.');
      return clone(this.currentKostalSnapshot);
    }

    if (this.hasManualData()) {
      this.currentKostalSnapshot = KostalPlenticoreMapper.toSnapshot({ config: this.kostalConfig, rawData: this.kostalConfig.rawData ?? this.kostalConfig.rawText, online: this.kostalConfig.online ?? true, source: 'manual' });
      return clone(this.currentKostalSnapshot);
    }

    this.currentKostalSnapshot = KostalPlenticoreMapper.offlineSnapshot(this.kostalConfig, 'Kostal Plenticore snapshots require host/password, injected client, snapshotProvider, snapshot, entities, or rawData.');
    return clone(this.currentKostalSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.currentKostalSnapshot = undefined;
    const liveCapable = Boolean(this.hasLiveTarget() || this.kostalConfig.client);
    const snapshot = await this.getSnapshot(liveCapable);
    const success = snapshot.online && !snapshot.error && (liveCapable || this.hasManualData() || Boolean(this.kostalConfig.snapshot));
    return { success, error: success ? undefined : snapshot.error || 'Kostal Plenticore refresh requires a live host/password or injected client.', data: snapshot };
  }

  public async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (this.kostalConfig.commandExecutor) {
      return normalizeServiceResult(await this.kostalConfig.commandExecutor.execute(requestArg));
    }
    if (this.kostalConfig.client?.execute) {
      return normalizeServiceResult(await this.kostalConfig.client.execute(requestArg));
    }
    if (requestArg.service === 'refresh') {
      return this.refresh();
    }
    if (!this.hasLiveTarget()) {
      return { success: false, error: `Kostal Plenticore service ${requestArg.domain}.${requestArg.service} requires an injected client.execute() or commandExecutor unless config.host/config.url plus password/sessionId and moduleId/dataId/value are supplied. Static snapshots/manual data are read-only.` };
    }

    try {
      const command = await this.commandForService(requestArg);
      if (!command) {
        return { success: false, error: `Unsupported Kostal Plenticore service or missing moduleId/dataId/value: ${requestArg.domain}.${requestArg.service}` };
      }
      await this.setSettingValues(command.moduleId, command.values);
      this.currentKostalSnapshot = undefined;
      return { success: true, data: command };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  public async destroy(): Promise<void> {
    await this.kostalConfig.client?.destroy?.();
    if (this.sessionId && !this.kostalConfig.sessionId) {
      await this.logout().catch(() => undefined);
    }
  }

  public async fetchSnapshot(): Promise<ISimpleLocalSnapshot> {
    await this.ensureSession();
    const [version, processData, settings] = await Promise.all([
      this.getVersion().catch(() => undefined),
      this.getProcessDataValues(this.processDataRequest()),
      this.getSettingValues(this.settingsRequest()).catch(() => ({})),
    ]);
    const rawData: IKostalPlenticoreRawData = {
      version,
      processData,
      settings,
      fetchedAt: new Date().toISOString(),
      source: 'http',
    };
    return KostalPlenticoreMapper.toSnapshot({ config: this.kostalConfig, rawData, online: true, source: 'http' });
  }

  public async getVersion(): Promise<IKostalPlenticoreVersionData> {
    return this.requestJson<IKostalPlenticoreVersionData>('info/version', { session: true });
  }

  public async getProcessDataValues(requestArg: Record<string, string[]>): Promise<IKostalPlenticoreRawData['processData']> {
    await this.ensureSession();
    const request = Object.entries(requestArg).map(([moduleId, processDataIds]) => ({ moduleid: moduleId, processdataids: processDataIds }));
    const response = await this.requestJson<unknown>('processdata', { method: 'POST', body: request, session: true });
    return normalizeProcessDataResponse(response);
  }

  public async getSettingValues(requestArg: Record<string, string[]>): Promise<Record<string, Record<string, string>>> {
    await this.ensureSession();
    const request = Object.entries(requestArg).map(([moduleId, settingIds]) => ({ moduleid: moduleId, settingids: settingIds }));
    const response = await this.requestJson<unknown>('settings', { method: 'POST', body: request, session: true });
    return normalizeSettingsResponse(response);
  }

  public async setSettingValues(moduleIdArg: string, valuesArg: Record<string, string>): Promise<void> {
    await this.ensureSession();
    const request = [{ moduleid: moduleIdArg, settings: Object.entries(valuesArg).map(([id, value]) => ({ id, value })) }];
    await this.requestJson<unknown>('settings', { method: 'PUT', body: request, session: true });
  }

  private async snapshotFromInjectedClient(): Promise<ISimpleLocalSnapshot> {
    const client = this.kostalConfig.client;
    if (!client) {
      throw new Error('Kostal Plenticore injected client is not configured.');
    }
    const value = client.getSnapshot ? await client.getSnapshot() : client.getRawData ? await client.getRawData() : client.getState ? await client.getState() : undefined;
    if (value === undefined) {
      throw new Error('Kostal Plenticore injected client must expose getSnapshot(), getRawData(), or getState().');
    }
    if (isSimpleLocalSnapshot(value)) {
      return KostalPlenticoreMapper.toSnapshot({ config: { ...this.kostalConfig, snapshot: value }, source: 'client', online: value.online });
    }
    return KostalPlenticoreMapper.toSnapshot({ config: this.kostalConfig, rawData: value as TSimpleLocalRawData, source: 'client', online: true });
  }

  private async commandForService(requestArg: IServiceCallRequest): Promise<{ moduleId: string; values: Record<string, string> } | undefined> {
    const directModuleId = stringValue(requestArg.data?.moduleId) || stringValue(requestArg.data?.module_id);
    const directDataId = stringValue(requestArg.data?.dataId) || stringValue(requestArg.data?.data_id) || stringValue(requestArg.data?.settingId) || stringValue(requestArg.data?.setting_id);
    if (directModuleId && directDataId) {
      const value = this.valueForService(requestArg, undefined);
      return value === undefined ? undefined : { moduleId: directModuleId, values: { [directDataId]: String(value) } };
    }

    const snapshot = await this.getSnapshot();
    const entity = KostalPlenticoreMapper.commandEntity(snapshot, requestArg.target.entityId);
    if (!entity?.attributes) {
      return undefined;
    }
    const moduleId = stringValue(entity.attributes.moduleId);
    const dataId = stringValue(entity.attributes.dataId);
    const optionSettingIds = stringArray(entity.attributes.optionSettingIds);
    if (requestArg.service === 'select_option' && moduleId && optionSettingIds.length) {
      const option = stringValue(requestArg.data?.option) || stringValue(requestArg.data?.option_name) || stringValue(requestArg.data?.value);
      if (!option) {
        return undefined;
      }
      const values = Object.fromEntries(optionSettingIds.map((dataIdArg) => [dataIdArg, '0']));
      if (option !== 'None') {
        values[option] = '1';
      }
      return { moduleId, values };
    }
    const value = this.valueForService(requestArg, entity);
    return moduleId && dataId && value !== undefined ? { moduleId, values: { [dataId]: String(value) } } : undefined;
  }

  private valueForService(requestArg: IServiceCallRequest, entityArg?: ISimpleLocalEntitySnapshot): unknown {
    if (requestArg.service === 'set_value') {
      return requestArg.data?.value;
    }
    if (requestArg.service === 'select_option') {
      return requestArg.data?.option ?? requestArg.data?.value;
    }
    if (requestArg.service === 'turn_on') {
      return entityArg?.attributes?.onValue ?? requestArg.data?.value ?? '1';
    }
    if (requestArg.service === 'turn_off') {
      return entityArg?.attributes?.offValue ?? requestArg.data?.value ?? '0';
    }
    if (requestArg.service === 'toggle') {
      if (typeof entityArg?.state === 'boolean') {
        return entityArg.state ? entityArg.attributes?.offValue ?? '0' : entityArg.attributes?.onValue ?? '1';
      }
      return requestArg.data?.value;
    }
    return requestArg.data?.value;
  }

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

  private async login(): Promise<void> {
    const key = this.authKey();
    if (!key) {
      throw new KostalPlenticoreAuthenticationError('Kostal Plenticore login requires config.password or config.apiKey.');
    }
    const serviceCode = this.serviceCode();
    const user = serviceCode ? 'master' : 'user';
    const clientNonce = plugins.crypto.randomBytes(12);
    const startResponse = await this.requestJson<Record<string, unknown>>('auth/start', {
      method: 'POST',
      body: { username: user, nonce: b64(clientNonce) },
      session: false,
    });
    const serverNonce = b64decode(requiredString(startResponse.nonce, 'auth/start nonce'));
    const transactionId = b64decode(requiredString(startResponse.transactionId, 'auth/start transactionId'));
    const salt = b64decode(requiredString(startResponse.salt, 'auth/start salt'));
    const rounds = requiredNumber(startResponse.rounds, 'auth/start rounds');

    const saltedPassword = plugins.crypto.pbkdf2Sync(Buffer.from(key, 'utf8'), salt, rounds, 32, 'sha256');
    const clientKey = hmac(saltedPassword, 'Client Key');
    const storedKey = sha256(clientKey);
    const authMessage = `n=${user},r=${b64(clientNonce)},r=${b64(serverNonce)},s=${b64(salt)},i=${rounds},c=biws,r=${b64(serverNonce)}`;
    const clientSignature = hmac(storedKey, authMessage);
    const clientProof = xor(clientKey, clientSignature);
    const serverKey = hmac(saltedPassword, 'Server Key');
    const serverSignature = hmac(serverKey, authMessage);

    const finishResponse = await this.requestJson<Record<string, unknown>>('auth/finish', {
      method: 'POST',
      body: { transactionId: b64(transactionId), proof: b64(clientProof) },
      session: false,
    });
    const returnedSignature = b64decode(requiredString(finishResponse.signature, 'auth/finish signature'));
    if (returnedSignature.length !== serverSignature.length || !plugins.crypto.timingSafeEqual(returnedSignature, serverSignature)) {
      throw new KostalPlenticoreAuthenticationError('Kostal Plenticore server signature mismatch.');
    }
    let token = requiredString(finishResponse.token, 'auth/finish token');
    if (serviceCode) {
      token = `${token}:${serviceCode}`;
    }

    const sessionKeyHmac = plugins.crypto.createHmac('sha256', storedKey);
    sessionKeyHmac.update(Buffer.from('Session Key', 'utf8'));
    sessionKeyHmac.update(Buffer.from(authMessage, 'utf8'));
    sessionKeyHmac.update(clientKey);
    const protocolKey = sessionKeyHmac.digest();
    const sessionNonce = plugins.crypto.randomBytes(16);
    const cipher = plugins.crypto.createCipheriv('aes-256-gcm', protocolKey, sessionNonce);
    const cipherText = Buffer.concat([cipher.update(token, 'utf8'), cipher.final()]);
    const authTag = cipher.getAuthTag();

    const sessionResponse = await this.requestJson<Record<string, unknown>>('auth/create_session', {
      method: 'POST',
      body: {
        iv: b64(sessionNonce),
        tag: b64(authTag),
        transactionId: b64(transactionId),
        payload: b64(cipherText),
      },
      session: false,
    });
    this.sessionId = requiredString(sessionResponse.sessionId, 'auth/create_session sessionId');
  }

  private async logout(): Promise<void> {
    if (!this.sessionId) {
      return;
    }
    await this.requestJson<unknown>('auth/logout', { method: 'POST', session: true });
    this.sessionId = undefined;
  }

  private async requestJson<TValue>(pathArg: string, optionsArg: { method?: 'GET' | 'POST' | 'PUT'; body?: unknown; session?: boolean } = {}): Promise<TValue> {
    const url = this.endpointUrl(pathArg);
    let response: Response;
    const headers: Record<string, string> = { accept: 'application/json' };
    if (optionsArg.body !== undefined) {
      headers['content-type'] = 'application/json';
    }
    if (optionsArg.session !== false && this.sessionId) {
      headers.authorization = `Session ${this.sessionId}`;
    }
    const controller = new AbortController();
    const timeout = globalThis.setTimeout(() => controller.abort(), this.kostalConfig.timeoutMs || kostalPlenticoreDefaultTimeoutMs);
    try {
      response = await globalThis.fetch(url, {
        method: optionsArg.method || 'GET',
        headers,
        body: optionsArg.body === undefined ? undefined : JSON.stringify(optionsArg.body),
        signal: controller.signal,
      });
    } catch (errorArg) {
      throw new KostalPlenticoreConnectionError(`Connection to ${url} failed: ${errorMessage(errorArg)}`);
    } finally {
      globalThis.clearTimeout(timeout);
    }

    const text = await response.text();
    if (!response.ok) {
      throw apiError(response.status, text);
    }
    if (!text.trim()) {
      return {} as TValue;
    }
    try {
      return JSON.parse(text) as TValue;
    } catch (errorArg) {
      throw new KostalPlenticoreApiError(`Kostal Plenticore endpoint ${pathArg} returned invalid JSON: ${errorMessage(errorArg)}`);
    }
  }

  private endpointUrl(pathArg: string): string {
    const base = this.baseUrl();
    return `${base}/${pathArg.replace(/^\/+/, '')}`;
  }

  private baseUrl(): string {
    const endpoint = this.endpoint();
    return `${endpoint.protocol}://${formatHost(endpoint.host)}${endpoint.port && !((endpoint.protocol === 'https' && endpoint.port === 443) || (endpoint.protocol === 'http' && endpoint.port === 80)) ? `:${endpoint.port}` : ''}/api/v1`;
  }

  private endpoint(): { protocol: 'http' | 'https'; host: string; port?: number } {
    const candidate = this.kostalConfig.url || this.kostalConfig.host;
    if (!candidate) {
      throw new KostalPlenticoreConnectionError('Kostal Plenticore local REST API requires config.host or config.url.');
    }
    if (/^[a-z][a-z0-9+.-]*:\/\//i.test(candidate)) {
      const url = new URL(candidate);
      return { protocol: url.protocol === 'https:' ? 'https' : 'http', host: url.hostname, port: url.port ? Number(url.port) : undefined };
    }
    return { protocol: this.kostalConfig.useTls ? 'https' : 'http', host: candidate, port: this.kostalConfig.port || kostalPlenticoreDefaultPort };
  }

  private endpointHost(): string | undefined {
    try {
      return this.endpoint().host;
    } catch {
      return undefined;
    }
  }

  private hasLiveTarget(): boolean {
    return Boolean(this.endpointHost() && (this.authKey() || this.sessionId));
  }

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

  private authKey(): string | undefined {
    return stringValue(this.kostalConfig.password) || stringValue(this.kostalConfig.apiKey);
  }

  private serviceCode(): string | undefined {
    return stringValue(this.kostalConfig.serviceCode) || stringValue(this.kostalConfig.service_code);
  }

  private processDataRequest(): Record<string, string[]> {
    return this.kostalConfig.processData || defaultProcessDataRequest;
  }

  private settingsRequest(): Record<string, string[]> {
    return this.kostalConfig.settingsData || defaultSettingsRequest;
  }
}

const normalizeProcessDataResponse = (valueArg: unknown): Record<string, IKostalPlenticoreProcessDataItem[]> => {
  const result: Record<string, IKostalPlenticoreProcessDataItem[]> = {};
  for (const moduleArg of arrayRecords(valueArg)) {
    const moduleId = stringValue(moduleArg.moduleid) || stringValue(moduleArg.moduleId);
    if (!moduleId) {
      continue;
    }
    result[moduleId] = arrayRecords(moduleArg.processdata || moduleArg.processData).map((itemArg) => ({
      id: requiredString(itemArg.id, 'process data id'),
      value: primitiveValue(itemArg.value),
      unit: stringValue(itemArg.unit),
    }));
  }
  return result;
};

const normalizeSettingsResponse = (valueArg: unknown): Record<string, Record<string, string>> => {
  const result: Record<string, Record<string, string>> = {};
  for (const moduleArg of arrayRecords(valueArg)) {
    const moduleId = stringValue(moduleArg.moduleid) || stringValue(moduleArg.moduleId);
    if (!moduleId) {
      continue;
    }
    result[moduleId] = {};
    for (const itemArg of arrayRecords(moduleArg.settings)) {
      const id = stringValue(itemArg.id);
      if (id) {
        result[moduleId][id] = String(itemArg.value ?? '');
      }
    }
  }
  return result;
};

const apiError = (statusArg: number, textArg: string): Error => {
  let message = textArg.slice(0, 200);
  try {
    const parsed = JSON.parse(textArg) as { message?: string };
    message = parsed.message || message;
  } catch {}
  if (statusArg === 400 || statusArg === 403) {
    return new KostalPlenticoreAuthenticationError(`Kostal Plenticore authentication failed with HTTP ${statusArg}${message ? `: ${message}` : ''}`);
  }
  if (statusArg === 401) {
    return new KostalPlenticoreAuthenticationError(`Kostal Plenticore session is not authorized${message ? `: ${message}` : ''}`);
  }
  return new KostalPlenticoreApiError(`Kostal Plenticore endpoint failed with HTTP ${statusArg}${message ? `: ${message}` : ''}`);
};

const arrayRecords = (valueArg: unknown): Array<Record<string, unknown>> => {
  return Array.isArray(valueArg) ? valueArg.filter((itemArg): itemArg is Record<string, unknown> => Boolean(itemArg && typeof itemArg === 'object' && !Array.isArray(itemArg))) : [];
};

const primitiveValue = (valueArg: unknown): IKostalPlenticoreProcessDataItem['value'] => {
  if (valueArg === null || ['string', 'number', 'boolean'].includes(typeof valueArg)) {
    return valueArg as IKostalPlenticoreProcessDataItem['value'];
  }
  return String(valueArg ?? '');
};

const hmac = (keyArg: Buffer, messageArg: string): Buffer => plugins.crypto.createHmac('sha256', keyArg).update(Buffer.from(messageArg, 'utf8')).digest();

const sha256 = (valueArg: Buffer): Buffer => plugins.crypto.createHash('sha256').update(valueArg).digest();

const xor = (leftArg: Buffer, rightArg: Buffer): Buffer => Buffer.from(leftArg.map((byteArg, indexArg) => byteArg ^ rightArg[indexArg]));

const b64 = (valueArg: Buffer): string => valueArg.toString('base64');

const b64decode = (valueArg: string): Buffer => Buffer.from(valueArg, 'base64');

const requiredString = (valueArg: unknown, labelArg: string): string => {
  const value = stringValue(valueArg);
  if (!value) {
    throw new KostalPlenticoreApiError(`Missing ${labelArg}.`);
  }
  return value;
};

const requiredNumber = (valueArg: unknown, labelArg: string): number => {
  const value = typeof valueArg === 'number' ? valueArg : typeof valueArg === 'string' ? Number(valueArg) : Number.NaN;
  if (!Number.isFinite(value)) {
    throw new KostalPlenticoreApiError(`Missing ${labelArg}.`);
  }
  return Math.round(value);
};

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

const stringArray = (valueArg: unknown): string[] => {
  return Array.isArray(valueArg) ? valueArg.filter((itemArg): itemArg is string => typeof itemArg === 'string') : [];
};

const formatHost = (hostArg: string): string => hostArg.includes(':') && !hostArg.startsWith('[') ? `[${hostArg}]` : hostArg;

const isSimpleLocalSnapshot = (valueArg: unknown): valueArg is ISimpleLocalSnapshot => {
  return Boolean(valueArg && typeof valueArg === 'object' && 'device' in valueArg && 'entities' in valueArg);
};

const normalizeServiceResult = (valueArg: unknown): IServiceCallResult => {
  if (valueArg && typeof valueArg === 'object' && 'success' in valueArg && typeof (valueArg as IServiceCallResult).success === 'boolean') {
    return valueArg as IServiceCallResult;
  }
  return { success: true, data: valueArg };
};

const clone = <TValue>(valueArg: TValue): TValue => JSON.parse(JSON.stringify(valueArg)) as TValue;

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