import { SimpleLocalClient, SimpleLocalMapper, type ISimpleLocalSnapshot, type IServiceCallRequest, type IServiceCallResult, type TSimpleLocalRawData } from '../../core/index.js';
import type { IPanasonicBlurayConfig } from './panasonic_bluray.types.js';
import { panasonicBlurayDefaultName, panasonicBlurayDefaultPort, panasonicBlurayProfile } from './panasonic_bluray.types.js';

type TPanasonicVariant = 'auto' | 'bd' | 'ub';

interface IPanasonicBlurayEndpoint {
  host: string;
  port: number;
  baseUrl: string;
  controlUrl: string;
}

interface IPanasonicCommandResult {
  kind: 'ok' | 'off' | 'error';
  values?: string[];
}

interface IPanasonicPlayStatus extends Record<string, unknown> {
  state: string;
  mediaPosition: number;
  mediaDuration: number;
  rawState?: string[];
  rawStatus?: string[];
  variant: TPanasonicVariant;
}

export class PanasonicBlurayClient extends SimpleLocalClient<IPanasonicBlurayConfig> {
  private cachedSnapshot?: ISimpleLocalSnapshot;
  private variant: TPanasonicVariant;

  constructor(private readonly configArg: IPanasonicBlurayConfig) {
    super(panasonicBlurayProfile, configArg);
    this.variant = normalizedVariant(configArg.playerVariant || configArg.variant);
  }

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

    if (this.shouldUseNativeHttp(forceRefreshArg)) {
      try {
        this.cachedSnapshot = this.snapshotFromPlayStatus(this.requireEndpoint(), await this.getPlayStatus());
      } catch (errorArg) {
        this.cachedSnapshot = this.createOfflineSnapshot(errorMessage(errorArg));
      }
      return SimpleLocalMapper.clone(this.cachedSnapshot);
    }

    const snapshot = await super.getSnapshot(forceRefreshArg);
    this.cachedSnapshot = SimpleLocalMapper.toSnapshot({
      profile: panasonicBlurayProfile,
      config: this.configArg,
      rawData: (snapshot.rawData as TSimpleLocalRawData | undefined) ?? snapshot,
      online: snapshot.online,
      source: snapshot.source,
      error: snapshot.error,
    });
    return SimpleLocalMapper.clone(this.cachedSnapshot);
  }

  public override async refresh(): Promise<IServiceCallResult> {
    this.cachedSnapshot = undefined;
    const liveCapable = Boolean(this.endpoint()) || Boolean(this.configArg.client || this.configArg.snapshotProvider);
    const snapshot = await this.getSnapshot(liveCapable);
    const success = snapshot.online && !snapshot.error && (liveCapable || this.hasManualData());
    return { success, error: success ? undefined : snapshot.error || 'Panasonic Blu-Ray refresh requires a local HTTP host, injected client, snapshotProvider, snapshot, entities, or rawData.', data: snapshot };
  }

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

    try {
      const result = await this.executeNative(requestArg);
      if (result.kind !== 'ok') {
        return { success: false, error: `Panasonic Blu-Ray service ${requestArg.domain}.${requestArg.service} returned ${result.kind}.`, data: result };
      }
      this.cachedSnapshot = undefined;
      return { success: true, data: result };
    } catch (errorArg) {
      return { success: false, error: errorMessage(errorArg) };
    }
  }

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

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

  private async executeNative(requestArg: IServiceCallRequest): Promise<IPanasonicCommandResult> {
    if (requestArg.service === 'turn_on' || requestArg.service === 'turn_off') {
      const snapshot = await this.getSnapshot(true);
      const state = snapshot.entities[0]?.state;
      if (requestArg.service === 'turn_on' && state !== 'off') {
        return { kind: 'ok', values: ['already_on'] };
      }
      if (requestArg.service === 'turn_off' && state === 'off') {
        return { kind: 'ok', values: ['already_off'] };
      }
      return this.sendKey('POWER');
    }
    return this.sendKey(this.keyForService(requestArg));
  }

  private keyForService(requestArg: IServiceCallRequest): string {
    if (requestArg.service === 'media_play') {
      return 'PLAYBACK';
    }
    if (requestArg.service === 'media_pause') {
      return 'PAUSE';
    }
    if (requestArg.service === 'media_stop') {
      return 'STOP';
    }
    throw new Error(`Unsupported Panasonic Blu-Ray service: ${requestArg.domain}.${requestArg.service}`);
  }

  private async sendKey(keyArg: string): Promise<IPanasonicCommandResult> {
    if (!panasonicKeys.has(keyArg)) {
      return { kind: 'error' };
    }
    if (this.variant === 'ub') {
      return { kind: 'error' };
    }
    const result = await this.sendForm(`cCMD_RC_${keyArg}.x=100&cCMD_RC_${keyArg}.y=100`);
    if (this.variant === 'auto') {
      this.variant = result.kind === 'error' ? 'ub' : 'bd';
    }
    return result;
  }

  private async getPlayStatus(): Promise<IPanasonicPlayStatus> {
    const playResponse = await this.sendForm('cCMD_PST.x=100&cCMD_PST.y=100');
    if (playResponse.kind === 'off') {
      return { state: 'off', mediaPosition: 0, mediaDuration: 0, variant: this.variant };
    }
    if (playResponse.kind === 'error' || !playResponse.values) {
      return { state: 'error', mediaPosition: 0, mediaDuration: 0, variant: this.variant };
    }

    const status = await this.getStatus();
    if (status.kind === 'off') {
      return { state: 'off', mediaPosition: 0, mediaDuration: 0, rawState: playResponse.values, variant: this.variant };
    }
    if (status.kind === 'error' || !status.values) {
      return { state: 'error', mediaPosition: 0, mediaDuration: 0, rawState: playResponse.values, variant: this.variant };
    }

    const state = playResponse.values;
    let mediaState = 'unknown';
    if (state[0] === '0') {
      mediaState = status.values[0] === '0' ? 'standby' : 'stopped';
    } else if (state[0] === '1') {
      mediaState = 'playing';
    } else if (state[0] === '2') {
      mediaState = 'paused';
    }

    return {
      state: mediaState,
      mediaPosition: Math.max(0, Math.trunc(numberValue(state[1]) ?? 0)),
      mediaDuration: Math.max(0, Math.trunc(numberValue(status.values[4]) ?? 0)),
      rawState: state,
      rawStatus: status.values,
      variant: this.variant,
    };
  }

  private async getStatus(): Promise<IPanasonicCommandResult> {
    if (this.variant === 'ub') {
      return { kind: 'ok', values: ['1', '0', '0', '00000000', '0'] };
    }
    const result = await this.sendForm('cCMD_GET_STATUS.x=100&cCMD_GET_STATUS.y=100');
    if (result.kind === 'error' && this.variant === 'auto') {
      this.variant = 'ub';
      return { kind: 'ok', values: ['1', '0', '0', '00000000', '0'] };
    }
    if (this.variant === 'auto' && result.kind === 'ok') {
      this.variant = 'bd';
    }
    return result;
  }

  private async sendForm(bodyArg: string): Promise<IPanasonicCommandResult> {
    const endpoint = this.requireEndpoint();
    const timeoutMs = this.configArg.timeoutMs || 5000;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      const response = await globalThis.fetch(endpoint.controlUrl, {
        method: 'POST',
        headers: {
          'content-type': 'application/x-www-form-urlencoded',
          'user-agent': 'MEI-LAN-REMOTE-CALL',
        },
        body: bodyArg,
        signal: controller.signal,
      });
      const text = await response.text();
      if (!response.ok) {
        return { kind: 'error' };
      }
      const lines = text.split(/\r?\n/).filter((lineArg) => lineArg.length > 0);
      if ((lines[0] || '').split(',')[0] !== '00') {
        return { kind: 'error' };
      }
      return { kind: 'ok', values: (lines[1] || '').split(',').filter((itemArg) => itemArg.length > 0) };
    } catch {
      return { kind: 'off' };
    } finally {
      clearTimeout(timer);
    }
  }

  private snapshotFromPlayStatus(endpointArg: IPanasonicBlurayEndpoint, statusArg: IPanasonicPlayStatus): ISimpleLocalSnapshot {
    const state = mediaPlayerState(statusArg.state);
    return {
      device: {
        id: this.configArg.uniqueId || this.configArg.deviceId || `${endpointArg.host}:${endpointArg.port}`,
        name: this.configArg.name || panasonicBlurayDefaultName,
        manufacturer: 'Panasonic',
        model: 'Panasonic Blu-Ray Player',
        host: endpointArg.host,
        port: endpointArg.port,
        protocol: 'http',
        configurationUrl: endpointArg.baseUrl,
      },
      entities: [{
        id: 'player',
        name: 'Player',
        platform: 'media_player',
        state,
        available: state !== 'unknown',
        writable: true,
        attributes: {
          playStatus: statusArg.state,
          mediaPosition: statusArg.mediaPosition,
          mediaDuration: statusArg.mediaDuration,
          variant: statusArg.variant,
        },
      }],
      online: state !== 'unknown',
      updatedAt: new Date().toISOString(),
      source: 'http',
      rawData: statusArg,
      capabilities: {
        localRead: true,
        localControl: true,
        panacottaHttpApi: true,
      },
    };
  }

  private requireEndpoint(): IPanasonicBlurayEndpoint {
    const endpoint = this.endpoint();
    if (!endpoint) {
      throw new Error('Panasonic Blu-Ray local HTTP API requires config.host or config.url.');
    }
    return endpoint;
  }

  private endpoint(): IPanasonicBlurayEndpoint | undefined {
    const rawUrl = stringValue(this.configArg.url) || stringValue(this.configArg.host);
    if (!rawUrl) {
      return undefined;
    }

    try {
      const url = new URL(rawUrl.includes('://') ? rawUrl : `http://${rawUrl}`);
      if (url.protocol !== 'http:') {
        return undefined;
      }
      return endpointFromParts(url.hostname, this.configArg.port || numberValue(url.port) || panasonicBlurayDefaultPort);
    } catch {
      return endpointFromParts(rawUrl, this.configArg.port || panasonicBlurayDefaultPort);
    }
  }

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

  private createOfflineSnapshot(errorArg: string): ISimpleLocalSnapshot {
    return SimpleLocalMapper.toSnapshot({ profile: panasonicBlurayProfile, config: this.configArg, online: false, source: 'runtime', error: errorArg });
  }
}

const panasonicKeys = new Set([
  'POWER', 'OP_CL', 'PLAYBACK', 'PAUSE', 'STOP', 'SKIPFWD', 'SKIPREV', 'REV', 'D0', 'D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7', 'D8', 'D9', 'D12', 'SHARP', 'CLEAR', 'UP', 'DOWN', 'LEFT', 'RIGHT', 'SELECT', 'RETURN', 'EXIT', 'MLTNAVI', 'DSPSEL', 'TITLE', 'MENU', 'PUPMENU', 'SHFWD1', 'SHFWD2', 'SHFWD3', 'SHFWD4', 'SHFWD5', 'SHREV1', 'SHREV2', 'SHREV3', 'SHREV4', 'SHREV5', 'JLEFT', 'JRIGHT', 'RED', 'BLUE', 'GREEN', 'YELLOW', 'NETFLIX', 'SKYPE', 'V_CAST', '3D', 'NETWORK', 'AUDIOSEL', 'KEYS', 'CUE', 'CHROMA', 'OSDONOFF', 'P_IN_P',
]);

const mediaPlayerState = (stateArg: string): string => {
  if (stateArg === 'off' || stateArg === 'standby') {
    return 'off';
  }
  if (stateArg === 'paused' || stateArg === 'stopped') {
    return 'idle';
  }
  if (stateArg === 'playing') {
    return 'playing';
  }
  return 'unknown';
};

const endpointFromParts = (hostArg: string, portArg: number): IPanasonicBlurayEndpoint => {
  const portPart = portArg === 80 ? '' : `:${portArg}`;
  const baseUrl = `http://${hostArg}${portPart}`;
  return { host: hostArg, port: portArg, baseUrl, controlUrl: `${baseUrl}/WAN/dvdr/dvdr_ctrl.cgi` };
};

const normalizedVariant = (valueArg: unknown): TPanasonicVariant => {
  const value = stringValue(valueArg)?.toLowerCase();
  return value === 'bd' || value === 'ub' ? value : 'auto';
};

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 valueArg;
  }
  if (typeof valueArg === 'string' && valueArg.trim() && Number.isFinite(Number(valueArg))) {
    return Number(valueArg);
  }
  return undefined;
};
const errorMessage = (errorArg: unknown): string => errorArg instanceof Error ? errorArg.message : String(errorArg);
