import { SimpleLocalClient, SimpleLocalMapper, type IServiceCallRequest, type IServiceCallResult, type ISimpleLocalSnapshot } from '../../core/index.js';
import type { INfandroidtvConfig } from './nfandroidtv.types.js';
import { nfandroidtvProfile } from './nfandroidtv.types.js';

const defaultIconBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGP6zwAAAgcBApocMXEAAAAASUVORK5CYII=';
const fontSizes: Record<string, number> = { small: 1, medium: 0, large: 2, max: 3 };
const positions: Record<string, number> = { 'bottom-right': 0, 'bottom-left': 1, 'top-right': 2, 'top-left': 3, center: 4 };
const transparencies: Record<string, number> = { '0%': 1, '25%': 2, '50%': 3, '75%': 4, '100%': 5 };
const backgroundColors: Record<string, string> = {
  grey: '#607d8b',
  black: '#000000',
  indigo: '#303F9F',
  green: '#4CAF50',
  red: '#F44336',
  cyan: '#00BCD4',
  teal: '#009688',
  amber: '#FFC107',
  pink: '#E91E63',
};

export class NfandroidtvClient extends SimpleLocalClient<INfandroidtvConfig> {
  private liveSnapshot?: ISimpleLocalSnapshot;

  constructor(configArg: INfandroidtvConfig) {
    super(nfandroidtvProfile, configArg);
    this.nfandroidtvConfig = configArg;
  }

  private readonly nfandroidtvConfig: INfandroidtvConfig;

  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 {
      await this.request('', { method: 'GET' });
      this.liveSnapshot = this.snapshot(true);
      return SimpleLocalMapper.clone(this.liveSnapshot);
    } catch (errorArg) {
      this.liveSnapshot = this.snapshot(false, errorMessage(errorArg));
      return SimpleLocalMapper.clone(this.liveSnapshot);
    }
  }

  public override async execute(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (!this.baseUrl()) {
      return super.execute(requestArg);
    }
    if (!['send_message', 'notify', 'send'].includes(requestArg.service)) {
      return { success: false, error: `Unsupported Notifications for Android TV service: ${requestArg.service}` };
    }

    const message = stringValue(requestArg.data?.message) || stringValue(requestArg.data?.msg);
    if (!message) {
      return { success: false, error: 'Notifications for Android TV send_message requires request.data.message.' };
    }

    try {
      await this.sendMessage({ ...requestArg.data, message });
      return { success: true, data: { message } };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  private async sendMessage(dataArg: Record<string, unknown>): Promise<void> {
    const payload: Record<string, string | number | boolean | Uint8Array> = {
      msg: String(dataArg.message),
      filename: Buffer.from(defaultIconBase64, 'base64'),
    };
    const title = stringValue(dataArg.title);
    if (title) {
      payload.title = title;
    }
    const duration = numberValue(dataArg.duration);
    if (duration !== undefined) {
      payload.duration = duration;
    }
    this.addMappedValue(payload, 'fontsize', dataArg.fontsize, fontSizes);
    this.addMappedValue(payload, 'position', dataArg.position, positions);
    this.addMappedValue(payload, 'transparency', dataArg.transparency, transparencies);
    this.addMappedValue(payload, 'bkgcolor', dataArg.bkgcolor ?? dataArg.color, backgroundColors);
    if (booleanValue(dataArg.interrupt)) {
      payload.interrupt = 1;
    }
    const icon = bytesValue(dataArg.icon);
    if (icon) {
      payload.filename = icon;
    }
    const image = bytesValue(dataArg.image ?? dataArg.image_file);
    if (image) {
      payload.filename2 = image;
    }

    const multipart = multipartBody(payload);
    const response = await this.request('', {
      method: 'POST',
      headers: { 'content-type': multipart.contentType },
      body: multipart.body as unknown as BodyInit,
    });
    if (response.status !== 200) {
      throw new Error(`Notifications for Android TV send_message failed with HTTP ${response.status}.`);
    }
  }

  private addMappedValue<TValue extends string | number>(payloadArg: Record<string, string | number | boolean | Uint8Array>, keyArg: string, valueArg: unknown, mapArg: Record<string, TValue>): void {
    const value = stringValue(valueArg);
    if (value && value in mapArg) {
      payloadArg[keyArg] = mapArg[value];
    }
  }

  private snapshot(onlineArg: boolean, errorArg?: string): ISimpleLocalSnapshot {
    const baseUrl = this.baseUrl();
    return {
      device: {
        id: this.nfandroidtvConfig.uniqueId || this.nfandroidtvConfig.host || baseUrl || nfandroidtvProfile.domain,
        name: this.nfandroidtvConfig.name || this.nfandroidtvConfig.host || nfandroidtvProfile.defaultName || nfandroidtvProfile.displayName,
        manufacturer: nfandroidtvProfile.manufacturer,
        model: nfandroidtvProfile.model,
        host: this.nfandroidtvConfig.host,
        port: this.nfandroidtvConfig.port || nfandroidtvProfile.defaultPort || 7676,
        protocol: this.nfandroidtvConfig.useTls ? 'https' : 'http',
        configurationUrl: baseUrl,
      },
      entities: [{
        id: 'notification_endpoint',
        name: 'Notification endpoint',
        platform: 'sensor',
        state: onlineArg ? 'online' : 'offline',
        available: onlineArg,
        attributes: { port: this.nfandroidtvConfig.port || nfandroidtvProfile.defaultPort || 7676 },
      }],
      online: onlineArg,
      updatedAt: new Date().toISOString(),
      source: onlineArg ? 'http' : 'runtime',
      rawData: { reachable: onlineArg },
      error: errorArg,
      capabilities: { localRead: true, localControl: true },
    };
  }

  private async request(pathArg: string, initArg: RequestInit): Promise<Response> {
    const baseUrl = this.baseUrl();
    if (!baseUrl) {
      throw new Error('Notifications for Android TV HTTP client requires config.host or config.url.');
    }
    const response = await this.fetchWithTimeout(`${baseUrl}${pathArg ? pathArg.startsWith('/') ? pathArg : `/${pathArg}` : ''}`, initArg);
    if (!response.ok) {
      const text = await response.text().catch(() => '');
      throw new Error(`Notifications for Android TV request 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.nfandroidtvConfig.timeoutMs || 5000);
    try {
      return await globalThis.fetch(urlArg, { ...initArg, signal: abortController.signal });
    } finally {
      clearTimeout(timeout);
    }
  }

  private baseUrl(): string | undefined {
    if (this.nfandroidtvConfig.url) {
      return this.nfandroidtvConfig.url.replace(/\/+$/, '');
    }
    const host = this.nfandroidtvConfig.host;
    if (!host) {
      return undefined;
    }
    const protocol = this.nfandroidtvConfig.useTls ? 'https' : 'http';
    const port = this.nfandroidtvConfig.port || nfandroidtvProfile.defaultPort || 7676;
    return `${protocol}://${host}:${port}`;
  }
}

const multipartBody = (payloadArg: Record<string, string | number | boolean | Uint8Array>): { body: Buffer; contentType: string } => {
  const boundary = `----smarthome-exchange-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
  const chunks: Buffer[] = [];
  const append = (valueArg: string | Buffer) => chunks.push(Buffer.isBuffer(valueArg) ? valueArg : Buffer.from(valueArg, 'utf8'));

  for (const [key, value] of Object.entries(payloadArg)) {
    append(`--${boundary}\r\n`);
    if (value instanceof Uint8Array) {
      append(`content-disposition: form-data; name="${key}"; filename="image"\r\n`);
      append('content-type: application/octet-stream\r\n\r\n');
      append(Buffer.from(value));
      append('\r\n');
      continue;
    }
    append(`content-disposition: form-data; name="${key}"\r\n\r\n${String(value)}\r\n`);
  }
  append(`--${boundary}--\r\n`);
  return { body: Buffer.concat(chunks), contentType: `multipart/form-data; boundary=${boundary}` };
};

const bytesValue = (valueArg: unknown): Uint8Array | undefined => {
  if (valueArg instanceof Uint8Array) {
    return valueArg;
  }
  if (valueArg instanceof ArrayBuffer) {
    return new Uint8Array(valueArg);
  }
  if (typeof valueArg === 'string' && valueArg.trim()) {
    const value = valueArg.includes(',') ? valueArg.split(',').pop() || '' : valueArg;
    try {
      return Buffer.from(value, 'base64');
    } catch {
      return undefined;
    }
  }
  return undefined;
};

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

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

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