import type {
  IFullyKioskClientCommand,
  IFullyKioskConfig,
  IFullyKioskDeviceInfo,
  IFullyKioskRestObject,
  IFullyKioskSettings,
  IFullyKioskSnapshot,
  IFullyKioskSnapshotImage,
  TFullyKioskImageKind,
  TFullyKioskProtocol,
} from './fully_kiosk.types.js';
import { fullyKioskDefaultPort, fullyKioskDefaultTimeoutMs } from './fully_kiosk.types.js';

export class FullyKioskApiError extends Error {}
export class FullyKioskConnectionError extends FullyKioskApiError {}

export class FullyKioskClient {
  private currentSnapshot?: IFullyKioskSnapshot;

  constructor(private readonly config: IFullyKioskConfig) {}

  public async getSnapshot(forceRefreshArg = false): Promise<IFullyKioskSnapshot> {
    if (!forceRefreshArg && this.currentSnapshot) {
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (!forceRefreshArg && this.config.snapshot) {
      this.currentSnapshot = this.normalizeSnapshot(this.cloneSnapshot(this.config.snapshot), 'snapshot');
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (this.config.client) {
      try {
        this.currentSnapshot = await this.snapshotFromClient();
      } catch (errorArg) {
        this.currentSnapshot = this.snapshotFromConfig(false, errorMessage(errorArg), 'client');
      }
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (this.hasLiveTarget()) {
      try {
        this.currentSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.currentSnapshot = this.snapshotFromConfig(false, errorMessage(errorArg), 'runtime');
      }
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (this.hasManualData()) {
      this.currentSnapshot = this.snapshotFromConfig(this.config.online ?? true, undefined, 'manual');
      return this.cloneSnapshot(this.currentSnapshot);
    }

    this.currentSnapshot = this.snapshotFromConfig(false, 'No Fully Kiosk host/password, injected client, or snapshot/manual data is configured.', 'runtime');
    return this.cloneSnapshot(this.currentSnapshot);
  }

  public async refresh(): Promise<{ success: boolean; snapshot: IFullyKioskSnapshot; data?: unknown; error?: string }> {
    if (this.config.commandExecutor) {
      const data = await this.execute({ type: 'refresh', service: 'refresh' });
      const snapshot = await this.getSnapshot(true);
      return { success: true, snapshot, data };
    }

    if (!this.config.client && !this.hasLiveTarget()) {
      const snapshot = await this.getSnapshot();
      return {
        success: false,
        snapshot,
        error: 'Fully Kiosk refresh requires config.host plus config.password, config.client, or commandExecutor.',
      };
    }

    const snapshot = await this.getSnapshot(true);
    return {
      success: snapshot.online && !snapshot.error,
      snapshot,
      error: snapshot.error,
      data: { source: snapshot.source },
    };
  }

  public async validateConnection(): Promise<IFullyKioskDeviceInfo> {
    return this.getDeviceInfo();
  }

  public async getDeviceInfo(): Promise<IFullyKioskDeviceInfo> {
    const data = await this.sendCommand<IFullyKioskDeviceInfo>('deviceInfo');
    if (!isRecord(data)) {
      throw new FullyKioskConnectionError('Fully Kiosk deviceInfo response was not a JSON object.');
    }
    return data as IFullyKioskDeviceInfo;
  }

  public async getSettings(): Promise<IFullyKioskSettings> {
    const data = await this.sendCommand<IFullyKioskSettings>('listSettings');
    if (!isRecord(data)) {
      throw new FullyKioskConnectionError('Fully Kiosk listSettings response was not a JSON object.');
    }
    return data as IFullyKioskSettings;
  }

  public async sendCommand<TResponse = unknown>(cmdArg: string, paramsArg: Record<string, unknown> = {}): Promise<TResponse> {
    const result = await this.requestCommand<TResponse>(cmdArg, paramsArg);
    return result as TResponse;
  }

  public async getImage(kindArg: TFullyKioskImageKind): Promise<IFullyKioskSnapshotImage> {
    const result = await this.requestCommand<unknown>(kindArg === 'screenshot' ? 'getScreenshot' : 'getCamshot');
    if (isSnapshotImage(result)) {
      return result;
    }
    throw new FullyKioskConnectionError(`Fully Kiosk ${kindArg} response was not image data.`);
  }

  public async execute(commandArg: IFullyKioskClientCommand): Promise<unknown> {
    if (this.config.commandExecutor) {
      return typeof this.config.commandExecutor === 'function'
        ? this.config.commandExecutor(commandArg)
        : this.config.commandExecutor.execute(commandArg);
    }

    if (this.config.client?.execute) {
      return this.config.client.execute(commandArg);
    }

    if (commandArg.type === 'refresh') {
      return this.refresh();
    }

    if (this.config.client) {
      return this.executeWithClient(commandArg);
    }

    if (!this.hasLiveTarget()) {
      throw new FullyKioskConnectionError('Fully Kiosk commands require config.host plus config.password, config.client, or commandExecutor.');
    }

    if (commandArg.type === 'snapshot_image') {
      const image = await this.getImage(commandArg.imageKind || 'screenshot');
      return { contentType: image.contentType, dataBase64: Buffer.from(image.data).toString('base64') };
    }

    if (commandArg.type === 'stream_source') {
      return {
        baseUrl: this.baseUrl(),
        remoteAdminUrl: this.baseUrl(),
        verified: false,
      };
    }

    if (commandArg.type === 'set_string_setting') {
      if (!commandArg.settingKey) {
        throw new FullyKioskApiError('Fully Kiosk set_string_setting requires settingKey.');
      }
      return this.sendCommand('setStringSetting', { key: commandArg.settingKey, value: String(commandArg.value ?? '') });
    }

    if (commandArg.type === 'set_boolean_setting') {
      if (!commandArg.settingKey) {
        throw new FullyKioskApiError('Fully Kiosk set_boolean_setting requires settingKey.');
      }
      return this.sendCommand('setBooleanSetting', { key: commandArg.settingKey, value: Boolean(commandArg.value) });
    }

    if (commandArg.type === 'send_command' && commandArg.cmd) {
      return this.sendCommand(commandArg.cmd, commandArg.params || {});
    }

    throw new FullyKioskApiError(`Unsupported Fully Kiosk command: ${commandArg.type}`);
  }

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

  private async fetchSnapshot(): Promise<IFullyKioskSnapshot> {
    const [deviceInfo, settings] = await Promise.all([
      this.getDeviceInfo(),
      this.getSettings().catch(() => ({} as IFullyKioskSettings)),
    ]);
    return this.normalizeSnapshot({
      deviceInfo: { ...deviceInfo, settings },
      settings,
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'http',
      host: this.config.host,
      port: this.config.port || fullyKioskDefaultPort,
      protocol: this.protocol(),
      ssl: this.protocol() === 'https',
      verifySsl: this.config.verifySsl,
      name: this.config.name,
      uniqueId: this.config.uniqueId,
      macAddress: this.config.macAddress,
    }, 'http');
  }

  private async snapshotFromClient(): Promise<IFullyKioskSnapshot> {
    const client = this.config.client;
    if (!client) {
      throw new FullyKioskConnectionError('Fully Kiosk injected client is not configured.');
    }
    if (client.getSnapshot) {
      return this.normalizeSnapshot(await client.getSnapshot(), 'client');
    }
    if (!client.getDeviceInfo) {
      throw new FullyKioskConnectionError('Fully Kiosk injected client must expose getSnapshot or getDeviceInfo.');
    }
    const [deviceInfo, settings] = await Promise.all([
      client.getDeviceInfo(),
      client.getSettings ? client.getSettings().catch(() => ({} as IFullyKioskSettings)) : Promise.resolve({} as IFullyKioskSettings),
    ]);
    return this.normalizeSnapshot({
      deviceInfo: { ...deviceInfo, settings },
      settings,
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'client',
      host: this.config.host,
      port: this.config.port || fullyKioskDefaultPort,
      protocol: this.protocol(),
      ssl: this.protocol() === 'https',
      verifySsl: this.config.verifySsl,
      name: this.config.name,
      uniqueId: this.config.uniqueId,
      macAddress: this.config.macAddress,
    }, 'client');
  }

  private snapshotFromConfig(onlineArg: boolean, errorArg?: string, sourceArg: IFullyKioskSnapshot['source'] = 'runtime'): IFullyKioskSnapshot {
    const snapshot = this.config.snapshot;
    const settings = this.config.settings || snapshot?.settings || record(this.config.deviceInfo?.settings) || {};
    const deviceInfo: IFullyKioskDeviceInfo = {
      deviceID: this.config.uniqueId || snapshot?.uniqueId || snapshot?.deviceInfo.deviceID || snapshot?.deviceInfo.deviceId || this.config.macAddress || this.hostId(),
      deviceName: this.config.name || snapshot?.name || snapshot?.deviceInfo.deviceName || 'Fully Kiosk Browser',
      deviceManufacturer: this.config.manufacturer || snapshot?.deviceInfo.deviceManufacturer || 'Fully Kiosk',
      deviceModel: this.config.model || snapshot?.deviceInfo.deviceModel || 'Android kiosk',
      Mac: this.config.macAddress || snapshot?.macAddress || snapshot?.deviceInfo.Mac || snapshot?.deviceInfo.mac,
      ip4: this.config.host || snapshot?.host || snapshot?.deviceInfo.ip4,
      ...snapshot?.deviceInfo,
      ...this.config.deviceInfo,
      settings,
    };

    return this.normalizeSnapshot({
      deviceInfo,
      settings,
      online: onlineArg,
      updatedAt: new Date().toISOString(),
      source: sourceArg,
      host: this.config.host || snapshot?.host,
      port: this.config.port || snapshot?.port || (this.config.host || snapshot?.host ? fullyKioskDefaultPort : undefined),
      protocol: this.protocol(snapshot),
      ssl: this.protocol(snapshot) === 'https',
      verifySsl: this.config.verifySsl ?? snapshot?.verifySsl,
      name: this.config.name || snapshot?.name,
      uniqueId: this.config.uniqueId || snapshot?.uniqueId,
      macAddress: this.config.macAddress || snapshot?.macAddress,
      error: errorArg,
      metadata: {
        ...snapshot?.metadata,
        ...this.config.metadata,
      },
    }, sourceArg);
  }

  private normalizeSnapshot(snapshotArg: IFullyKioskSnapshot, sourceArg: IFullyKioskSnapshot['source']): IFullyKioskSnapshot {
    const settings = snapshotArg.settings || record(snapshotArg.deviceInfo.settings) || {};
    const deviceInfo = {
      ...snapshotArg.deviceInfo,
      settings,
    };
    const macAddress = normalizeMac(snapshotArg.macAddress || deviceInfo.Mac || deviceInfo.mac);
    const protocol = snapshotArg.protocol || this.protocol(snapshotArg);
    return {
      ...snapshotArg,
      deviceInfo,
      settings,
      online: Boolean(snapshotArg.online),
      updatedAt: snapshotArg.updatedAt || new Date().toISOString(),
      source: snapshotArg.source || sourceArg,
      host: snapshotArg.host || this.config.host || deviceInfo.ip4 || deviceInfo.hostname4,
      port: snapshotArg.port || this.config.port || (snapshotArg.host || this.config.host ? fullyKioskDefaultPort : undefined),
      protocol,
      ssl: protocol === 'https',
      verifySsl: snapshotArg.verifySsl ?? this.config.verifySsl,
      name: snapshotArg.name || this.config.name || deviceInfo.deviceName,
      uniqueId: snapshotArg.uniqueId || this.config.uniqueId || deviceInfo.deviceID || deviceInfo.deviceId || macAddress,
      macAddress,
    };
  }

  private async executeWithClient(commandArg: IFullyKioskClientCommand): Promise<unknown> {
    const client = this.config.client;
    if (!client) {
      throw new FullyKioskConnectionError('Fully Kiosk injected client is not configured.');
    }
    if (commandArg.type === 'refresh') {
      return this.refresh();
    }
    if (commandArg.type === 'snapshot_image') {
      const imageData = commandArg.imageKind === 'camshot'
        ? await client.getCamshot?.()
        : await client.getScreenshot?.();
      if (!imageData) {
        throw new FullyKioskApiError('Fully Kiosk injected client cannot return the requested image.');
      }
      return { contentType: 'image/png', dataBase64: Buffer.from(toUint8Array(imageData)).toString('base64') };
    }
    if (commandArg.type === 'set_string_setting') {
      if (!commandArg.settingKey || !client.sendCommand) {
        throw new FullyKioskApiError('Fully Kiosk injected client cannot set string settings.');
      }
      return client.sendCommand('setStringSetting', { key: commandArg.settingKey, value: String(commandArg.value ?? '') });
    }
    if (commandArg.type === 'set_boolean_setting') {
      if (!commandArg.settingKey || !client.sendCommand) {
        throw new FullyKioskApiError('Fully Kiosk injected client cannot set boolean settings.');
      }
      return client.sendCommand('setBooleanSetting', { key: commandArg.settingKey, value: Boolean(commandArg.value) });
    }
    if (commandArg.type === 'send_command' && commandArg.cmd && client.sendCommand) {
      return client.sendCommand(commandArg.cmd, commandArg.params || {});
    }
    throw new FullyKioskApiError('Fully Kiosk command is not available on the injected client and no HTTP endpoint or commandExecutor is configured.');
  }

  private async requestCommand<TResponse>(cmdArg: string, paramsArg: Record<string, unknown> = {}): Promise<TResponse | IFullyKioskSnapshotImage> {
    if (!this.hasLiveTarget()) {
      throw new FullyKioskConnectionError('Fully Kiosk HTTP requests require config.host and config.password.');
    }
    const url = new URL(this.baseUrl());
    url.searchParams.set('cmd', cmdArg);
    url.searchParams.set('password', this.config.password || '');
    url.searchParams.set('type', 'json');
    for (const [key, value] of Object.entries(paramsArg)) {
      if (value !== undefined && value !== null) {
        url.searchParams.set(key, paramValue(value));
      }
    }

    let response: Response;
    try {
      response = await globalThis.fetch(url, {
        headers: { accept: 'application/json' },
        signal: AbortSignal.timeout(this.config.timeoutMs || fullyKioskDefaultTimeoutMs),
      });
    } catch (errorArg) {
      throw new FullyKioskConnectionError(errorMessage(errorArg));
    }

    const contentType = response.headers.get('content-type') || '';
    if (!response.ok) {
      throw new FullyKioskConnectionError(`Fully Kiosk request ${cmdArg} failed with HTTP ${response.status}: ${await response.text()}`);
    }
    if (contentType.startsWith('image/') || contentType === 'application/octet-stream') {
      return {
        contentType: contentType || 'application/octet-stream',
        data: new Uint8Array(await response.arrayBuffer()),
      };
    }

    const text = await response.text();
    if (!text) {
      return {} as TResponse;
    }
    let data: unknown;
    try {
      data = JSON.parse(text);
    } catch (errorArg) {
      throw new FullyKioskConnectionError(`Fully Kiosk request ${cmdArg} returned invalid JSON: ${errorMessage(errorArg)}`);
    }
    if (isErrorResponse(data)) {
      throw new FullyKioskApiError(String(data.statustext || 'Fully Kiosk API returned Error.'));
    }
    return data as TResponse;
  }

  private hasLiveTarget(): boolean {
    return Boolean(this.config.host && typeof this.config.password === 'string');
  }

  private hasManualData(): boolean {
    return Boolean(this.config.deviceInfo || this.config.settings || this.config.snapshot);
  }

  private baseUrl(): string {
    if (!this.config.host) {
      throw new FullyKioskConnectionError('Fully Kiosk host is required for HTTP API access.');
    }
    return `${this.protocol()}://${this.config.host}:${this.config.port || fullyKioskDefaultPort}`;
  }

  private protocol(snapshotArg?: Pick<IFullyKioskSnapshot, 'protocol' | 'ssl'>): TFullyKioskProtocol {
    if (this.config.protocol) {
      return this.config.protocol;
    }
    if (typeof this.config.ssl === 'boolean') {
      return this.config.ssl ? 'https' : 'http';
    }
    if (snapshotArg?.protocol) {
      return snapshotArg.protocol;
    }
    if (snapshotArg?.ssl) {
      return 'https';
    }
    return 'http';
  }

  private hostId(): string | undefined {
    return this.config.host ? `${this.config.host}:${this.config.port || fullyKioskDefaultPort}` : undefined;
  }

  private cloneSnapshot(snapshotArg: IFullyKioskSnapshot): IFullyKioskSnapshot {
    return JSON.parse(JSON.stringify(snapshotArg)) as IFullyKioskSnapshot;
  }
}

export const normalizeMac = (valueArg: unknown): string | undefined => {
  if (typeof valueArg !== 'string') {
    return undefined;
  }
  const hex = valueArg.toLowerCase().replace(/[^a-f0-9]/g, '');
  if (hex.length !== 12) {
    return valueArg.trim().toLowerCase() || undefined;
  }
  return hex.match(/.{1,2}/g)?.join(':');
};

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

const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> => typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg);

const record = (valueArg: unknown): Record<string, unknown> | undefined => isRecord(valueArg) ? valueArg : undefined;

const isErrorResponse = (valueArg: unknown): valueArg is IFullyKioskRestObject => {
  return isRecord(valueArg) && valueArg.status === 'Error';
};

const isSnapshotImage = (valueArg: unknown): valueArg is IFullyKioskSnapshotImage => {
  return isRecord(valueArg) && typeof valueArg.contentType === 'string' && valueArg.data instanceof Uint8Array;
};

const paramValue = (valueArg: unknown): string => {
  if (typeof valueArg === 'boolean') {
    return valueArg ? 'true' : 'false';
  }
  return String(valueArg);
};

const toUint8Array = (valueArg: Uint8Array | ArrayBuffer | Buffer): Uint8Array => {
  if (valueArg instanceof Uint8Array) {
    return valueArg;
  }
  return new Uint8Array(valueArg);
};
