import { EventEmitter } from 'events';
import { Config, MpvClient, MpvSubtitleRenderMetrics } from '../../types';
import {
  dispatchMpvProtocolMessage,
  MPV_REQUEST_ID_TRACK_LIST_AUDIO,
  MPV_REQUEST_ID_TRACK_LIST_SECONDARY,
  MpvMessage,
  MpvProtocolHandleMessageDeps,
  splitMpvMessagesFromBuffer,
} from './mpv-protocol';
import { requestMpvInitialState, subscribeToMpvProperties } from './mpv-properties';
import {
  scheduleMpvReconnect,
  MpvSocketTransport,
  MpvSocketTransportOptions,
} from './mpv-transport';
import { createLogger } from '../../logger';

const logger = createLogger('main:mpv');

export type MpvTrackProperty = {
  type?: string;
  id?: number;
  selected?: boolean;
  'ff-index'?: number;
};

export function resolveCurrentAudioStreamIndex(
  tracks: Array<MpvTrackProperty> | null | undefined,
  currentAudioTrackId: number | null,
): number | null {
  if (!Array.isArray(tracks)) {
    return null;
  }

  const audioTracks = tracks.filter((track) => track.type === 'audio');
  const activeTrack =
    audioTracks.find((track) => track.id === currentAudioTrackId) ||
    audioTracks.find((track) => track.selected === true);

  const ffIndex = activeTrack?.['ff-index'];
  return typeof ffIndex === 'number' && Number.isInteger(ffIndex) && ffIndex >= 0 ? ffIndex : null;
}

export interface MpvRuntimeClientLike {
  connected: boolean;
  send: (payload: { command: (string | number)[] }) => void;
  replayCurrentSubtitle?: () => void;
  playNextSubtitle?: () => void;
  setSubVisibility?: (visible: boolean) => void;
  setSecondarySubVisibility?: (visible: boolean) => void;
  setCurrentSecondarySubText?: (text: string) => void;
}

export function showMpvOsdRuntime(
  mpvClient: MpvRuntimeClientLike | null,
  text: string,
  fallbackLog: (text: string) => void = (line) => logger.info(line),
): boolean {
  if (mpvClient && mpvClient.connected) {
    const command = text.includes('${')
      ? ['expand-properties', 'show-text', text, '3000']
      : ['show-text', text, '3000'];
    mpvClient.send({ command });
    return true;
  }
  fallbackLog(`OSD (MPV not connected): ${text}`);
  return false;
}

export function replayCurrentSubtitleRuntime(mpvClient: MpvRuntimeClientLike | null): void {
  if (!mpvClient?.replayCurrentSubtitle) return;
  mpvClient.replayCurrentSubtitle();
}

export function playNextSubtitleRuntime(mpvClient: MpvRuntimeClientLike | null): void {
  if (!mpvClient?.playNextSubtitle) return;
  mpvClient.playNextSubtitle();
}

export function sendMpvCommandRuntime(
  mpvClient: MpvRuntimeClientLike | null,
  command: (string | number)[],
): void {
  if (!mpvClient) return;
  mpvClient.send({ command });
}

export function setMpvSubVisibilityRuntime(
  mpvClient: MpvRuntimeClientLike | null,
  visible: boolean,
): void {
  if (!mpvClient?.setSubVisibility) return;
  mpvClient.setSubVisibility(visible);
}

export function setMpvSecondarySubVisibilityRuntime(
  mpvClient: MpvRuntimeClientLike | null,
  visible: boolean,
): void {
  if (!mpvClient?.setSecondarySubVisibility) return;
  mpvClient.setSecondarySubVisibility(visible);
}

export { MPV_REQUEST_ID_SECONDARY_SUB_VISIBILITY } from './mpv-protocol';

export interface MpvIpcClientProtocolDeps {
  getResolvedConfig: () => Config;
  autoStartOverlay: boolean;
  setOverlayVisible: (visible: boolean) => void;
  isVisibleOverlayVisible: () => boolean;
  getReconnectTimer: () => ReturnType<typeof setTimeout> | null;
  setReconnectTimer: (timer: ReturnType<typeof setTimeout> | null) => void;
  shouldAutoLoadSecondarySubTrack?: (path: string) => boolean;
  shouldQuitOnMpvShutdown?: () => boolean;
  requestAppQuit?: () => void;
  socketFactory?: MpvSocketTransportOptions['socketFactory'];
  connectTimeoutMs?: number;
}

export interface MpvIpcClientDeps extends MpvIpcClientProtocolDeps {}

export interface MpvIpcClientEventMap {
  'connection-change': { connected: boolean };
  'subtitle-change': { text: string; isOverlayVisible: boolean };
  'subtitle-ass-change': { text: string };
  'subtitle-timing': { text: string; start: number; end: number };
  'time-pos-change': { time: number };
  'duration-change': { duration: number };
  'pause-change': { paused: boolean };
  'fullscreen-change': { fullscreen: boolean };
  'secondary-subtitle-change': { text: string };
  'subtitle-track-change': { sid: number | null };
  'secondary-subtitle-track-change': { sid: number | null };
  'secondary-subtitle-delay-change': { delay: number };
  'subtitle-track-list-change': { trackList: unknown[] | null };
  'media-path-change': { path: string };
  'media-title-change': { title: string | null };
  'subtitle-metrics-change': { patch: Partial<MpvSubtitleRenderMetrics> };
  'secondary-subtitle-visibility': { visible: boolean };
  'client-message': { args: string[] };
}

type MpvIpcClientEventName = keyof MpvIpcClientEventMap;

export class MpvIpcClient implements MpvClient {
  private deps: MpvIpcClientProtocolDeps;
  private transport: MpvSocketTransport;
  private socketPath: string;
  public socket: ReturnType<MpvSocketTransport['getSocket']> = null;
  private eventBus = new EventEmitter();
  private buffer = '';
  public connected = false;
  private connecting = false;
  private reconnectAttempt = 0;
  private firstConnection = true;
  private hasConnectedOnce = false;
  private socketErrorWarnedForDisconnect = false;
  public currentVideoPath = '';
  public currentMediaTitle: string | null = null;
  public currentTimePos = 0;
  public currentSubStart = 0;
  public currentSubEnd = 0;
  public currentSubText = '';
  public currentSecondarySubText = '';
  public currentAudioStreamIndex: number | null = null;
  private currentAudioTrackId: number | null = null;
  private mpvSubtitleRenderMetrics: MpvSubtitleRenderMetrics = {
    subPos: 100,
    subFontSize: 36,
    subScale: 1,
    subMarginY: 0,
    subMarginX: 0,
    subFont: '',
    subSpacing: 0,
    subBold: false,
    subItalic: false,
    subBorderSize: 0,
    subShadowOffset: 0,
    subAssOverride: 'yes',
    subScaleByWindow: true,
    subUseMargins: true,
    osdHeight: 0,
    osdDimensions: null,
  };
  private previousSecondarySubVisibility: boolean | null = null;
  private enforceSecondarySubVisibilityHidden = true;
  private playbackPaused: boolean | null = null;
  private pauseAtTime: number | null = null;
  private pendingPauseAtSubEnd = false;
  private nextDynamicRequestId = 1000;
  private pendingRequests = new Map<number, (message: MpvMessage) => void>();

  constructor(socketPath: string, deps: MpvIpcClientDeps) {
    this.deps = deps;
    this.socketPath = socketPath;

    this.transport = new MpvSocketTransport({
      socketPath,
      socketFactory: deps.socketFactory,
      connectTimeoutMs: deps.connectTimeoutMs,
      onConnect: () => {
        this.enforceSecondarySubVisibilityHidden = true;
        this.connected = true;
        this.connecting = false;
        this.socket = this.transport.getSocket();
        this.reconnectAttempt = 0;
        this.hasConnectedOnce = true;
        this.socketErrorWarnedForDisconnect = false;
        const resolvedConfig = this.deps.getResolvedConfig();
        logger.info('MPV IPC socket connected', {
          socketPath: this.socketPath,
          autoStartOverlay: this.deps.autoStartOverlay,
          configAutoStartOverlay: resolvedConfig.auto_start_overlay === true,
        });
        this.setSecondarySubVisibility(false);
        subscribeToMpvProperties(this.send.bind(this));
        requestMpvInitialState(this.send.bind(this));
        this.emit('connection-change', { connected: true });

        const shouldAutoStart =
          this.deps.autoStartOverlay || resolvedConfig.auto_start_overlay === true;
        if (this.firstConnection && shouldAutoStart) {
          logger.debug('Auto-starting overlay, hiding mpv subtitles');
          setTimeout(() => {
            this.deps.setOverlayVisible(true);
          }, 100);
        }

        this.firstConnection = false;
      },
      onData: (data) => {
        this.buffer += data.toString();
        this.processBuffer();
      },
      onError: (err: Error) => {
        this.logSocketError(err);
        this.failPendingRequests();
      },
      onClose: () => {
        const wasConnected = this.connected;
        const shouldQuitOnMpvShutdown = this.deps.shouldQuitOnMpvShutdown?.() === true;
        if (wasConnected) {
          logger.warn('MPV IPC socket closed', {
            socketPath: this.socketPath,
            shouldQuitOnMpvShutdown,
          });
        } else {
          logger.debug('MPV IPC socket closed before first connection', {
            socketPath: this.socketPath,
            reconnectAttempt: this.reconnectAttempt,
          });
        }
        this.connected = false;
        this.connecting = false;
        this.socket = null;
        this.playbackPaused = null;
        this.emit('connection-change', { connected: false });
        this.failPendingRequests();
        if (shouldQuitOnMpvShutdown) {
          this.deps.requestAppQuit?.();
          return;
        }
        this.scheduleReconnect();
      },
    });
  }

  on<EventName extends MpvIpcClientEventName>(
    event: EventName,
    listener: (payload: MpvIpcClientEventMap[EventName]) => void,
  ): void {
    this.eventBus.on(event as string, listener);
  }

  off<EventName extends MpvIpcClientEventName>(
    event: EventName,
    listener: (payload: MpvIpcClientEventMap[EventName]) => void,
  ): void {
    this.eventBus.off(event as string, listener);
  }

  private emit<EventName extends MpvIpcClientEventName>(
    event: EventName,
    payload: MpvIpcClientEventMap[EventName],
  ): void {
    this.eventBus.emit(event as string, payload);
  }

  private emitSubtitleMetricsChange(patch: Partial<MpvSubtitleRenderMetrics>): void {
    this.mpvSubtitleRenderMetrics = {
      ...this.mpvSubtitleRenderMetrics,
      ...patch,
    };
    this.emit('subtitle-metrics-change', { patch });
  }

  setSocketPath(socketPath: string): void {
    if (socketPath !== this.socketPath) {
      logger.info('MPV IPC socket path updated', {
        previousSocketPath: this.socketPath,
        socketPath,
      });
      if (this.connecting && !this.connected) {
        // Abort the in-flight dial to the old path; otherwise the connecting
        // latch turns every later connect() into a no-op while we hang on a
        // stale socket.
        logger.debug('Aborting in-flight MPV IPC connect for socket path change.');
        this.transport.shutdown();
        this.connecting = false;
      }
    }
    this.socketPath = socketPath;
    this.transport.setSocketPath(socketPath);
  }

  connect(): void {
    if (this.connected || this.connecting) {
      logger.debug(
        `MPV IPC connect request skipped; connected=${this.connected}, connecting=${this.connecting}`,
      );
      return;
    }

    logger.debug('MPV IPC connect requested.');
    this.connecting = true;
    this.transport.connect();
  }

  reconnect(): void {
    logger.debug('MPV IPC reconnect requested.');
    const wasConnected = this.connected;
    this.transport.shutdown();
    this.connected = false;
    this.connecting = false;
    this.socket = null;
    this.playbackPaused = null;
    if (wasConnected) {
      this.emit('connection-change', { connected: false });
    }
    this.failPendingRequests();
    this.connect();
  }

  private scheduleReconnect(): void {
    this.reconnectAttempt = scheduleMpvReconnect({
      attempt: this.reconnectAttempt,
      hasConnectedOnce: this.hasConnectedOnce,
      getReconnectTimer: () => this.deps.getReconnectTimer(),
      setReconnectTimer: (timer) => this.deps.setReconnectTimer(timer),
      onReconnectAttempt: (attempt, delay) => {
        logger.debug(`Attempting to reconnect to MPV (attempt ${attempt}, delay ${delay}ms)...`, {
          socketPath: this.socketPath,
        });
      },
      connect: () => {
        this.connect();
      },
    });
  }

  private shouldLogPreConnectionFailure(): boolean {
    const nextAttempt = this.reconnectAttempt + 1;
    return nextAttempt <= 3 || nextAttempt % 10 === 0;
  }

  private logSocketError(err: Error): void {
    const errorWithCode = err as Error & { code?: unknown };
    const details = {
      socketPath: this.socketPath,
      reconnectAttempt: this.reconnectAttempt,
      hasConnectedOnce: this.hasConnectedOnce,
      message: err.message,
      code: typeof errorWithCode.code === 'string' ? errorWithCode.code : undefined,
    };

    if (!this.hasConnectedOnce) {
      if (this.shouldLogPreConnectionFailure()) {
        logger.warn('MPV IPC socket error', details);
        return;
      }
      logger.debug('MPV IPC socket error', details);
      return;
    }

    if (!this.socketErrorWarnedForDisconnect) {
      this.socketErrorWarnedForDisconnect = true;
      logger.warn('MPV IPC socket error', details);
      return;
    }

    logger.debug('MPV IPC socket error', details);
  }

  private processBuffer(): void {
    const parsed = splitMpvMessagesFromBuffer(
      this.buffer,
      (message) => {
        this.handleMessage(message);
      },
      (line, error) => {
        logger.error('Failed to parse MPV message:', line, error);
      },
    );
    this.buffer = parsed.nextBuffer;
  }

  private async handleMessage(msg: MpvMessage): Promise<void> {
    await dispatchMpvProtocolMessage(msg, this.createProtocolMessageDeps());
  }

  private createProtocolMessageDeps(): MpvProtocolHandleMessageDeps {
    return {
      getResolvedConfig: () => this.deps.getResolvedConfig(),
      getSubtitleMetrics: () => this.mpvSubtitleRenderMetrics,
      isVisibleOverlayVisible: () => this.deps.isVisibleOverlayVisible(),
      emitSubtitleChange: (payload) => {
        this.emit('subtitle-change', payload);
      },
      emitSubtitleAssChange: (payload) => {
        this.emit('subtitle-ass-change', payload);
      },
      emitSubtitleTiming: (payload) => {
        this.emit('subtitle-timing', payload);
      },
      emitTimePosChange: (payload) => {
        this.emit('time-pos-change', payload);
      },
      emitDurationChange: (payload) => {
        this.emit('duration-change', payload);
      },
      emitPauseChange: (payload) => {
        this.playbackPaused = payload.paused;
        this.emit('pause-change', payload);
      },
      emitFullscreenChange: (payload) => {
        this.emit('fullscreen-change', payload);
      },
      emitSecondarySubtitleChange: (payload) => {
        this.emit('secondary-subtitle-change', payload);
      },
      emitSubtitleTrackChange: (payload) => {
        this.emit('subtitle-track-change', payload);
      },
      emitSecondarySubtitleTrackChange: (payload) => {
        this.emit('secondary-subtitle-track-change', payload);
      },
      emitSecondarySubtitleDelayChange: (payload) => {
        this.emit('secondary-subtitle-delay-change', payload);
      },
      emitSubtitleTrackListChange: (payload) => {
        this.emit('subtitle-track-list-change', payload);
      },
      getCurrentSubText: () => this.currentSubText,
      setCurrentSubText: (text: string) => {
        this.currentSubText = text;
      },
      setCurrentSubStart: (value: number) => {
        this.currentSubStart = value;
      },
      getCurrentSubStart: () => this.currentSubStart,
      setCurrentSubEnd: (value: number) => {
        this.currentSubEnd = value;
      },
      getCurrentSubEnd: () => this.currentSubEnd,
      emitMediaPathChange: (payload) => {
        this.emit('media-path-change', payload);
      },
      emitMediaTitleChange: (payload) => {
        this.currentMediaTitle = payload.title;
        this.emit('media-title-change', payload);
      },
      emitSubtitleMetricsChange: (patch) => {
        this.emitSubtitleMetricsChange(patch);
      },
      setCurrentSecondarySubText: (text: string) => {
        this.currentSecondarySubText = text;
      },
      resolvePendingRequest: (requestId: number, message: MpvMessage) =>
        this.tryResolvePendingRequest(requestId, message),
      shouldEnforceSecondarySubVisibilityHidden: () => this.enforceSecondarySubVisibilityHidden,
      setSecondarySubVisibility: (visible: boolean) => this.setSecondarySubVisibility(visible),
      syncCurrentAudioStreamIndex: () => {
        this.syncCurrentAudioStreamIndex();
      },
      setCurrentAudioTrackId: (value: number | null) => {
        this.currentAudioTrackId = value;
      },
      setCurrentTimePos: (value: number) => {
        this.currentTimePos = value;
      },
      getCurrentTimePos: () => this.currentTimePos,
      getPendingPauseAtSubEnd: () => this.pendingPauseAtSubEnd,
      setPendingPauseAtSubEnd: (value: boolean) => {
        this.pendingPauseAtSubEnd = value;
      },
      getPauseAtTime: () => this.pauseAtTime,
      setPauseAtTime: (value: number | null) => {
        this.pauseAtTime = value;
      },
      autoLoadSecondarySubTrack: (path: string) => {
        this.autoLoadSecondarySubTrack(path);
      },
      setCurrentVideoPath: (value: string) => {
        this.currentVideoPath = value;
        this.currentMediaTitle = null;
      },
      emitSecondarySubtitleVisibility: (payload) => {
        this.emit('secondary-subtitle-visibility', payload);
      },
      setPreviousSecondarySubVisibility: (visible: boolean) => {
        this.previousSecondarySubVisibility = visible;
      },
      setCurrentAudioStreamIndex: (tracks) => {
        this.updateCurrentAudioStreamIndex(tracks);
      },
      sendCommand: (payload) => this.send(payload),
      restorePreviousSecondarySubVisibility: () => {
        this.restorePreviousSecondarySubVisibility();
      },
      shouldQuitOnMpvShutdown: () => this.deps.shouldQuitOnMpvShutdown?.() ?? false,
      requestAppQuit: () => this.deps.requestAppQuit?.(),
      emitClientMessage: (payload) => {
        this.emit('client-message', payload);
      },
    };
  }

  private autoLoadSecondarySubTrack(path: string): void {
    const normalizedPath = path.trim();
    if (!normalizedPath) return;
    if (this.deps.shouldAutoLoadSecondarySubTrack?.(normalizedPath) === false) {
      return;
    }
    const config = this.deps.getResolvedConfig();
    if (!config.secondarySub?.autoLoadSecondarySub) return;
    const languages = config.secondarySub.secondarySubLanguages;
    if (!languages || languages.length === 0) return;

    setTimeout(() => {
      this.send({
        command: ['get_property', 'track-list'],
        request_id: MPV_REQUEST_ID_TRACK_LIST_SECONDARY,
      });
    }, 500);
  }

  private syncCurrentAudioStreamIndex(): void {
    this.send({
      command: ['get_property', 'track-list'],
      request_id: MPV_REQUEST_ID_TRACK_LIST_AUDIO,
    });
  }

  private updateCurrentAudioStreamIndex(
    tracks: Array<{
      type?: string;
      id?: number;
      selected?: boolean;
      'ff-index'?: number;
    }>,
  ): void {
    this.currentAudioStreamIndex = resolveCurrentAudioStreamIndex(tracks, this.currentAudioTrackId);
  }

  send(command: { command: unknown[]; request_id?: number }): boolean {
    if (!this.connected || !this.socket) {
      return false;
    }
    return this.transport.send(command);
  }

  request(command: unknown[]): Promise<MpvMessage> {
    return new Promise((resolve, reject) => {
      if (!this.connected || !this.socket) {
        reject(new Error('MPV not connected'));
        return;
      }

      const requestId = this.nextDynamicRequestId++;
      this.pendingRequests.set(requestId, resolve);
      const sent = this.send({ command, request_id: requestId });
      if (!sent) {
        this.pendingRequests.delete(requestId);
        reject(new Error('Failed to send MPV request'));
        return;
      }

      setTimeout(() => {
        if (this.pendingRequests.delete(requestId)) {
          reject(new Error('MPV request timed out'));
        }
      }, 4000);
    });
  }

  async requestProperty(name: string): Promise<unknown> {
    const response = await this.request(['get_property', name]);
    if (response.error && response.error !== 'success') {
      throw new Error(`Failed to read MPV property '${name}': ${response.error}`);
    }
    return response.data;
  }

  private failPendingRequests(): void {
    for (const [requestId, resolve] of this.pendingRequests.entries()) {
      resolve({ request_id: requestId, error: 'disconnected' });
    }
    this.pendingRequests.clear();
  }

  private tryResolvePendingRequest(requestId: number, message: MpvMessage): boolean {
    const pending = this.pendingRequests.get(requestId);
    if (!pending) {
      return false;
    }
    this.pendingRequests.delete(requestId);
    pending(message);
    return true;
  }

  setSubVisibility(visible: boolean): void {
    const value = visible ? 'yes' : 'no';
    this.send({
      command: ['set_property', 'sub-visibility', visible],
    });
    this.send({
      command: ['set_property', 'sub-visibility', value],
    });
    // Compatibility write for mpv command aliases across setups.
    this.send({
      command: ['set', 'sub-visibility', value],
    });
  }

  replayCurrentSubtitle(): void {
    this.pendingPauseAtSubEnd = true;
    this.send({ command: ['sub-seek', 0] });
  }

  playNextSubtitle(): void {
    this.pendingPauseAtSubEnd = true;
    this.pauseAtTime = null;
    this.send({ command: ['sub-seek', 1] });
    if (this.playbackPaused !== false) {
      this.send({ command: ['set_property', 'pause', false] });
    }
  }

  restorePreviousSecondarySubVisibility(): void {
    const previous = this.previousSecondarySubVisibility;
    if (previous === null) return;
    const restored = this.send({
      command: ['set_property', 'secondary-sub-visibility', previous ? 'yes' : 'no'],
    });
    if (!restored) return;
    this.enforceSecondarySubVisibilityHidden = false;
    this.previousSecondarySubVisibility = null;
  }

  setSecondarySubVisibility(visible: boolean): void {
    this.send({
      command: ['set_property', 'secondary-sub-visibility', visible ? 'yes' : 'no'],
    });
  }
}
