import type {
  IJellyfinAuthenticationResult,
  IJellyfinConfig,
  IJellyfinGeneralCommandRequest,
  IJellyfinServerInfo,
  IJellyfinSession,
  IJellyfinSnapshot,
  TJellyfinGeneralCommand,
  TJellyfinPlayCommand,
  TJellyfinPlaystateCommand,
} from './jellyfin.types.js';

const defaultHttpPort = 8096;
const defaultHttpsPort = 8920;
const defaultTimeoutMs = 5000;
const clientName = 'smarthome.exchange';
const clientVersion = '0.1.0';

export class JellyfinUnsupportedLivePushError extends Error {
  constructor() {
    super('Jellyfin live WebSocket push is not implemented in this native integration; use the polling snapshot runtime.');
    this.name = 'JellyfinUnsupportedLivePushError';
  }
}

export class JellyfinClient {
  private authenticatedAccessToken: string | undefined;

  constructor(private readonly config: IJellyfinConfig) {}

  public async getSnapshot(): Promise<IJellyfinSnapshot> {
    if (this.config.snapshot) {
      return this.normalizeSnapshot(this.cloneSnapshot(this.config.snapshot));
    }

    if (!this.hasBaseUrl()) {
      return this.normalizeSnapshot({
        server: this.serverInfoFromConfig(),
        sessions: this.config.sessions || [],
        users: this.config.users,
        online: true,
        updatedAt: new Date().toISOString(),
      });
    }

    const [server, sessions] = await Promise.all([
      this.getSystemInfo(),
      this.getSessions(),
    ]);
    return this.normalizeSnapshot({
      server,
      sessions,
      users: this.config.users,
      online: true,
      updatedAt: new Date().toISOString(),
    });
  }

  public async getSystemInfo(): Promise<IJellyfinServerInfo> {
    if (this.config.server) {
      return this.config.server;
    }
    if (!this.hasBaseUrl()) {
      return this.serverInfoFromConfig();
    }
    if (this.hasAuthConfig()) {
      return this.fetchJson<IJellyfinServerInfo>('/System/Info', { token: await this.ensureAccessToken() });
    }
    return this.fetchJson<IJellyfinServerInfo>('/System/Info/Public');
  }

  public async getSessions(): Promise<IJellyfinSession[]> {
    if (!this.hasAuthConfig()) {
      return this.config.sessions || [];
    }
    const query: Record<string, string | number> = {};
    if (this.config.userId) {
      query.controllableByUserId = this.config.userId;
    }
    if (typeof this.config.activeWithinSeconds === 'number') {
      query.activeWithinSeconds = this.config.activeWithinSeconds;
    }
    return this.fetchJson<IJellyfinSession[]>('/Sessions', { query, token: await this.ensureAccessToken() });
  }

  public async authenticateByName(usernameArg: string, passwordArg: string): Promise<IJellyfinAuthenticationResult> {
    const result = await this.fetchJson<IJellyfinAuthenticationResult>('/Users/AuthenticateByName', {
      method: 'POST',
      body: {
        Username: usernameArg,
        Pw: passwordArg,
      },
    });
    if (result.AccessToken) {
      this.authenticatedAccessToken = result.AccessToken;
    }
    return result;
  }

  public async pause(sessionIdArg: string): Promise<void> {
    await this.sendPlaystateCommand(sessionIdArg, 'Pause');
  }

  public async play(sessionIdArg: string): Promise<void> {
    await this.sendPlaystateCommand(sessionIdArg, 'Unpause');
  }

  public async playPause(sessionIdArg: string): Promise<void> {
    await this.sendPlaystateCommand(sessionIdArg, 'PlayPause');
  }

  public async stop(sessionIdArg: string): Promise<void> {
    await this.sendPlaystateCommand(sessionIdArg, 'Stop');
  }

  public async nextTrack(sessionIdArg: string): Promise<void> {
    await this.sendPlaystateCommand(sessionIdArg, 'NextTrack');
  }

  public async previousTrack(sessionIdArg: string): Promise<void> {
    await this.sendPlaystateCommand(sessionIdArg, 'PreviousTrack');
  }

  public async seek(sessionIdArg: string, positionSecondsArg: number): Promise<void> {
    await this.sendPlaystateCommand(sessionIdArg, '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.sendFullGeneralCommand(sessionIdArg, 'SetVolume', { Volume: String(volume) });
  }

  public async volumeUp(sessionIdArg: string): Promise<void> {
    await this.sendGeneralCommand(sessionIdArg, 'VolumeUp');
  }

  public async volumeDown(sessionIdArg: string): Promise<void> {
    await this.sendGeneralCommand(sessionIdArg, 'VolumeDown');
  }

  public async mute(sessionIdArg: string): Promise<void> {
    await this.sendGeneralCommand(sessionIdArg, 'Mute');
  }

  public async unmute(sessionIdArg: string): Promise<void> {
    await this.sendGeneralCommand(sessionIdArg, 'Unmute');
  }

  public async playMedia(sessionIdArg: string, itemIdsArg: string[], commandArg: TJellyfinPlayCommand = 'PlayNow'): Promise<void> {
    await this.fetchNoContent(`/Sessions/${encodeURIComponent(sessionIdArg)}/Playing`, {
      method: 'POST',
      token: await this.ensureAccessToken(),
      query: {
        playCommand: commandArg,
        itemIds: itemIdsArg.join(','),
      },
    });
  }

  public async sendGeneralCommand(sessionIdArg: string, commandArg: TJellyfinGeneralCommand): Promise<void> {
    await this.fetchNoContent(`/Sessions/${encodeURIComponent(sessionIdArg)}/Command/${encodeURIComponent(commandArg)}`, {
      method: 'POST',
      token: await this.ensureAccessToken(),
    });
  }

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

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

  public async destroy(): Promise<void> {}

  private async sendPlaystateCommand(sessionIdArg: string, commandArg: TJellyfinPlaystateCommand, optionsArg?: { seekPositionTicks?: number }): Promise<void> {
    await this.fetchNoContent(`/Sessions/${encodeURIComponent(sessionIdArg)}/Playing/${encodeURIComponent(commandArg)}`, {
      method: 'POST',
      token: await this.ensureAccessToken(),
      query: optionsArg?.seekPositionTicks !== undefined ? { seekPositionTicks: optionsArg.seekPositionTicks } : undefined,
    });
  }

  private async ensureAccessToken(): Promise<string> {
    const token = this.accessToken();
    if (token) {
      return token;
    }
    if (this.config.username && this.config.password !== undefined) {
      const result = await this.authenticateByName(this.config.username, this.config.password);
      if (result.AccessToken) {
        return result.AccessToken;
      }
    }
    throw new Error('Jellyfin REST sessions and control require accessToken/apiToken or username/password authentication.');
  }

  private hasAuthConfig(): boolean {
    return Boolean(this.accessToken() || (this.config.username && this.config.password !== undefined));
  }

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

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

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

  private async fetchResponse(pathArg: string, optionsArg: IJellyfinRequestOptions): Promise<Response> {
    const abortController = new AbortController();
    const timeout = setTimeout(() => abortController.abort(), this.config.timeoutMs || defaultTimeoutMs);
    try {
      return await globalThis.fetch(this.requestUrl(pathArg, optionsArg.query), {
        method: optionsArg.method || 'GET',
        headers: this.headers(optionsArg.token, 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));
      }
    }
    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 Error('Jellyfin host or url is required for REST calls.');
    }
    const ssl = this.config.ssl === true;
    const protocol = ssl ? 'https' : 'http';
    const port = this.config.port || (ssl ? defaultHttpsPort : defaultHttpPort);
    return `${protocol}://${this.config.host}:${port}`;
  }

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

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

  private normalizeSnapshot(snapshotArg: IJellyfinSnapshot): IJellyfinSnapshot {
    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(): IJellyfinServerInfo {
    return {
      ...this.config.server,
      Id: this.config.server?.Id || this.config.server?.ServerId || this.config.uniqueId || this.config.host || this.config.url || 'jellyfin',
      Name: this.config.server?.Name || this.config.server?.ServerName || this.config.name || this.config.host || 'Jellyfin',
      ProductName: this.config.server?.ProductName || 'Jellyfin Server',
      LocalAddress: this.config.server?.LocalAddress || this.config.url,
    };
  }

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

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