import type {
  IEmbyClientLike,
  IEmbyCommandRequest,
  IEmbyConfig,
  IEmbyGeneralCommandRequest,
  IEmbyRefreshResult,
  IEmbyServerInfo,
  IEmbySession,
  IEmbySnapshot,
  TEmbyGeneralCommand,
  TEmbyPlayCommand,
  TEmbyPlaystateCommand,
} from './emby.types.js';
import { embyDefaultHttpPort, embyDefaultHttpsPort, embyDefaultTimeoutMs, embyDisplayName } from './emby.types.js';

const clientName = 'smarthome.exchange';
const clientVersion = '0.1.0';

export class EmbyApiError extends Error {}
export class EmbyApiConnectionError extends EmbyApiError {}
export class EmbyApiAuthorizationError extends EmbyApiError {}
export class EmbyUnsupportedCommandError extends EmbyApiError {}

export class EmbyUnsupportedLivePushError extends Error {
  constructor() {
    super('Emby live push via pyemby/websocket is not implemented in this native integration; use snapshot polling or an injected client.');
    this.name = 'EmbyUnsupportedLivePushError';
  }
}

export class EmbyClient {
  private currentSnapshot?: IEmbySnapshot;

  constructor(private readonly config: IEmbyConfig) {}

  public async getSnapshot(forceRefreshArg = false): Promise<IEmbySnapshot> {
    if (!forceRefreshArg && this.currentSnapshot) {
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (!forceRefreshArg && this.config.snapshot) {
      this.currentSnapshot = this.normalizeSnapshot({ ...this.cloneSnapshot(this.config.snapshot), source: 'snapshot' });
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (this.config.client) {
      try {
        this.currentSnapshot = await this.snapshotFromClient(this.config.client);
      } catch (errorArg) {
        this.currentSnapshot = this.offlineSnapshot(this.errorMessage(errorArg));
      }
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (!forceRefreshArg && this.hasManualData()) {
      this.currentSnapshot = this.normalizeSnapshot({
        server: this.serverInfoFromConfig(),
        sessions: this.config.sessions || [],
        users: this.config.users,
        online: this.config.online ?? true,
        updatedAt: new Date().toISOString(),
        source: 'manual',
      });
      return this.cloneSnapshot(this.currentSnapshot);
    }

    if (this.hasEndpoint()) {
      try {
        this.currentSnapshot = await this.fetchSnapshot();
      } catch (errorArg) {
        this.currentSnapshot = this.offlineSnapshot(this.errorMessage(errorArg));
      }
      return this.cloneSnapshot(this.currentSnapshot);
    }

    this.currentSnapshot = this.offlineSnapshot('No Emby local endpoint, injected client, snapshot, or manual session data is configured.');
    return this.cloneSnapshot(this.currentSnapshot);
  }

  public async refresh(): Promise<IEmbyRefreshResult> {
    try {
      this.currentSnapshot = undefined;
      const liveCapable = Boolean(this.config.client || this.hasEndpoint());
      const snapshot = await this.getSnapshot(liveCapable);
      const success = liveCapable && snapshot.online && !snapshot.error && snapshot.source !== 'runtime';
      return { success, snapshot, error: success ? undefined : snapshot.error || 'Emby refresh requires a live local endpoint or injected client.', data: { source: snapshot.source } };
    } catch (errorArg) {
      const error = this.errorMessage(errorArg);
      const snapshot = this.offlineSnapshot(error);
      this.currentSnapshot = snapshot;
      return { success: false, snapshot: this.cloneSnapshot(snapshot), error };
    }
  }

  public async fetchSnapshot(): Promise<IEmbySnapshot> {
    if (!this.hasEndpoint()) {
      throw new EmbyApiConnectionError('Emby local API snapshot requires config.host or config.url.');
    }

    const token = this.accessToken();
    const server = await this.getSystemInfo();
    const sessions = token ? await this.getSessions() : [];

    return this.normalizeSnapshot({
      server,
      sessions,
      users: this.config.users,
      online: true,
      updatedAt: new Date().toISOString(),
      source: 'http',
      error: token ? undefined : 'Emby session polling requires config.apiKey, apiToken, or accessToken.',
    });
  }

  public async getSystemInfo(): Promise<IEmbyServerInfo> {
    if (this.config.server && !this.hasEndpoint()) {
      return this.serverInfoFromConfig();
    }
    const token = this.accessToken();
    const path = token ? '/System/Info' : '/System/Info/Public';
    return this.fetchJson<IEmbyServerInfo>(path, { token });
  }

  public async getSessions(): Promise<IEmbySession[]> {
    const token = this.accessToken();
    if (!token) {
      return this.config.sessions || [];
    }
    const query: Record<string, string | number> = {};
    if (this.config.userId) {
      query.ControllableByUserId = this.config.userId;
    }
    if (this.config.deviceId) {
      query.DeviceId = this.config.deviceId;
    }
    if (this.config.sessionId) {
      query.Id = this.config.sessionId;
    }
    return this.fetchJson<IEmbySession[]>('/Sessions', { query, token });
  }

  public async execute(commandArg: IEmbyCommandRequest): Promise<unknown> {
    if (this.config.commandExecutor) {
      return this.config.commandExecutor.execute(commandArg);
    }
    if (this.config.client?.execute) {
      return this.config.client.execute(commandArg);
    }
    if (commandArg.action === 'refresh') {
      return (await this.refresh()).snapshot;
    }
    if (!this.hasEndpoint()) {
      throw new EmbyApiConnectionError('Emby commands require config.host/config.url with an API key, an injected client.execute, or commandExecutor. Static snapshots/manual data are read-only.');
    }
    if (!this.accessToken()) {
      throw new EmbyApiAuthorizationError('Emby commands require config.apiKey, apiToken, or accessToken.');
    }

    if (commandArg.action === 'playstate') {
      if (!commandArg.sessionId || !commandArg.command) {
        throw new EmbyUnsupportedCommandError('Emby playstate commands require sessionId and command.');
      }
      await this.sendPlaystateCommand(commandArg.sessionId, commandArg.command as TEmbyPlaystateCommand, commandArg.seekPositionTicks);
      this.currentSnapshot = undefined;
      return { command: commandArg };
    }

    if (commandArg.action === 'general_command') {
      if (!commandArg.sessionId || !commandArg.command) {
        throw new EmbyUnsupportedCommandError('Emby general commands require sessionId and command.');
      }
      await this.sendGeneralCommandToApi(commandArg.sessionId, commandArg.command, commandArg.arguments);
      this.currentSnapshot = undefined;
      return { command: commandArg };
    }

    if (commandArg.action === 'play_media') {
      if (!commandArg.sessionId || !commandArg.itemIds?.length) {
        throw new EmbyUnsupportedCommandError('Emby play_media requires sessionId and itemIds.');
      }
      await this.playMediaToApi(commandArg.sessionId, commandArg.itemIds, commandArg.playCommand, commandArg.startPositionTicks);
      this.currentSnapshot = undefined;
      return { command: commandArg };
    }

    throw new EmbyUnsupportedCommandError(`Unsupported Emby command action: ${commandArg.action}`);
  }

  public async pause(sessionIdArg: string): Promise<void> {
    await this.execute({ action: 'playstate', sessionId: sessionIdArg, command: 'Pause' });
  }

  public async play(sessionIdArg: string): Promise<void> {
    await this.execute({ action: 'playstate', sessionId: sessionIdArg, command: 'Unpause' });
  }

  public async playPause(sessionIdArg: string): Promise<void> {
    await this.execute({ action: 'playstate', sessionId: sessionIdArg, command: 'PlayPause' });
  }

  public async stop(sessionIdArg: string): Promise<void> {
    await this.execute({ action: 'playstate', sessionId: sessionIdArg, command: 'Stop' });
  }

  public async nextTrack(sessionIdArg: string): Promise<void> {
    await this.execute({ action: 'playstate', sessionId: sessionIdArg, command: 'NextTrack' });
  }

  public async previousTrack(sessionIdArg: string): Promise<void> {
    await this.execute({ action: 'playstate', sessionId: sessionIdArg, command: 'PreviousTrack' });
  }

  public async seek(sessionIdArg: string, positionSecondsArg: number): Promise<void> {
    await this.execute({
      action: 'playstate',
      sessionId: sessionIdArg,
      command: 'Seek',
      seekPositionTicks: Math.max(0, Math.round(positionSecondsArg * 10000000)),
    });
  }

  public async setVolumeLevel(sessionIdArg: string, volumeLevelArg: number): Promise<void> {
    const volume = Math.max(0, Math.min(100, Math.round(volumeLevelArg * 100)));
    await this.execute({ action: 'general_command', sessionId: sessionIdArg, command: 'SetVolume', arguments: { Volume: String(volume) } });
  }

  public async volumeUp(sessionIdArg: string): Promise<void> {
    await this.execute({ action: 'general_command', sessionId: sessionIdArg, command: 'VolumeUp' });
  }

  public async volumeDown(sessionIdArg: string): Promise<void> {
    await this.execute({ action: 'general_command', sessionId: sessionIdArg, command: 'VolumeDown' });
  }

  public async mute(sessionIdArg: string): Promise<void> {
    await this.execute({ action: 'general_command', sessionId: sessionIdArg, command: 'Mute' });
  }

  public async unmute(sessionIdArg: string): Promise<void> {
    await this.execute({ action: 'general_command', sessionId: sessionIdArg, command: 'Unmute' });
  }

  public async playMedia(sessionIdArg: string, itemIdsArg: string[], commandArg: TEmbyPlayCommand = 'PlayNow', startPositionTicksArg?: number): Promise<void> {
    await this.execute({ action: 'play_media', sessionId: sessionIdArg, itemIds: itemIdsArg, playCommand: commandArg, startPositionTicks: startPositionTicksArg });
  }

  public async sendGeneralCommand(sessionIdArg: string, commandArg: TEmbyGeneralCommand, argumentsArg?: Record<string, string>): Promise<void> {
    await this.execute({ action: 'general_command', sessionId: sessionIdArg, command: commandArg, arguments: argumentsArg });
  }

  public async subscribeToLiveEvents(): Promise<never> {
    throw new EmbyUnsupportedLivePushError();
  }

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

  private async playMediaToApi(sessionIdArg: string, itemIdsArg: string[], commandArg: TEmbyPlayCommand = 'PlayNow', startPositionTicksArg?: number): Promise<void> {
    await this.fetchNoContent(`/Sessions/${encodeURIComponent(sessionIdArg)}/Playing`, {
      method: 'POST',
      token: await this.ensureAccessToken(),
      query: {
        ItemIds: itemIdsArg.join(','),
        PlayCommand: commandArg,
        StartPositionTicks: startPositionTicksArg,
      },
      body: {
        ControllingUserId: this.config.userId,
      },
    });
  }

  private async sendGeneralCommandToApi(sessionIdArg: string, commandArg: TEmbyGeneralCommand, argumentsArg?: Record<string, string>): Promise<void> {
    const body: IEmbyGeneralCommandRequest = {
      Name: commandArg,
      ControllingUserId: this.config.userId,
      Arguments: argumentsArg,
    };
    await this.fetchNoContent(`/Sessions/${encodeURIComponent(sessionIdArg)}/Command`, {
      method: 'POST',
      token: await this.ensureAccessToken(),
      body,
    });
  }

  private async sendPlaystateCommand(sessionIdArg: string, commandArg: TEmbyPlaystateCommand, seekPositionTicksArg?: number): Promise<void> {
    await this.fetchNoContent(`/Sessions/${encodeURIComponent(sessionIdArg)}/Playing/${encodeURIComponent(commandArg)}`, {
      method: 'POST',
      token: await this.ensureAccessToken(),
      body: {
        Command: commandArg,
        SeekPositionTicks: seekPositionTicksArg,
        ControllingUserId: this.config.userId,
      },
    });
  }

  private async snapshotFromClient(clientArg: IEmbyClientLike): Promise<IEmbySnapshot> {
    if (clientArg.getSnapshot) {
      const snapshot = await clientArg.getSnapshot();
      return this.normalizeSnapshot({ ...this.cloneSnapshot(snapshot), source: 'client' });
    }
    if (clientArg.getSystemInfo || clientArg.getSessions) {
      const [server, sessions] = await Promise.all([
        clientArg.getSystemInfo ? clientArg.getSystemInfo() : Promise.resolve(this.serverInfoFromConfig()),
        clientArg.getSessions ? clientArg.getSessions() : Promise.resolve(this.config.sessions || []),
      ]);
      return this.normalizeSnapshot({
        server,
        sessions,
        users: this.config.users,
        online: true,
        updatedAt: new Date().toISOString(),
        source: 'client',
      });
    }
    throw new EmbyApiConnectionError('Emby injected client must expose getSnapshot(), getSystemInfo(), getSessions(), or execute().');
  }

  private async ensureAccessToken(): Promise<string> {
    const token = this.accessToken();
    if (token) {
      return token;
    }
    throw new EmbyApiAuthorizationError('Emby REST sessions and control require config.apiKey, apiToken, or accessToken.');
  }

  private accessToken(): string | undefined {
    return this.config.apiKey || this.config.api_key || this.config.apiToken || this.config.accessToken;
  }

  private async fetchJson<T>(pathArg: string, optionsArg: IEmbyRequestOptions = {}): Promise<T> {
    const response = await this.fetchResponse(pathArg, optionsArg);
    const text = await response.text();
    if (!response.ok) {
      throw new EmbyApiConnectionError(`Emby request ${pathArg} failed with HTTP ${response.status}: ${text}`);
    }
    return text ? JSON.parse(text) as T : undefined as T;
  }

  private async fetchNoContent(pathArg: string, optionsArg: IEmbyRequestOptions): Promise<void> {
    const response = await this.fetchResponse(pathArg, optionsArg);
    const text = await response.text();
    if (!response.ok) {
      throw new EmbyApiConnectionError(`Emby request ${pathArg} failed with HTTP ${response.status}: ${text}`);
    }
  }

  private async fetchResponse(pathArg: string, optionsArg: IEmbyRequestOptions): Promise<Response> {
    const abortController = new AbortController();
    const timeout = setTimeout(() => abortController.abort(), this.config.timeoutMs || embyDefaultTimeoutMs);
    try {
      return await globalThis.fetch(this.requestUrl(pathArg, optionsArg.query), {
        method: optionsArg.method || 'GET',
        headers: this.headers(optionsArg.token || this.accessToken(), optionsArg.body !== undefined),
        body: optionsArg.body !== undefined ? JSON.stringify(optionsArg.body) : undefined,
        signal: abortController.signal,
      });
    } finally {
      clearTimeout(timeout);
    }
  }

  private requestUrl(pathArg: string, queryArg?: Record<string, string | number | boolean | undefined>): string {
    const url = new URL(pathArg, `${this.baseUrl()}/`);
    for (const [key, value] of Object.entries(queryArg || {})) {
      if (value !== undefined) {
        url.searchParams.set(key, String(value));
      }
    }
    const token = this.accessToken();
    if (token && !url.searchParams.has('api_key')) {
      url.searchParams.set('api_key', token);
    }
    return url.toString();
  }

  private headers(tokenArg: string | undefined, hasBodyArg: boolean): Record<string, string> {
    const headers: Record<string, string> = {
      accept: 'application/json',
      authorization: this.authorizationHeader(tokenArg),
    };
    if (hasBodyArg) {
      headers['content-type'] = 'application/json';
    }
    if (tokenArg) {
      headers['x-emby-token'] = tokenArg;
    }
    return headers;
  }

  private authorizationHeader(tokenArg: string | undefined): string {
    const parts: Record<string, string> = {
      Client: clientName,
      Device: this.config.deviceName || 'smarthome.exchange',
      DeviceId: this.clientDeviceId(),
      Version: clientVersion,
    };
    if (tokenArg) {
      parts.Token = tokenArg;
    }
    return `MediaBrowser ${Object.entries(parts).map(([key, value]) => `${key}=\"${value.replace(/\\/g, '\\\\').replace(/\"/g, '\\\"')}\"`).join(', ')}`;
  }

  private baseUrl(): string {
    if (this.config.url) {
      return this.config.url.replace(/\/+$/, '');
    }
    if (!this.config.host) {
      throw new EmbyApiConnectionError('Emby host or url is required for REST calls.');
    }
    const ssl = this.config.ssl === true || this.config.protocol === 'https';
    const protocol = ssl ? 'https' : 'http';
    const port = this.config.port || (ssl ? embyDefaultHttpsPort : embyDefaultHttpPort);
    return `${protocol}://${this.config.host}:${port}`;
  }

  private hasEndpoint(): boolean {
    return Boolean(this.config.url || this.config.host);
  }

  private hasManualData(): boolean {
    return Boolean(this.config.server || this.config.sessions?.length || this.config.users?.length);
  }

  private clientDeviceId(): string {
    if (this.config.clientDeviceId) {
      return this.config.clientDeviceId;
    }
    if (this.config.uniqueId) {
      return this.config.uniqueId;
    }
    return 'smarthome-exchange-emby';
  }

  private normalizeSnapshot(snapshotArg: IEmbySnapshot): IEmbySnapshot {
    const server = {
      ...this.serverInfoFromConfig(),
      ...snapshotArg.server,
    };
    const ownDeviceId = this.config.clientDeviceId;
    const sessions = (snapshotArg.sessions || []).filter((sessionArg) => {
      if (ownDeviceId && sessionArg.DeviceId === ownDeviceId) {
        return false;
      }
      return sessionArg.Client !== clientName;
    });

    return {
      ...snapshotArg,
      server,
      sessions,
      online: snapshotArg.online,
      updatedAt: snapshotArg.updatedAt || new Date().toISOString(),
    };
  }

  private serverInfoFromConfig(): IEmbyServerInfo {
    const ssl = this.config.ssl === true || this.config.protocol === 'https';
    const port = this.config.port || (ssl ? embyDefaultHttpsPort : embyDefaultHttpPort);
    return {
      ...this.config.server,
      Id: this.config.server?.Id || this.config.server?.ServerId || this.config.uniqueId || this.config.host || this.config.url || 'emby',
      ServerName: this.config.server?.ServerName || this.config.server?.Name || this.config.name || this.config.host || embyDisplayName,
      ProductName: this.config.server?.ProductName || 'Emby Server',
      LocalAddress: this.config.server?.LocalAddress || this.config.url || (this.config.host ? `${ssl ? 'https' : 'http'}://${this.config.host}:${port}` : undefined),
      HttpServerPortNumber: this.config.server?.HttpServerPortNumber || (!ssl ? port : undefined),
      HttpsPortNumber: this.config.server?.HttpsPortNumber || (ssl ? port : undefined),
      SupportsHttps: this.config.server?.SupportsHttps || ssl,
    };
  }

  private offlineSnapshot(errorArg: string): IEmbySnapshot {
    return this.normalizeSnapshot({
      server: this.serverInfoFromConfig(),
      sessions: this.config.sessions || [],
      users: this.config.users,
      online: false,
      updatedAt: new Date().toISOString(),
      source: 'runtime',
      error: errorArg,
    });
  }

  private cloneSnapshot(snapshotArg: IEmbySnapshot): IEmbySnapshot {
    return JSON.parse(JSON.stringify(snapshotArg)) as IEmbySnapshot;
  }

  private errorMessage(errorArg: unknown): string {
    return errorArg instanceof Error ? errorArg.message : String(errorArg);
  }
}

interface IEmbyRequestOptions {
  method?: 'GET' | 'POST' | 'DELETE';
  query?: Record<string, string | number | boolean | undefined>;
  token?: string;
  body?: unknown;
}
