import * as plugins from '../../plugins.js';
import type { IIntegrationEntity } from '../../core/types.js';
import type { IPlexClientInfo, IPlexLibrarySection, IPlexSession, IPlexSnapshot, TPlexMediaContentType } from './plex.types.js';

const plexLibraryPrimaryTypes: Record<string, string> = {
  show: 'episode',
  artist: 'track',
};

export class PlexMapper {
  public static toDevices(snapshotArg: IPlexSnapshot): plugins.shxInterfaces.data.IDeviceDefinition[] {
    const updatedAt = snapshotArg.capturedAt || new Date().toISOString();
    const devices: plugins.shxInterfaces.data.IDeviceDefinition[] = [this.serverDevice(snapshotArg, updatedAt)];
    const sessionsByClient = this.sessionsByClient(snapshotArg.sessions);

    for (const client of snapshotArg.clients) {
      devices.push(this.clientDevice(snapshotArg, client, sessionsByClient.get(this.clientIdentifier(client) || ''), updatedAt));
    }

    return devices;
  }

  public static toEntities(snapshotArg: IPlexSnapshot): IIntegrationEntity[] {
    const entities: IIntegrationEntity[] = [];
    const sessionsByClient = this.sessionsByClient(snapshotArg.sessions);
    const serverId = this.serverDeviceId(snapshotArg);
    const serverName = this.serverName(snapshotArg);

    entities.push({
      id: `sensor.${this.slug(serverName)}_plex`,
      uniqueId: `plex_server_${this.uniqueServerBase(snapshotArg)}_activity`,
      integrationDomain: 'plex',
      deviceId: serverId,
      platform: 'sensor',
      name: `${serverName} Plex Activity`,
      state: snapshotArg.sessions.filter((sessionArg) => sessionArg.state !== 'stopped').length,
      attributes: {
        serverId: snapshotArg.server.machineIdentifier,
        version: snapshotArg.server.version,
        url: snapshotArg.server.url,
        watching: this.sensorAttributes(snapshotArg.sessions),
        sessionKeys: snapshotArg.sessions.map((sessionArg) => sessionArg.sessionKey).filter((valueArg) => valueArg !== undefined),
      },
      available: this.serverOnline(snapshotArg),
    });

    for (const client of snapshotArg.clients) {
      const session = sessionsByClient.get(this.clientIdentifier(client) || '');
      entities.push(this.clientEntity(snapshotArg, client, session));
    }

    for (const library of snapshotArg.libraries) {
      entities.push(this.libraryEntity(snapshotArg, library));
    }

    return entities;
  }

  public static clientDeviceId(snapshotArg: IPlexSnapshot, clientArg: IPlexClientInfo): string {
    return `plex.client.${this.uniqueServerBase(snapshotArg)}.${this.slug(this.clientIdentifier(clientArg) || this.clientName(clientArg))}`;
  }

  public static serverDeviceId(snapshotArg: IPlexSnapshot): string {
    return `plex.server.${this.uniqueServerBase(snapshotArg)}`;
  }

  public static slug(valueArg: string): string {
    return valueArg.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'plex';
  }

  private static serverDevice(snapshotArg: IPlexSnapshot, updatedAtArg: string): plugins.shxInterfaces.data.IDeviceDefinition {
    return {
      id: this.serverDeviceId(snapshotArg),
      integrationDomain: 'plex',
      name: this.serverName(snapshotArg),
      protocol: 'http',
      manufacturer: 'Plex',
      model: 'Plex Media Server',
      online: this.serverOnline(snapshotArg),
      features: [
        { id: 'active_sessions', capability: 'sensor', name: 'Active sessions', readable: true, writable: false },
        { id: 'library_count', capability: 'sensor', name: 'Library count', readable: true, writable: false },
        { id: 'client_count', capability: 'sensor', name: 'Client count', readable: true, writable: false },
        { id: 'version', capability: 'sensor', name: 'Version', readable: true, writable: false },
        { id: 'refresh_library', capability: 'media', name: 'Refresh library', readable: false, writable: true },
      ],
      state: [
        { featureId: 'active_sessions', value: snapshotArg.sessions.filter((sessionArg) => sessionArg.state !== 'stopped').length, updatedAt: updatedAtArg },
        { featureId: 'library_count', value: snapshotArg.libraries.length, updatedAt: updatedAtArg },
        { featureId: 'client_count', value: snapshotArg.clients.length, updatedAt: updatedAtArg },
        { featureId: 'version', value: snapshotArg.server.version || null, updatedAt: updatedAtArg },
      ],
      metadata: {
        serverId: snapshotArg.server.machineIdentifier,
        url: snapshotArg.server.url,
        platform: snapshotArg.server.platform,
        platformVersion: snapshotArg.server.platformVersion,
        myPlexUsername: snapshotArg.server.myPlexUsername,
      },
    };
  }

  private static clientDevice(snapshotArg: IPlexSnapshot, clientArg: IPlexClientInfo, sessionArg: IPlexSession | undefined, updatedAtArg: string): plugins.shxInterfaces.data.IDeviceDefinition {
    return {
      id: this.clientDeviceId(snapshotArg, clientArg),
      integrationDomain: 'plex',
      name: this.clientName(clientArg),
      protocol: 'http',
      manufacturer: clientArg.platform || clientArg.vendor || 'Plex',
      model: clientArg.product || clientArg.model || clientArg.device,
      online: this.clientOnline(clientArg, sessionArg),
      features: [
        { id: 'playback', capability: 'media', name: 'Playback', readable: true, writable: this.hasPlayback(clientArg) },
        { id: 'volume', capability: 'media', name: 'Volume', readable: true, writable: this.hasPlayback(clientArg), unit: '%' },
        { id: 'muted', capability: 'media', name: 'Muted', readable: true, writable: this.hasPlayback(clientArg) },
        { id: 'current_title', capability: 'media', name: 'Current title', readable: true, writable: false },
        { id: 'user', capability: 'sensor', name: 'User', readable: true, writable: false },
        { id: 'source', capability: 'sensor', name: 'Source', readable: true, writable: false },
      ],
      state: [
        { featureId: 'playback', value: this.mediaState(clientArg, sessionArg), updatedAt: updatedAtArg },
        { featureId: 'volume', value: this.volumePercent(clientArg), updatedAt: updatedAtArg },
        { featureId: 'muted', value: clientArg.muted ?? null, updatedAt: updatedAtArg },
        { featureId: 'current_title', value: this.mediaTitle(sessionArg) || null, updatedAt: updatedAtArg },
        { featureId: 'user', value: this.username(sessionArg) || null, updatedAt: updatedAtArg },
        { featureId: 'source', value: clientArg.source || null, updatedAt: updatedAtArg },
      ],
      metadata: {
        serverId: snapshotArg.server.machineIdentifier,
        clientIdentifier: this.clientIdentifier(clientArg),
        host: clientArg.host || clientArg.address,
        port: clientArg.port,
        protocolCapabilities: clientArg.protocolCapabilities,
        sessionKey: sessionArg?.sessionKey,
        viaDevice: this.serverDeviceId(snapshotArg),
      },
    };
  }

  private static clientEntity(snapshotArg: IPlexSnapshot, clientArg: IPlexClientInfo, sessionArg: IPlexSession | undefined): IIntegrationEntity {
    const clientName = this.clientName(clientArg);
    return {
      id: `media_player.${this.slug(clientName)}_plex`,
      uniqueId: `plex_client_${this.uniqueServerBase(snapshotArg)}_${this.slug(this.clientIdentifier(clientArg) || clientName)}`,
      integrationDomain: 'plex',
      deviceId: this.clientDeviceId(snapshotArg, clientArg),
      platform: 'media_player',
      name: `Plex (${clientName})`,
      state: this.mediaState(clientArg, sessionArg),
      attributes: {
        plexServerId: snapshotArg.server.machineIdentifier,
        plexClientId: this.clientIdentifier(clientArg),
        playerSource: clientArg.source,
        sessionKey: sessionArg?.sessionKey,
        supportedFeatures: this.hasPlayback(clientArg) ? ['play', 'pause', 'stop', 'previous_track', 'next_track', 'seek', 'volume_set', 'volume_mute', 'play_media'] : ['play_media'],
        volumeLevel: clientArg.volumeLevel,
        volumePercent: this.volumePercent(clientArg),
        isVolumeMuted: clientArg.muted,
        mediaContentId: sessionArg?.ratingKey,
        mediaContentType: this.mediaContentType(sessionArg),
        mediaDuration: this.millisecondsToSeconds(sessionArg?.duration),
        mediaPosition: this.millisecondsToSeconds(sessionArg?.viewOffset),
        mediaPositionUpdatedAt: sessionArg?.mediaPositionUpdatedAt,
        mediaTitle: this.mediaTitle(sessionArg),
        mediaSeriesTitle: sessionArg?.grandparentTitle,
        mediaSeason: sessionArg?.parentIndex,
        mediaEpisode: sessionArg?.index,
        mediaAlbumName: sessionArg?.parentTitle,
        mediaArtist: sessionArg?.originalTitle || sessionArg?.grandparentTitle,
        mediaAlbumArtist: sessionArg?.grandparentTitle,
        mediaImageUrl: this.mediaImageUrl(sessionArg),
        mediaSummary: sessionArg?.summary,
        username: this.username(sessionArg),
        mediaLibraryTitle: sessionArg?.librarySectionTitle,
        platform: clientArg.platform,
        product: clientArg.product,
      },
      available: this.clientOnline(clientArg, sessionArg),
    };
  }

  private static libraryEntity(snapshotArg: IPlexSnapshot, libraryArg: IPlexLibrarySection): IIntegrationEntity {
    const serverName = this.serverName(snapshotArg);
    const libraryTitle = libraryArg.title || String(libraryArg.key);
    return {
      id: `sensor.${this.slug(serverName)}_${this.slug(libraryTitle)}_plex_library`,
      uniqueId: `plex_library_${this.uniqueServerBase(snapshotArg)}_${this.slug(libraryArg.uuid || String(libraryArg.key))}`,
      integrationDomain: 'plex',
      deviceId: this.serverDeviceId(snapshotArg),
      platform: 'sensor',
      name: `${serverName} Library - ${libraryTitle}`,
      state: this.libraryCount(libraryArg),
      attributes: {
        plexServerId: snapshotArg.server.machineIdentifier,
        libraryKey: libraryArg.key,
        libraryUuid: libraryArg.uuid,
        libraryType: libraryArg.type,
        primaryType: this.libraryPrimaryType(libraryArg),
        counts: libraryArg.counts,
        refreshing: libraryArg.refreshing,
        locations: libraryArg.Location?.map((locationArg) => locationArg.path).filter((valueArg) => valueArg !== undefined),
        lastAddedItem: libraryArg.lastAddedItem,
        lastAddedTimestamp: libraryArg.lastAddedTimestamp,
        scannedAt: libraryArg.scannedAt,
        updatedAt: libraryArg.updatedAt,
      },
      available: this.serverOnline(snapshotArg),
    };
  }

  private static sessionsByClient(sessionsArg: IPlexSession[]): Map<string, IPlexSession> {
    const sessions = new Map<string, IPlexSession>();
    for (const session of sessionsArg) {
      const clientId = this.clientIdentifier(session.Player || session.players?.[0]);
      if (clientId) {
        sessions.set(clientId, session);
      }
    }
    return sessions;
  }

  private static sensorAttributes(sessionsArg: IPlexSession[]): Record<string, string> {
    const attributes: Record<string, string> = {};
    for (const session of sessionsArg) {
      const user = this.username(session) || session.Player?.product || 'Unknown';
      const product = session.Player?.product && session.Player.product !== user ? ` - ${session.Player.product}` : '';
      attributes[`${user}${product}`] = this.mediaSensorTitle(session);
    }
    return attributes;
  }

  private static mediaState(clientArg: IPlexClientInfo, sessionArg?: IPlexSession): string {
    const state = sessionArg?.state || clientArg.state;
    if (!this.clientOnline(clientArg, sessionArg)) {
      return 'off';
    }
    if (state === 'playing') {
      return 'playing';
    }
    if (state === 'paused') {
      return 'paused';
    }
    return 'idle';
  }

  private static mediaContentType(sessionArg: IPlexSession | undefined): string | undefined {
    const type = sessionArg?.type?.toLowerCase() as TPlexMediaContentType | undefined;
    if (type === 'episode') {
      return 'tvshow';
    }
    if (type === 'track' || type === 'album' || type === 'artist') {
      return 'music';
    }
    if (type === 'clip') {
      return 'video';
    }
    return type;
  }

  private static mediaTitle(sessionArg: IPlexSession | undefined): string | undefined {
    if (!sessionArg) {
      return undefined;
    }
    if (sessionArg.type === 'movie' && sessionArg.year && sessionArg.title) {
      return `${sessionArg.title} (${sessionArg.year})`;
    }
    return sessionArg.title;
  }

  private static mediaSensorTitle(sessionArg: IPlexSession): string {
    if (sessionArg.type === 'episode') {
      return [sessionArg.grandparentTitle, sessionArg.parentIndex && sessionArg.index ? `S${sessionArg.parentIndex}:E${sessionArg.index}` : undefined, sessionArg.title]
        .filter((valueArg) => valueArg !== undefined && valueArg !== '')
        .join(' - ') || 'Unknown';
    }
    if (sessionArg.type === 'track') {
      return [sessionArg.originalTitle || sessionArg.grandparentTitle, sessionArg.parentTitle, sessionArg.title]
        .filter((valueArg) => valueArg !== undefined && valueArg !== '')
        .join(' - ') || 'Unknown';
    }
    return this.mediaTitle(sessionArg) || 'Unknown';
  }

  private static mediaImageUrl(sessionArg: IPlexSession | undefined): string | undefined {
    if (!sessionArg) {
      return undefined;
    }
    if (sessionArg.type === 'episode') {
      return sessionArg.grandparentThumb || sessionArg.thumb || sessionArg.art;
    }
    return sessionArg.thumb || sessionArg.art || sessionArg.parentThumb || sessionArg.grandparentThumb;
  }

  private static libraryCount(libraryArg: IPlexLibrarySection): number | null {
    const primaryType = this.libraryPrimaryType(libraryArg);
    return libraryArg.itemCount
      ?? libraryArg.totalSize
      ?? libraryArg.leafCount
      ?? libraryArg.counts?.[primaryType]
      ?? null;
  }

  private static libraryPrimaryType(libraryArg: IPlexLibrarySection): string {
    const type = libraryArg.type || 'item';
    return plexLibraryPrimaryTypes[type] || type;
  }

  private static username(sessionArg: IPlexSession | undefined): string | undefined {
    return sessionArg?.username || sessionArg?.User?.title || sessionArg?.User?.username;
  }

  private static volumePercent(clientArg: IPlexClientInfo): number | null {
    if (typeof clientArg.volumeLevel !== 'number') {
      return null;
    }
    return Math.round((clientArg.volumeLevel <= 1 ? clientArg.volumeLevel * 100 : clientArg.volumeLevel));
  }

  private static millisecondsToSeconds(valueArg: unknown): number | undefined {
    return typeof valueArg === 'number' ? Math.round(valueArg / 1000) : undefined;
  }

  private static hasPlayback(clientArg: IPlexClientInfo): boolean {
    return Boolean(clientArg.protocolCapabilities?.includes('playback'));
  }

  private static clientOnline(clientArg: IPlexClientInfo, sessionArg?: IPlexSession): boolean {
    return Boolean(sessionArg && sessionArg.state !== 'stopped') || Boolean(clientArg.host || clientArg.address || clientArg.source === 'PMS' || clientArg.source === 'GDM');
  }

  private static clientIdentifier(clientArg: IPlexClientInfo | undefined): string | undefined {
    return clientArg?.machineIdentifier || clientArg?.id;
  }

  private static clientName(clientArg: IPlexClientInfo): string {
    return clientArg.title || clientArg.name || clientArg.product || clientArg.machineIdentifier || clientArg.id || 'Plex Client';
  }

  private static serverName(snapshotArg: IPlexSnapshot): string {
    return snapshotArg.server.friendlyName || snapshotArg.server.name || 'Plex Media Server';
  }

  private static serverOnline(snapshotArg: IPlexSnapshot): boolean {
    return snapshotArg.online ?? snapshotArg.server.online ?? false;
  }

  private static uniqueServerBase(snapshotArg: IPlexSnapshot): string {
    return this.slug(snapshotArg.server.machineIdentifier || snapshotArg.server.host || this.serverName(snapshotArg));
  }
}
