import * as plugins from '../../plugins.js';
import { SimpleLocalClient, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { PanasonicVieraMapper } from './panasonic_viera.mapper.js';
import type { IPanasonicVieraConfig, IPanasonicVieraRawApiData } from './panasonic_viera.types.js';
import { panasonicVieraDefaultPort, panasonicVieraDefaultTimeoutMs, panasonicVieraProfile } from './panasonic_viera.types.js';

interface IPanasonicVieraSession {
  key: Buffer;
  iv: Buffer;
  hmacKey: Buffer;
  id?: string;
  sequenceNumber?: number;
}

export class PanasonicVieraClient extends SimpleLocalClient<IPanasonicVieraConfig> {
  private cachedSnapshot?: ISimpleLocalSnapshot;
  private encryptedSession?: IPanasonicVieraSession;

  constructor(private readonly clientConfig: IPanasonicVieraConfig) {
    super(panasonicVieraProfile, clientConfig);
  }

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

    if (!forceRefreshArg && this.clientConfig.snapshot) {
      this.cachedSnapshot = PanasonicVieraMapper.toSnapshot({ config: this.clientConfig, source: 'snapshot', online: this.clientConfig.snapshot.online });
      return clone(this.cachedSnapshot);
    }

    if (this.endpoint()) {
      try {
        const rawData = await this.fetchRawData();
        this.cachedSnapshot = PanasonicVieraMapper.toSnapshot({ config: this.clientConfig, rawData, online: true, source: 'http' });
      } catch (errorArg) {
        this.cachedSnapshot = this.createOfflineSnapshot(errorMessage(errorArg));
      }
      return clone(this.cachedSnapshot);
    }

    this.cachedSnapshot = await super.getSnapshot(forceRefreshArg);
    return clone(this.cachedSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.cachedSnapshot = undefined;
    const snapshot = await this.getSnapshot(Boolean(this.endpoint()));
    const success = snapshot.online && !snapshot.error;
    return { success, error: success ? undefined : snapshot.error || 'Panasonic Viera refresh requires a live SOAP endpoint, injected client, snapshotProvider, snapshot, entities, or rawData.', data: snapshot };
  }

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

    if (!this.endpoint()) {
      return super.execute(requestArg);
    }

    try {
      const data = requestArg.data || {};
      if (requestArg.service === 'volume_set' || requestArg.service === 'set_volume') {
        const volume = numberValue(data.volume_level ?? data.volumeLevel ?? data.volume);
        if (volume === undefined) {
          throw new Error('Panasonic Viera volume_set requires data.volume_level or data.volume.');
        }
        await this.setVolume(volume <= 1 ? Math.round(volume * 100) : Math.round(volume));
        this.cachedSnapshot = undefined;
        return { success: true, data: { volume } };
      }

      if (requestArg.service === 'volume_mute' || requestArg.service === 'set_mute') {
        const mute = booleanValue(data.is_volume_muted ?? data.mute ?? data.muted);
        if (mute === undefined) {
          await this.sendKey('NRC_MUTE-ONOFF');
          this.cachedSnapshot = undefined;
          return { success: true, data: { key: 'NRC_MUTE-ONOFF' } };
        }
        await this.setMute(mute);
        this.cachedSnapshot = undefined;
        return { success: true, data: { muted: mute } };
      }

      const key = keyForService(requestArg.service, data);
      if (!key) {
        return { success: false, error: `Unsupported Panasonic Viera service: ${requestArg.service}` };
      }
      await this.sendKey(key);
      this.cachedSnapshot = undefined;
      return { success: true, data: { key } };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

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

  private async fetchRawData(): Promise<IPanasonicVieraRawApiData> {
    const deviceInfoXml = await this.requestText('/nrc/ddd.xml');
    const deviceInfo = parseDeviceInfo(deviceInfoXml);
    const rawData: IPanasonicVieraRawApiData = {
      deviceInfo,
      fetchedAt: new Date().toISOString(),
    };

    try {
      rawData.volume = await this.getVolume();
    } catch (errorArg) {
      rawData.volumeError = errorMessage(errorArg);
    }

    try {
      rawData.muted = await this.getMute();
    } catch (errorArg) {
      rawData.muteError = errorMessage(errorArg);
    }

    return rawData;
  }

  private async getVolume(): Promise<number> {
    const text = await this.soapRequest('/dmr/control_0', 'schemas-upnp-org:service:RenderingControl:1', 'GetVolume', '<InstanceID>0</InstanceID><Channel>Master</Channel>');
    const volume = numberValue(extractXmlValue(text, 'CurrentVolume'));
    if (volume === undefined) {
      throw new Error('Panasonic Viera GetVolume response did not include CurrentVolume.');
    }
    return volume;
  }

  private async setVolume(volumeArg: number): Promise<void> {
    if (volumeArg < 0 || volumeArg > 100) {
      throw new Error('Panasonic Viera volume must be between 0 and 100.');
    }
    await this.soapRequest('/dmr/control_0', 'schemas-upnp-org:service:RenderingControl:1', 'SetVolume', `<InstanceID>0</InstanceID><Channel>Master</Channel><DesiredVolume>${volumeArg}</DesiredVolume>`);
  }

  private async getMute(): Promise<boolean> {
    const text = await this.soapRequest('/dmr/control_0', 'schemas-upnp-org:service:RenderingControl:1', 'GetMute', '<InstanceID>0</InstanceID><Channel>Master</Channel>');
    return extractXmlValue(text, 'CurrentMute') !== '0';
  }

  private async setMute(muteArg: boolean): Promise<void> {
    await this.soapRequest('/dmr/control_0', 'schemas-upnp-org:service:RenderingControl:1', 'SetMute', `<InstanceID>0</InstanceID><Channel>Master</Channel><DesiredMute>${muteArg ? '1' : '0'}</DesiredMute>`);
  }

  private async sendKey(keyArg: string): Promise<void> {
    await this.soapRequest('/nrc/control_0', 'panasonic-com:service:p00NetworkControl:1', 'X_SendKey', `<X_KeyEvent>${escapeXml(keyArg)}</X_KeyEvent>`);
  }

  private async soapRequest(pathArg: string, urnArg: string, actionArg: string, paramsArg: string, bodyElementArg = 'm'): Promise<string> {
    let action = actionArg;
    let params = paramsArg;
    let bodyElement = bodyElementArg;
    let encrypted = false;

    if (this.shouldEncrypt(urnArg, actionArg)) {
      const session = await this.ensureEncryptedSession();
      session.sequenceNumber = (session.sequenceNumber || 1) + 1;
      const originalCommand = `<X_SessionId>${session.id}</X_SessionId><X_SequenceNumber>${String(session.sequenceNumber).padStart(8, '0')}</X_SequenceNumber><X_OriginalCommand><u:${actionArg} xmlns:u="urn:${urnArg}">${paramsArg}</u:${actionArg}></X_OriginalCommand>`;
      const encryptedCommand = this.encryptSoapPayload(originalCommand, session);
      action = 'X_EncryptedCommand';
      params = `<X_ApplicationId>${escapeXml(this.clientConfig.appId || '')}</X_ApplicationId><X_EncInfo>${encryptedCommand}</X_EncInfo>`;
      bodyElement = 'u';
      encrypted = true;
    }

    const body = `<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><${bodyElement}:${action} xmlns:${bodyElement}="urn:${urnArg}">${params}</${bodyElement}:${action}></s:Body></s:Envelope>`;
    const text = await this.requestText(pathArg, {
      method: 'POST',
      body,
      headers: {
        'Content-Type': 'text/xml; charset=utf-8',
        SOAPAction: `"urn:${urnArg}#${action}"`,
      },
    });

    if (!encrypted) {
      return text;
    }

    const encResult = extractXmlValue(text, 'X_EncResult');
    if (!encResult) {
      throw new Error('Panasonic Viera encrypted SOAP response did not include X_EncResult.');
    }
    return this.decryptSoapPayload(encResult, await this.ensureEncryptedSession());
  }

  private shouldEncrypt(urnArg: string, actionArg: string): boolean {
    return urnArg === 'panasonic-com:service:p00NetworkControl:1'
      && !['X_GetEncryptSessionId', 'X_DisplayPinCode', 'X_RequestAuth'].includes(actionArg)
      && Boolean(this.clientConfig.appId && this.clientConfig.encryptionKey);
  }

  private async ensureEncryptedSession(): Promise<IPanasonicVieraSession> {
    if (!this.clientConfig.appId || !this.clientConfig.encryptionKey) {
      throw new Error('Panasonic Viera encrypted commands require config.appId and config.encryptionKey.');
    }
    if (!this.encryptedSession) {
      this.encryptedSession = deriveSession(this.clientConfig.encryptionKey);
    }
    if (this.encryptedSession.id) {
      return this.encryptedSession;
    }

    const encInfo = this.encryptSoapPayload(`<X_ApplicationId>${escapeXml(this.clientConfig.appId)}</X_ApplicationId>`, this.encryptedSession);
    const text = await this.soapRequest('/nrc/control_0', 'panasonic-com:service:p00NetworkControl:1', 'X_GetEncryptSessionId', `<X_ApplicationId>${escapeXml(this.clientConfig.appId)}</X_ApplicationId><X_EncInfo>${encInfo}</X_EncInfo>`, 'u');
    const encResult = extractXmlValue(text, 'X_EncResult');
    if (!encResult) {
      throw new Error('Panasonic Viera session response did not include X_EncResult.');
    }
    const decrypted = this.decryptSoapPayload(encResult, this.encryptedSession);
    const sessionId = extractXmlValue(decrypted, 'X_SessionId');
    if (!sessionId) {
      throw new Error('Panasonic Viera encrypted session response did not include X_SessionId.');
    }
    this.encryptedSession.id = sessionId;
    this.encryptedSession.sequenceNumber = 1;
    return this.encryptedSession;
  }

  private encryptSoapPayload(dataArg: string, sessionArg: IPanasonicVieraSession): string {
    const header = Buffer.concat([plugins.crypto.randomBytes(12), uint32be(Buffer.byteLength(dataArg, 'latin1'))]);
    const payload = Buffer.concat([header, Buffer.from(dataArg, 'latin1')]);
    const padLength = (16 - (payload.length % 16)) % 16;
    const paddedPayload = padLength ? Buffer.concat([payload, Buffer.alloc(padLength)]) : payload;
    const cipher = plugins.crypto.createCipheriv('aes-128-cbc', sessionArg.key, sessionArg.iv);
    cipher.setAutoPadding(false);
    const ciphertext = Buffer.concat([cipher.update(paddedPayload), cipher.final()]);
    const signature = plugins.crypto.createHmac('sha256', sessionArg.hmacKey).update(ciphertext).digest();
    return Buffer.concat([ciphertext, signature]).toString('base64');
  }

  private decryptSoapPayload(dataArg: string, sessionArg: IPanasonicVieraSession): string {
    const encrypted = Buffer.from(dataArg, 'base64');
    const decipher = plugins.crypto.createDecipheriv('aes-128-cbc', sessionArg.key, sessionArg.iv);
    decipher.setAutoPadding(false);
    const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('latin1');
    return decrypted.slice(16).split('\0')[0];
  }

  private async requestText(pathArg: string, optionsArg: { method?: 'GET' | 'POST'; body?: string; headers?: Record<string, string> } = {}): Promise<string> {
    const endpoint = this.requireEndpoint();
    const url = new URL(pathArg.replace(/^\/+/, ''), `${endpoint.origin}/`);
    const timeoutMs = this.clientConfig.timeoutMs || panasonicVieraDefaultTimeoutMs;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      const response = await globalThis.fetch(url, {
        method: optionsArg.method || 'GET',
        headers: { Accept: 'text/xml, application/xml;q=0.9, */*;q=0.8', ...(optionsArg.headers || {}) },
        body: optionsArg.body,
        signal: controller.signal,
      });
      const text = await response.text();
      if (!response.ok) {
        throw new Error(`Panasonic Viera request ${url.pathname} failed with ${response.status}: ${extractXmlValue(text, 'errorDescription') || text.slice(0, 160)}`);
      }
      return text;
    } finally {
      clearTimeout(timer);
    }
  }

  private requireEndpoint(): URL {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('Panasonic Viera local SOAP API requires config.host or config.url.');
    }
    return endpoint;
  }

  private endpoint(): URL | undefined {
    const configuredUrl = stringValue(this.clientConfig.url);
    if (configuredUrl) {
      return normalizeBaseUrl(configuredUrl, this.clientConfig.port, this.clientConfig.useTls);
    }
    const host = stringValue(this.clientConfig.host);
    if (!host) {
      return undefined;
    }
    return normalizeBaseUrl(host, this.clientConfig.port || panasonicVieraDefaultPort, this.clientConfig.useTls);
  }

  private createOfflineSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return PanasonicVieraMapper.toSnapshot({ config: this.clientConfig, online: false, source: 'runtime', error: errorArg });
  }
}

const keyMap: Record<string, string> = {
  media_play: 'NRC_PLAY-ONOFF',
  media_pause: 'NRC_PAUSE-ONOFF',
  media_stop: 'NRC_STOP-ONOFF',
  media_next_track: 'NRC_FF-ONOFF',
  media_previous_track: 'NRC_REW-ONOFF',
  volume_up: 'NRC_VOLUP-ONOFF',
  volume_down: 'NRC_VOLDOWN-ONOFF',
  turn_on: 'NRC_POWER-ONOFF',
  turn_off: 'NRC_POWER-ONOFF',
  toggle: 'NRC_POWER-ONOFF',
};

const keyForService = (serviceArg: string, dataArg: Record<string, unknown>): string | undefined => {
  const explicit = stringValue(dataArg.command) || stringValue(dataArg.key) || stringValue(dataArg.button);
  if (serviceArg === 'press' && explicit) {
    return normalizeKey(explicit);
  }
  return keyMap[serviceArg];
};

const normalizeKey = (valueArg: string): string => {
  if (valueArg.startsWith('NRC_')) {
    return valueArg;
  }
  const normalized = valueArg.toUpperCase().replace(/[^A-Z0-9]+/g, '_');
  const aliases: Record<string, string> = {
    PLAY: 'NRC_PLAY-ONOFF',
    PAUSE: 'NRC_PAUSE-ONOFF',
    STOP: 'NRC_STOP-ONOFF',
    POWER: 'NRC_POWER-ONOFF',
    VOLUME_UP: 'NRC_VOLUP-ONOFF',
    VOLUME_DOWN: 'NRC_VOLDOWN-ONOFF',
    MUTE: 'NRC_MUTE-ONOFF',
    ENTER: 'NRC_ENTER-ONOFF',
    UP: 'NRC_UP-ONOFF',
    DOWN: 'NRC_DOWN-ONOFF',
    LEFT: 'NRC_LEFT-ONOFF',
    RIGHT: 'NRC_RIGHT-ONOFF',
    HOME: 'NRC_HOME-ONOFF',
    MENU: 'NRC_MENU-ONOFF',
    BACK: 'NRC_RETURN-ONOFF',
  };
  return aliases[normalized] || `NRC_${normalized}-ONOFF`;
};

const deriveSession = (encryptionKeyArg: string): IPanasonicVieraSession => {
  const iv = Buffer.from(encryptionKeyArg, 'base64').subarray(0, 16);
  if (iv.length !== 16) {
    throw new Error('Panasonic Viera encryptionKey must decode to a 16 byte AES IV.');
  }
  const key = Buffer.alloc(16);
  for (let index = 0; index < 16; index += 4) {
    key[index] = iv[index + 2];
    key[index + 1] = iv[index + 3];
    key[index + 2] = iv[index];
    key[index + 3] = iv[index + 1];
  }
  return { key, iv, hmacKey: Buffer.concat([iv, iv]) };
};

const uint32be = (valueArg: number): Buffer => {
  const buffer = Buffer.alloc(4);
  buffer.writeUInt32BE(valueArg, 0);
  return buffer;
};

const parseDeviceInfo = (xmlArg: string): Record<string, unknown> => ({
  UDN: extractXmlValue(xmlArg, 'UDN'),
  friendlyName: extractXmlValue(xmlArg, 'friendlyName'),
  manufacturer: extractXmlValue(xmlArg, 'manufacturer'),
  modelName: extractXmlValue(xmlArg, 'modelName'),
  modelNumber: extractXmlValue(xmlArg, 'modelNumber'),
});

const extractXmlValue = (xmlArg: string, tagArg: string): string | undefined => {
  const pattern = new RegExp(`<(?:\\w+:)?${tagArg}[^>]*>([\\s\\S]*?)</(?:\\w+:)?${tagArg}>`, 'i');
  const match = pattern.exec(xmlArg);
  return match?.[1]?.trim();
};

const escapeXml = (valueArg: string): string => valueArg.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');

const normalizeBaseUrl = (valueArg: string, portArg: number | undefined, useTlsArg?: boolean): URL | undefined => {
  try {
    const parsed = new URL(valueArg.includes('://') ? valueArg : `${useTlsArg ? 'https' : 'http'}://${valueArg}`);
    if (portArg) {
      parsed.port = String(portArg);
    }
    parsed.pathname = '';
    parsed.search = '';
    parsed.hash = '';
    return parsed;
  } catch {
    return 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() && Number.isFinite(Number(valueArg))) {
    return Number(valueArg);
  }
  return undefined;
};

const booleanValue = (valueArg: unknown): boolean | undefined => {
  if (typeof valueArg === 'boolean') {
    return valueArg;
  }
  if (typeof valueArg === 'number') {
    return valueArg !== 0;
  }
  if (typeof valueArg === 'string') {
    if (valueArg === '1' || valueArg.toLowerCase() === 'true') {
      return true;
    }
    if (valueArg === '0' || valueArg.toLowerCase() === 'false') {
      return false;
    }
  }
  return undefined;
};

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);
