import { SimpleLocalClient, SimpleLocalMapper, type ISimpleLocalEntitySnapshot, type IServiceCallRequest, type IServiceCallResult } from '../../core/index.js';
import { LgNetcastMapper } from './lg_netcast.mapper.js';
import type { ILgNetcastConfig, ILgNetcastSnapshot, TLgNetcastProtocol } from './lg_netcast.types.js';
import { lgNetcastProfile } from './lg_netcast.types.js';

export class LgNetcastClient extends SimpleLocalClient<ILgNetcastConfig> {
  private liveSnapshot?: ILgNetcastSnapshot;

  constructor(private readonly configArg: ILgNetcastConfig) {
    super(lgNetcastProfile, configArg);
  }

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

    if (this.shouldUseNativeHttp(forceRefreshArg)) {
      try {
        this.liveSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.liveSnapshot = LgNetcastMapper.toSnapshot({ config: this.configArg, online: false, source: 'runtime', error: errorMessage(errorArg) });
      }
      return SimpleLocalMapper.clone(this.liveSnapshot);
    }

    const snapshot = await super.getSnapshot(forceRefreshArg);
    this.liveSnapshot = LgNetcastMapper.toSnapshot({ config: this.configArg, rawData: snapshot.rawData ?? snapshot, online: snapshot.online, source: snapshot.source, error: snapshot.error });
    return SimpleLocalMapper.clone(this.liveSnapshot);
  }

  public async refresh(): Promise<IServiceCallResult> {
    this.liveSnapshot = undefined;
    const snapshot = await this.getSnapshot(true);
    const success = snapshot.online && !snapshot.error;
    return { success, error: success ? undefined : snapshot.error, data: snapshot };
  }

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

    try {
      const command = this.commandForRequest(requestArg);
      await this.sendRemoteCommand(command);
      this.liveSnapshot = undefined;
      return { success: true, data: { command } };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

  private shouldUseNativeHttp(forceRefreshArg: boolean): boolean {
    if (!this.endpoint() || this.configArg.client || this.configArg.snapshotProvider) {
      return false;
    }
    if (!forceRefreshArg && this.configArg.snapshot) {
      return false;
    }
    return true;
  }

  private async fetchSnapshot(): Promise<ILgNetcastSnapshot> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('LG Netcast live snapshots require config.host or config.url.');
    }

    const errors: string[] = [];
    let deviceInfo: ILgNetcastDeviceInfo | undefined;
    let volumeInfo: ILgNetcastVolumeInfo | undefined;
    let channelInfo: ILgNetcastChannelInfo | undefined;

    try {
      deviceInfo = await this.queryDeviceInfo();
    } catch (errorArg) {
      errors.push(`device_info: ${errorMessage(errorArg)}`);
    }
    try {
      volumeInfo = await this.getVolume();
    } catch (errorArg) {
      errors.push(`volume_info: ${errorMessage(errorArg)}`);
    }
    try {
      channelInfo = await this.getCurrentChannel();
    } catch (errorArg) {
      errors.push(`cur_channel: ${errorMessage(errorArg)}`);
    }

    if (!deviceInfo && !volumeInfo && !channelInfo) {
      throw new Error(errors[0] || 'LG Netcast did not return device, volume, or channel data.');
    }

    const volumeLevel = volumeInfo?.level === undefined ? undefined : volumeInfo.level / 100;
    const mediaAttributes = cleanAttributes({
      volume: volumeInfo?.level,
      volumeLevel,
      isVolumeMuted: volumeInfo?.muted,
      channelId: channelInfo?.channelId,
      channelName: channelInfo?.channelName,
      programName: channelInfo?.programName,
      inputSourceName: channelInfo?.inputSourceName,
      mediaImageUrl: `${endpoint.origin}/data?target=screen_image`,
      accessTokenConfigured: Boolean(this.accessToken()),
    });
    const entities: ISimpleLocalEntitySnapshot[] = [{
      id: 'media_player',
      uniqueId: `${lgNetcastProfile.domain}_${SimpleLocalMapper.slug(this.configArg.uniqueId || deviceInfo?.uuid || endpoint.host)}_media_player`,
      name: this.configArg.name || deviceInfo?.friendlyName || lgNetcastProfile.defaultName || lgNetcastProfile.displayName,
      platform: 'media_player',
      state: 'on',
      available: true,
      writable: Boolean(this.accessToken()),
      attributes: mediaAttributes,
    }];

    const rawData = cleanAttributes({
      deviceInfo,
      volumeInfo,
      channelInfo,
      errors: errors.length ? errors : undefined,
    }) || {};

    return {
      device: {
        id: this.configArg.uniqueId || this.configArg.deviceId || deviceInfo?.uuid || `${endpoint.host}:${endpoint.port}`,
        name: this.configArg.name || deviceInfo?.friendlyName || lgNetcastProfile.defaultName || lgNetcastProfile.displayName,
        manufacturer: 'LG',
        model: deviceInfo?.modelName || 'LG Netcast TV',
        serialNumber: deviceInfo?.uuid,
        host: endpoint.host,
        port: endpoint.port,
        protocol: 'http',
        configurationUrl: endpoint.origin,
        attributes: cleanAttributes({
          tvProtocol: endpoint.tvProtocol,
          friendlyName: deviceInfo?.friendlyName,
          modelName: deviceInfo?.modelName,
          partialErrors: errors.length ? errors : undefined,
        }),
      },
      entities,
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'http',
      rawData,
      capabilities: {
        localRead: true,
        localControl: Boolean(this.accessToken()),
        pairingRequiredForControl: !this.accessToken(),
      },
    };
  }

  private async queryDeviceInfo(): Promise<ILgNetcastDeviceInfo> {
    const text = await this.requestText('data', {
      query: { target: 'rootservice.xml' },
      headers: { 'User-Agent': 'UDAP/2.0' },
    });
    return {
      uuid: textOf(text, 'uuid'),
      modelName: textOf(text, 'modelName'),
      friendlyName: textOf(text, 'friendlyName'),
    };
  }

  private async getVolume(): Promise<ILgNetcastVolumeInfo | undefined> {
    const dataXml = await this.queryDataXml('volume_info');
    if (!dataXml) {
      return undefined;
    }
    return {
      level: numberValue(textOf(dataXml, 'level')),
      muted: textOf(dataXml, 'mute') === 'true',
    };
  }

  private async getCurrentChannel(): Promise<ILgNetcastChannelInfo | undefined> {
    const dataXml = await this.queryDataXml('cur_channel');
    if (!dataXml) {
      return undefined;
    }
    return {
      channelId: numberValue(textOf(dataXml, 'major')),
      channelName: textOf(dataXml, 'chname') || textOf(dataXml, 'inputSourceName'),
      programName: textOf(dataXml, 'progName') || textOf(dataXml, 'labelName'),
      inputSourceName: textOf(dataXml, 'inputSourceName'),
    };
  }

  private async queryDataXml(targetArg: string): Promise<string | undefined> {
    const text = await this.requestText('data', { query: { target: targetArg } });
    return firstXmlNode(text, 'data') || undefined;
  }

  private async sendRemoteCommand(commandArg: number): Promise<void> {
    const session = await this.getSessionId();
    const message = `${xmlDeclaration}<command><session>${escapeXml(session)}</session><type>HandleKeyInput</type><value>${commandArg}</value></command>`;
    await this.requestText('command', { method: 'POST', body: message });
  }

  private async getSessionId(): Promise<string> {
    const accessToken = this.accessToken();
    if (!accessToken) {
      await this.displayPairKey();
      throw new Error('LG Netcast control requires config.accessToken or config.token; a pairing key display request was sent to the TV.');
    }

    const message = `${xmlDeclaration}<auth><type>AuthReq</type><value>${escapeXml(accessToken)}</value></auth>`;
    const text = await this.requestText('auth', { method: 'POST', body: message });
    const session = textOf(text, 'session');
    if (!session) {
      throw new Error('LG Netcast auth response did not include a session id.');
    }
    return session;
  }

  private async displayPairKey(): Promise<void> {
    const message = `${xmlDeclaration}<auth><type>AuthKeyReq</type></auth>`;
    await this.requestText('auth', { method: 'POST', body: message });
  }

  private async requestText(pathArg: 'auth' | 'command' | 'data', optionsArg: ILgNetcastRequestOptions = {}): Promise<string> {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('LG Netcast HTTP requests require config.host or config.url.');
    }
    const path = endpoint.tvProtocol === 'hdcp' && pathArg !== 'command' ? 'dtv_wifirc' : pathArg;
    const url = new URL(`${endpoint.apiBaseUrl}/${path}`);
    for (const [key, value] of Object.entries(optionsArg.query || {})) {
      url.searchParams.set(key, value);
    }

    const response = await globalThis.fetch(url, {
      method: optionsArg.method || 'GET',
      headers: {
        accept: 'application/xml, text/xml;q=0.9, */*;q=0.8',
        'Content-Type': 'application/atom+xml',
        ...optionsArg.headers,
      },
      body: optionsArg.body,
      signal: AbortSignal.timeout(this.configArg.timeoutMs || 3000),
    });
    const text = await response.text();
    if (!response.ok) {
      throw new Error(`LG Netcast HTTP ${path} failed with ${response.status}: ${text.slice(0, 160)}`);
    }
    return text;
  }

  private commandForRequest(requestArg: IServiceCallRequest): number {
    const data = requestArg.data || {};
    const explicit = data.command ?? data.key ?? data.button;
    const explicitCommand = commandValue(explicit);
    if (explicitCommand !== undefined) {
      return explicitCommand;
    }
    const mapped = serviceCommandMap[requestArg.service];
    if (mapped !== undefined) {
      return mapped;
    }
    throw new Error(`Unsupported LG Netcast service: ${requestArg.domain}.${requestArg.service}`);
  }

  private accessToken(): string | undefined {
    return stringValue(this.configArg.accessToken) || stringValue(this.configArg.token) || stringValue(this.configArg.pairingKey);
  }

  private endpoint(): ILgNetcastEndpoint | undefined {
    const rawUrl = stringValue(this.configArg.url) || stringValue(this.configArg.metadata?.url);
    const configuredTvProtocol = tvProtocolValue(this.configArg.netcastProtocol || this.configArg.protocol) || 'roap';
    if (rawUrl) {
      const parsed = parseUrl(rawUrl);
      if (parsed) {
        const tvProtocol = parsed.pathname.includes('/hdcp/') ? 'hdcp' : parsed.pathname.includes('/roap/') ? 'roap' : configuredTvProtocol;
        const port = this.configArg.port || numberValue(parsed.port) || lgNetcastDefaultPort;
        const apiPath = parsed.pathname.includes('/api') ? parsed.pathname.slice(0, parsed.pathname.indexOf('/api') + 4) : `/${tvProtocol}/api`;
        const origin = `${parsed.protocol}//${parsed.hostname}${portPart(parsed.protocol, port)}`;
        return { host: parsed.hostname, port, origin, apiBaseUrl: `${origin}${apiPath.replace(/\/+$/, '')}`, tvProtocol };
      }
    }

    const host = stringValue(this.configArg.host);
    if (!host) {
      return undefined;
    }
    const parsedHost = parseUrl(host);
    if (parsedHost) {
      const port = this.configArg.port || numberValue(parsedHost.port) || lgNetcastDefaultPort;
      const tvProtocol = parsedHost.pathname.includes('/hdcp/') ? 'hdcp' : parsedHost.pathname.includes('/roap/') ? 'roap' : configuredTvProtocol;
      const origin = `${parsedHost.protocol}//${parsedHost.hostname}${portPart(parsedHost.protocol, port)}`;
      return { host: parsedHost.hostname, port, origin, apiBaseUrl: `${origin}/${tvProtocol}/api`, tvProtocol };
    }

    const port = this.configArg.port || lgNetcastDefaultPort;
    const origin = `http://${host}${portPart('http:', port)}`;
    return { host, port, origin, apiBaseUrl: `${origin}/${configuredTvProtocol}/api`, tvProtocol: configuredTvProtocol };
  }
}

interface ILgNetcastEndpoint {
  host: string;
  port: number;
  origin: string;
  apiBaseUrl: string;
  tvProtocol: TLgNetcastProtocol;
}

interface ILgNetcastRequestOptions {
  method?: 'GET' | 'POST';
  body?: string;
  headers?: Record<string, string>;
  query?: Record<string, string>;
}

interface ILgNetcastDeviceInfo {
  uuid?: string;
  modelName?: string;
  friendlyName?: string;
}

interface ILgNetcastVolumeInfo {
  level?: number;
  muted?: boolean;
}

interface ILgNetcastChannelInfo {
  channelId?: number;
  channelName?: string;
  programName?: string;
  inputSourceName?: string;
}

const lgNetcastDefaultPort = 8080;
const xmlDeclaration = '<?xml version="1.0" encoding="utf-8"?>';

const lgCommands: Record<string, number> = {
  POWER: 1,
  NUMBER_0: 2,
  NUMBER_1: 3,
  NUMBER_2: 4,
  NUMBER_3: 5,
  NUMBER_4: 6,
  NUMBER_5: 7,
  NUMBER_6: 8,
  NUMBER_7: 9,
  NUMBER_8: 10,
  NUMBER_9: 11,
  UP: 12,
  DOWN: 13,
  LEFT: 14,
  RIGHT: 15,
  OK: 20,
  HOME_MENU: 21,
  BACK: 23,
  VOLUME_UP: 24,
  VOLUME_DOWN: 25,
  MUTE_TOGGLE: 26,
  CHANNEL_UP: 27,
  CHANNEL_DOWN: 28,
  BLUE: 29,
  GREEN: 30,
  RED: 31,
  YELLOW: 32,
  PLAY: 33,
  PAUSE: 34,
  STOP: 35,
  FAST_FORWARD: 36,
  REWIND: 37,
  SKIP_FORWARD: 38,
  SKIP_BACKWARD: 39,
  RECORD: 40,
  RECORDING_LIST: 41,
  REPEAT: 42,
  LIVE_TV: 43,
  EPG: 44,
  PROGRAM_INFORMATION: 45,
  ASPECT_RATIO: 46,
  EXTERNAL_INPUT: 47,
  PIP_SECONDARY_VIDEO: 48,
  SHOW_SUBTITLE: 49,
  PROGRAM_LIST: 50,
  TELE_TEXT: 51,
  MARK: 52,
  VIDEO_3D: 400,
  LR_3D: 401,
  DASH: 402,
  PREVIOUS_CHANNEL: 403,
  FAVORITE_CHANNEL: 404,
  QUICK_MENU: 405,
  TEXT_OPTION: 406,
  AUDIO_DESCRIPTION: 407,
  ENERGY_SAVING: 409,
  AV_MODE: 410,
  SIMPLINK: 411,
  EXIT: 412,
  RESERVATION_PROGRAM_LIST: 413,
  PIP_CHANNEL_UP: 414,
  PIP_CHANNEL_DOWN: 415,
  SWITCH_VIDEO: 416,
  APPS: 417,
};

const serviceCommandMap: Record<string, number> = {
  turn_off: lgCommands.POWER,
  volume_up: lgCommands.VOLUME_UP,
  volume_down: lgCommands.VOLUME_DOWN,
  volume_mute: lgCommands.MUTE_TOGGLE,
  media_play: lgCommands.PLAY,
  media_pause: lgCommands.PAUSE,
  media_stop: lgCommands.STOP,
  media_next_track: lgCommands.FAST_FORWARD,
  media_previous_track: lgCommands.REWIND,
};

const commandValue = (valueArg: unknown): number | undefined => {
  const numeric = numberValue(valueArg);
  if (numeric !== undefined) {
    return numeric;
  }
  const commandName = stringValue(valueArg)?.toUpperCase().replace(/[^A-Z0-9]+/g, '_');
  return commandName ? lgCommands[commandName] : undefined;
};

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

const textOf = (xmlArg: string, tagArg: string): string | undefined => decodeXml(firstXmlNode(xmlArg, tagArg)?.trim());
const decodeXml = (valueArg: string | undefined): string | undefined => valueArg?.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, '&');
const escapeXml = (valueArg: string): string => valueArg.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
const stringValue = (valueArg: unknown): string | undefined => typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
const numberValue = (valueArg: unknown): number | undefined => typeof valueArg === 'number' && Number.isFinite(valueArg) ? valueArg : stringValue(valueArg) && Number.isFinite(Number(valueArg)) ? Number(valueArg) : undefined;
const tvProtocolValue = (valueArg: unknown): TLgNetcastProtocol | undefined => valueArg === 'roap' || valueArg === 'hdcp' ? valueArg : undefined;
const parseUrl = (valueArg: string): URL | undefined => {
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(valueArg)) {
    return undefined;
  }
  try {
    return new URL(valueArg);
  } catch {
    return undefined;
  }
};
const portPart = (protocolArg: string, portArg: number): string => (protocolArg === 'http:' && portArg === 80) || (protocolArg === 'https:' && portArg === 443) ? '' : `:${portArg}`;
const cleanAttributes = (attributesArg: Record<string, unknown>): Record<string, unknown> | undefined => {
  const clean = Object.fromEntries(Object.entries(attributesArg).filter(([, valueArg]) => valueArg !== undefined));
  return Object.keys(clean).length ? clean : undefined;
};
const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
