import type * as shxInterfaces from '@smarthome.exchange/interfaces';
import { BaseIntegration } from '../../core/classes.baseintegration.js';
import type { IIntegrationEntity, IIntegrationEvent, IIntegrationRuntime, IIntegrationSetupContext, IServiceCallRequest, IServiceCallResult } from '../../core/types.js';
import { PlexClient, plexMediaTypeForSession } from './plex.classes.client.js';
import { PlexConfigFlow } from './plex.classes.configflow.js';
import { createPlexDiscoveryDescriptor } from './plex.discovery.js';
import { PlexMapper } from './plex.mapper.js';
import type { IPlexConfig, IPlexPlayMediaCommand, IPlexSnapshot } from './plex.types.js';

export class PlexIntegration extends BaseIntegration<IPlexConfig> {
  public readonly domain = 'plex';
  public readonly displayName = 'Plex Media Server';
  public readonly status = 'control-runtime' as const;
  public readonly discoveryDescriptor = createPlexDiscoveryDescriptor();
  public readonly configFlow = new PlexConfigFlow();
  public readonly metadata = {
    source: 'home-assistant/core',
    upstreamPath: 'homeassistant/components/plex',
    upstreamDomain: 'plex',
    integrationType: 'service',
    iotClass: 'local_push',
    requirements: ['PlexAPI==4.15.16', 'plexauth==0.0.6', 'plexwebsocket==0.0.14'],
    dependencies: ['http'],
    afterDependencies: [],
    codeowners: ['@jjlawren'],
    configFlow: true,
    documentation: 'https://www.home-assistant.io/integrations/plex',
    zeroconf: ['_plexmediasvr._tcp.local.'],
    localApi: [
      '/',
      '/identity',
      '/clients',
      '/status/sessions',
      '/library/sections/all',
      '/library/sections/{sectionId}/refresh',
      '/player/playback/{command}',
    ],
  };

  public async setup(configArg: IPlexConfig, contextArg: IIntegrationSetupContext): Promise<IIntegrationRuntime> {
    void contextArg;
    return new PlexRuntime(new PlexClient(configArg));
  }

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

export class HomeAssistantPlexIntegration extends PlexIntegration {}

class PlexRuntime implements IIntegrationRuntime {
  public domain = 'plex';

  constructor(private readonly client: PlexClient) {}

  public async devices(): Promise<shxInterfaces.data.IDeviceDefinition[]> {
    return PlexMapper.toDevices(await this.client.getSnapshot());
  }

  public async entities(): Promise<IIntegrationEntity[]> {
    return PlexMapper.toEntities(await this.client.getSnapshot());
  }

  public async subscribe(handlerArg: (eventArg: IIntegrationEvent) => void): Promise<() => Promise<void>> {
    const unsubscribe = this.client.onEvent((eventArg) => handlerArg({
      type: eventArg.type === 'error' ? 'error' : 'state_changed',
      integrationDomain: 'plex',
      deviceId: eventArg.clientIdentifier ? `plex.client.${eventArg.clientIdentifier}` : eventArg.serverId ? `plex.server.${eventArg.serverId}` : undefined,
      entityId: eventArg.entityId,
      data: eventArg.data || eventArg.command,
      timestamp: eventArg.timestamp,
    }));
    return async () => unsubscribe();
  }

  public async callService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    try {
      if (requestArg.domain === 'media_player') {
        return await this.callMediaPlayerService(requestArg);
      }
      if (requestArg.domain === 'plex') {
        return await this.callPlexService(requestArg);
      }
      return { success: false, error: `Unsupported Plex service domain: ${requestArg.domain}` };
    } catch (errorArg) {
      return { success: false, error: errorArg instanceof Error ? errorArg.message : String(errorArg) };
    }
  }

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

  private async callMediaPlayerService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    const snapshot = await this.client.getSnapshot();
    const target = this.resolveClientTarget(snapshot, requestArg);
    const session = snapshot.sessions.find((sessionArg) => this.clientIdentifier(sessionArg.Player) === target.clientIdentifier);
    const mediaType = plexMediaTypeForSession(session);

    if (requestArg.service === 'play' || requestArg.service === 'media_play') {
      return this.client.sendPlaybackCommand({ clientIdentifier: target.clientIdentifier, command: 'play', mediaType });
    }
    if (requestArg.service === 'pause' || requestArg.service === 'media_pause') {
      return this.client.sendPlaybackCommand({ clientIdentifier: target.clientIdentifier, command: 'pause', mediaType });
    }
    if (requestArg.service === 'stop' || requestArg.service === 'media_stop') {
      return this.client.sendPlaybackCommand({ clientIdentifier: target.clientIdentifier, command: 'stop', mediaType });
    }
    if (requestArg.service === 'next_track' || requestArg.service === 'media_next_track') {
      return this.client.sendPlaybackCommand({ clientIdentifier: target.clientIdentifier, command: 'skipNext', mediaType });
    }
    if (requestArg.service === 'previous_track' || requestArg.service === 'media_previous_track') {
      return this.client.sendPlaybackCommand({ clientIdentifier: target.clientIdentifier, command: 'skipPrevious', mediaType });
    }
    if (requestArg.service === 'seek' || requestArg.service === 'media_seek') {
      const position = this.numberValue(requestArg.data?.seek_position ?? requestArg.data?.position, 'Plex media_seek requires data.seek_position or data.position.');
      return this.client.sendPlaybackCommand({ clientIdentifier: target.clientIdentifier, command: 'seekTo', mediaType, offsetMs: Math.round(position * 1000) });
    }
    if (requestArg.service === 'volume_set') {
      const level = this.numberValue(requestArg.data?.volume_level ?? requestArg.data?.volume, 'Plex volume_set requires data.volume_level or data.volume.');
      const volume = Math.max(0, Math.min(100, Math.round(level <= 1 ? level * 100 : level)));
      return this.client.sendPlaybackCommand({ clientIdentifier: target.clientIdentifier, command: 'setParameters', mediaType, volume });
    }
    if (requestArg.service === 'volume_mute') {
      const muted = this.booleanValue(requestArg.data?.is_volume_muted ?? requestArg.data?.muted, 'Plex volume_mute requires data.is_volume_muted or data.muted.');
      return this.client.sendPlaybackCommand({ clientIdentifier: target.clientIdentifier, command: 'setParameters', mediaType, volume: muted ? 0 : 100, muted });
    }
    if (requestArg.service === 'play_media') {
      return this.playMedia(snapshot, target.clientIdentifier, requestArg, mediaType);
    }

    return { success: false, error: `Unsupported Plex media_player service: ${requestArg.service}` };
  }

  private async callPlexService(requestArg: IServiceCallRequest): Promise<IServiceCallResult> {
    if (requestArg.service === 'snapshot') {
      return { success: true, data: await this.client.getSnapshot() };
    }
    if (requestArg.service === 'refresh_library') {
      return this.client.refreshLibrary({
        libraryName: this.optionalString(requestArg.data?.library_name),
        libraryKey: this.optionalString(requestArg.data?.library_id ?? requestArg.data?.section_id),
        force: this.optionalBoolean(requestArg.data?.force),
        path: this.optionalString(requestArg.data?.path),
      });
    }
    if (requestArg.service === 'scan_clients' || requestArg.service === 'refresh_clients') {
      return { success: true, data: await this.client.getSnapshot() };
    }
    if (requestArg.service === 'play' || requestArg.service === 'pause' || requestArg.service === 'stop') {
      return this.callMediaPlayerService({ ...requestArg, domain: 'media_player', service: `media_${requestArg.service}` });
    }
    return { success: false, error: `Unsupported Plex service: ${requestArg.service}` };
  }

  private async playMedia(snapshotArg: IPlexSnapshot, clientIdentifierArg: string, requestArg: IServiceCallRequest, fallbackMediaTypeArg: string): Promise<IServiceCallResult> {
    const key = this.optionalString(requestArg.data?.key ?? requestArg.data?.media_content_id);
    if (!key) {
      return { success: false, error: 'Plex play_media requires data.media_content_id or data.key.' };
    }
    const command: IPlexPlayMediaCommand = {
      clientIdentifier: clientIdentifierArg,
      command: 'playMedia',
      mediaType: this.optionalString(requestArg.data?.media_content_type ?? requestArg.data?.type) || fallbackMediaTypeArg,
      key,
      containerKey: this.optionalString(requestArg.data?.container_key ?? requestArg.data?.containerKey),
      machineIdentifier: snapshotArg.server.machineIdentifier,
      address: snapshotArg.server.host,
      port: snapshotArg.server.port,
      protocol: snapshotArg.server.ssl ? 'https' : 'http',
      offsetMs: Math.round((this.optionalNumber(requestArg.data?.offset) || 0) * 1000),
    };
    return this.client.sendPlaybackCommand(command);
  }

  private resolveClientTarget(snapshotArg: IPlexSnapshot, requestArg: IServiceCallRequest): { clientIdentifier: string } {
    const directClientId = this.optionalString(requestArg.data?.client_id ?? requestArg.data?.clientIdentifier);
    if (directClientId) {
      return { clientIdentifier: directClientId };
    }

    const targetId = requestArg.target.entityId || requestArg.target.deviceId;
    if (!targetId) {
      const activeSession = snapshotArg.sessions.find((sessionArg) => sessionArg.state === 'playing' || sessionArg.state === 'paused');
      const activeClientId = this.clientIdentifier(activeSession?.Player);
      if (activeClientId) {
        return { clientIdentifier: activeClientId };
      }
      throw new Error('Plex media service calls require target.entityId, target.deviceId, or data.client_id.');
    }

    const entity = PlexMapper.toEntities(snapshotArg).find((entityArg) => entityArg.id === targetId || entityArg.deviceId === targetId);
    const clientId = entity?.attributes?.plexClientId;
    if (typeof clientId === 'string' && clientId) {
      return { clientIdentifier: clientId };
    }
    const client = snapshotArg.clients.find((clientArg) => PlexMapper.clientDeviceId(snapshotArg, clientArg) === targetId || this.clientIdentifier(clientArg) === targetId);
    const fallbackClientId = this.clientIdentifier(client);
    if (fallbackClientId) {
      return { clientIdentifier: fallbackClientId };
    }
    throw new Error(`Plex target was not found: ${targetId}`);
  }

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

  private numberValue(valueArg: unknown, errorArg: string): number {
    const value = this.optionalNumber(valueArg);
    if (value === undefined) {
      throw new Error(errorArg);
    }
    return value;
  }

  private optionalNumber(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;
  }

  private booleanValue(valueArg: unknown, errorArg: string): boolean {
    const value = this.optionalBoolean(valueArg);
    if (value === undefined) {
      throw new Error(errorArg);
    }
    return value;
  }

  private optionalBoolean(valueArg: unknown): boolean | undefined {
    if (typeof valueArg === 'boolean') {
      return valueArg;
    }
    if (typeof valueArg === 'number') {
      return valueArg !== 0;
    }
    if (typeof valueArg === 'string') {
      if (['1', 'true', 'yes'].includes(valueArg.toLowerCase())) {
        return true;
      }
      if (['0', 'false', 'no'].includes(valueArg.toLowerCase())) {
        return false;
      }
    }
    return undefined;
  }

  private optionalString(valueArg: unknown): string | undefined {
    if (typeof valueArg === 'number') {
      return String(valueArg);
    }
    return typeof valueArg === 'string' && valueArg.trim() ? valueArg.trim() : undefined;
  }
}
