import * as plugins from '../../plugins.js';
import type {
  ISnapcastClient,
  ISnapcastConfig,
  ISnapcastGroup,
  ISnapcastRpcRequest,
  ISnapcastRpcResponse,
  ISnapcastServerStatus,
  ISnapcastSnapshot,
  ISnapcastVolume,
  TSnapcastRpcMethod,
} from './snapcast.types.js';
import { snapcastHttpControlPort, snapcastTcpControlPort } from './snapcast.types.js';

export class SnapcastClient {
  private nextId = 1;
  private currentSnapshot?: ISnapcastSnapshot;
  private restorePoint?: ISnapcastSnapshot;

  constructor(private readonly config: ISnapcastConfig) {
    this.currentSnapshot = config.snapshot ? this.cloneSnapshot(config.snapshot) : undefined;
  }

  public async getRpcVersion(): Promise<{ major: number; minor: number; patch: number }> {
    return this.rpc('Server.GetRPCVersion');
  }

  public async getStatus(): Promise<ISnapcastServerStatus> {
    const result = await this.rpc<{ server: ISnapcastServerStatus }>('Server.GetStatus');
    return result.server;
  }

  public async getSnapshot(): Promise<ISnapcastSnapshot> {
    return {
      server: await this.getStatus(),
      capturedAt: new Date().toISOString(),
      source: this.isSnapshotMode() ? 'manual' : 'jsonrpc',
    };
  }

  public async setClientVolume(clientIdArg: string, percentArg: number): Promise<ISnapcastVolume> {
    const volume = await this.currentClientVolume(clientIdArg);
    return this.setClientVolumeState(clientIdArg, {
      muted: volume.muted,
      percent: this.clampPercent(percentArg),
    });
  }

  public async setClientMuted(clientIdArg: string, mutedArg: boolean): Promise<ISnapcastVolume> {
    const volume = await this.currentClientVolume(clientIdArg);
    return this.setClientVolumeState(clientIdArg, {
      muted: mutedArg,
      percent: volume.percent,
    });
  }

  public async setClientLatency(clientIdArg: string, latencyArg: number): Promise<number> {
    const result = await this.rpc<{ latency: number }>('Client.SetLatency', {
      id: clientIdArg,
      latency: Math.round(latencyArg),
    });
    return result.latency;
  }

  public async setClientName(clientIdArg: string, nameArg: string): Promise<string> {
    const result = await this.rpc<{ name: string }>('Client.SetName', {
      id: clientIdArg,
      name: nameArg,
    });
    return result.name;
  }

  public async setGroupMuted(groupIdArg: string, mutedArg: boolean): Promise<boolean> {
    const result = await this.rpc<{ mute: boolean }>('Group.SetMute', {
      id: groupIdArg,
      mute: mutedArg,
    });
    return result.mute;
  }

  public async setGroupStream(groupIdArg: string, streamIdArg: string): Promise<string> {
    const result = await this.rpc<{ stream_id: string }>('Group.SetStream', {
      id: groupIdArg,
      stream_id: streamIdArg,
    });
    return result.stream_id;
  }

  public async setGroupClients(groupIdArg: string, clientIdsArg: string[]): Promise<ISnapcastServerStatus> {
    const result = await this.rpc<{ server: ISnapcastServerStatus }>('Group.SetClients', {
      id: groupIdArg,
      clients: clientIdsArg,
    });
    return result.server;
  }

  public async setGroupName(groupIdArg: string, nameArg: string): Promise<string> {
    const result = await this.rpc<{ name: string }>('Group.SetName', {
      id: groupIdArg,
      name: nameArg,
    });
    return result.name;
  }

  public async streamControl(streamIdArg: string, commandArg: string, paramsArg?: Record<string, unknown>): Promise<unknown> {
    return this.rpc('Stream.Control', {
      id: streamIdArg,
      command: commandArg,
      params: paramsArg || {},
    });
  }

  public async snapshot(): Promise<ISnapcastSnapshot> {
    this.restorePoint = await this.getSnapshot();
    return this.cloneSnapshot(this.restorePoint);
  }

  public async restore(snapshotArg = this.restorePoint): Promise<void> {
    if (!snapshotArg) {
      throw new Error('Snapcast restore requires a prior snapshot.');
    }

    if (this.isSnapshotMode()) {
      this.currentSnapshot = this.cloneSnapshot(snapshotArg);
      return;
    }

    await this.applySnapshot(snapshotArg);
  }

  public async watchEvents(): Promise<never> {
    throw new Error('Snapcast live event subscription is not implemented in this TypeScript port; use devices()/entities() polling or JSON-RPC service calls.');
  }

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

  private async setClientVolumeState(clientIdArg: string, volumeArg: ISnapcastVolume): Promise<ISnapcastVolume> {
    const result = await this.rpc<{ volume: ISnapcastVolume }>('Client.SetVolume', {
      id: clientIdArg,
      volume: {
        muted: volumeArg.muted,
        percent: this.clampPercent(volumeArg.percent),
      },
    });
    return result.volume;
  }

  private async currentClientVolume(clientIdArg: string): Promise<ISnapcastVolume> {
    const client = this.findClient((await this.getStatus()).groups, clientIdArg);
    return {
      muted: client?.config.volume?.muted ?? false,
      percent: client?.config.volume?.percent ?? 0,
    };
  }

  private async rpc<TResult>(methodArg: TSnapcastRpcMethod, paramsArg?: Record<string, unknown>): Promise<TResult> {
    if (this.isSnapshotMode()) {
      return this.snapshotRpc<TResult>(methodArg, paramsArg);
    }

    const request: ISnapcastRpcRequest = {
      id: this.nextId++,
      jsonrpc: '2.0',
      method: methodArg,
      params: paramsArg,
    };
    const transport = this.config.transport || 'tcp';
    if (transport === 'http' || transport === 'https') {
      return this.requestHttp<TResult>(request, transport);
    }
    if (transport !== 'tcp') {
      throw new Error(`Unsupported Snapcast JSON-RPC transport: ${transport}`);
    }
    return this.requestTcp<TResult>(request);
  }

  private async requestTcp<TResult>(requestArg: ISnapcastRpcRequest): Promise<TResult> {
    const host = this.config.host;
    if (!host) {
      throw new Error('Snapcast TCP JSON-RPC requires config.host.');
    }
    const port = this.config.port || snapcastTcpControlPort;
    const timeoutMs = this.config.timeoutMs || 5000;

    return new Promise<TResult>((resolve, reject) => {
      let buffer = '';
      let settled = false;
      const socket = plugins.net.createConnection({ host, port });

      const finish = (errorArg: Error | undefined, valueArg?: TResult) => {
        if (settled) {
          return;
        }
        settled = true;
        socket.removeAllListeners();
        socket.destroy();
        if (errorArg) {
          reject(errorArg);
          return;
        }
        resolve(valueArg as TResult);
      };

      socket.setEncoding('utf8');
      socket.setTimeout(timeoutMs, () => finish(new Error(`Snapcast TCP JSON-RPC timed out after ${timeoutMs}ms.`)));
      socket.on('connect', () => socket.write(`${JSON.stringify(requestArg)}\n`));
      socket.on('error', (errorArg) => finish(errorArg));
      socket.on('close', () => finish(new Error('Snapcast TCP JSON-RPC connection closed before a response was received.')));
      socket.on('data', (chunkArg) => {
        buffer += chunkArg;
        const lines = buffer.split(/\r?\n/);
        buffer = lines.pop() || '';
        for (const line of lines) {
          if (!line.trim()) {
            continue;
          }
          try {
            const parsed = JSON.parse(line) as ISnapcastRpcResponse<TResult> | Array<ISnapcastRpcResponse<TResult>>;
            const value = this.unwrapRpcResponse<TResult>(parsed, requestArg.id, requestArg.method);
            if (value !== undefined) {
              finish(undefined, value);
            }
          } catch (errorArg) {
            finish(errorArg instanceof Error ? errorArg : new Error(String(errorArg)));
          }
        }
      });
    });
  }

  private async requestHttp<TResult>(requestArg: ISnapcastRpcRequest, transportArg: 'http' | 'https'): Promise<TResult> {
    const host = this.config.host;
    if (!host) {
      throw new Error('Snapcast HTTP JSON-RPC requires config.host.');
    }
    const port = this.config.port || snapcastHttpControlPort;
    const timeoutMs = this.config.timeoutMs || 5000;
    const controller = new AbortController();
    const timeout = globalThis.setTimeout(() => controller.abort(), timeoutMs);
    try {
      const response = await globalThis.fetch(`${transportArg}://${host}:${port}/jsonrpc`, {
        method: 'POST',
        headers: {
          accept: 'application/json',
          'content-type': 'application/json',
        },
        body: JSON.stringify(requestArg),
        signal: controller.signal,
      });
      const text = await response.text();
      if (!response.ok) {
        throw new Error(`Snapcast HTTP JSON-RPC failed with HTTP ${response.status}: ${text}`);
      }
      return this.unwrapRpcResponse<TResult>(JSON.parse(text), requestArg.id, requestArg.method) as TResult;
    } finally {
      globalThis.clearTimeout(timeout);
    }
  }

  private unwrapRpcResponse<TResult>(responseArg: ISnapcastRpcResponse<TResult> | Array<ISnapcastRpcResponse<TResult>>, idArg: string | number, methodArg: string): TResult | undefined {
    const responses = Array.isArray(responseArg) ? responseArg : [responseArg];
    const response = responses.find((itemArg) => itemArg.id === idArg);
    if (!response) {
      return undefined;
    }
    if (response.error) {
      throw new Error(`Snapcast ${methodArg} failed: ${response.error.message} (${response.error.code})`);
    }
    return response.result as TResult;
  }

  private snapshotRpc<TResult>(methodArg: TSnapcastRpcMethod, paramsArg?: Record<string, unknown>): TResult {
    const snapshot = this.requireSnapshot();
    const server = snapshot.server;
    if (methodArg === 'Server.GetRPCVersion') {
      return { major: 2, minor: 0, patch: 0 } as TResult;
    }
    if (methodArg === 'Server.GetStatus') {
      return { server: this.cloneServer(server) } as TResult;
    }
    if (methodArg === 'Client.GetStatus') {
      const client = this.requireClient(server, this.stringParam(paramsArg, 'id'));
      return { client: this.cloneClient(client) } as TResult;
    }
    if (methodArg === 'Client.SetVolume') {
      const client = this.requireClient(server, this.stringParam(paramsArg, 'id'));
      const volume = this.volumeParam(paramsArg);
      client.config.volume = { muted: volume.muted, percent: this.clampPercent(volume.percent) };
      return { volume: { ...client.config.volume } } as TResult;
    }
    if (methodArg === 'Client.SetLatency') {
      const client = this.requireClient(server, this.stringParam(paramsArg, 'id'));
      client.config.latency = Math.round(this.numberParam(paramsArg, 'latency'));
      return { latency: client.config.latency } as TResult;
    }
    if (methodArg === 'Client.SetName') {
      const client = this.requireClient(server, this.stringParam(paramsArg, 'id'));
      client.config.name = this.stringParam(paramsArg, 'name');
      return { name: client.config.name } as TResult;
    }
    if (methodArg === 'Group.GetStatus') {
      const group = this.requireGroup(server, this.stringParam(paramsArg, 'id'));
      return { group: this.cloneGroup(group) } as TResult;
    }
    if (methodArg === 'Group.SetMute') {
      const group = this.requireGroup(server, this.stringParam(paramsArg, 'id'));
      group.muted = this.booleanParam(paramsArg, 'mute');
      return { mute: group.muted } as TResult;
    }
    if (methodArg === 'Group.SetStream') {
      const group = this.requireGroup(server, this.stringParam(paramsArg, 'id'));
      const streamId = this.stringParam(paramsArg, 'stream_id');
      if (server.streams.length && !server.streams.some((streamArg) => streamArg.id === streamId)) {
        throw new Error(`Snapcast stream not found: ${streamId}`);
      }
      group.stream_id = streamId;
      return { stream_id: streamId } as TResult;
    }
    if (methodArg === 'Group.SetClients') {
      this.setSnapshotGroupClients(server, this.stringParam(paramsArg, 'id'), this.stringArrayParam(paramsArg, 'clients'));
      return { server: this.cloneServer(server) } as TResult;
    }
    if (methodArg === 'Group.SetName') {
      const group = this.requireGroup(server, this.stringParam(paramsArg, 'id'));
      group.name = this.stringParam(paramsArg, 'name');
      return { name: group.name } as TResult;
    }
    throw new Error(`Snapcast snapshot transport does not support ${methodArg}.`);
  }

  private async applySnapshot(snapshotArg: ISnapcastSnapshot): Promise<void> {
    for (const group of snapshotArg.server.groups) {
      await this.setGroupClients(group.id, group.clients.map((clientArg) => clientArg.id));
      if (this.groupStreamId(group)) {
        await this.setGroupStream(group.id, this.groupStreamId(group));
      }
      await this.setGroupMuted(group.id, Boolean(group.muted));
    }

    for (const client of this.allClients(snapshotArg.server)) {
      if (client.config.volume) {
        await this.setClientVolumeState(client.id, client.config.volume);
      }
      if (typeof client.config.latency === 'number') {
        await this.setClientLatency(client.id, client.config.latency);
      }
      if (client.config.name) {
        await this.setClientName(client.id, client.config.name);
      }
    }
  }

  private setSnapshotGroupClients(serverArg: ISnapcastServerStatus, groupIdArg: string, clientIdsArg: string[]): void {
    const targetGroup = this.requireGroup(serverArg, groupIdArg);
    const clientMap = new Map(this.allClients(serverArg).map((clientArg) => [clientArg.id, clientArg]));
    const desiredClients = clientIdsArg.map((clientIdArg) => clientMap.get(clientIdArg)).filter((clientArg): clientArg is ISnapcastClient => Boolean(clientArg));
    const desiredSet = new Set(desiredClients.map((clientArg) => clientArg.id));
    const removedClients = targetGroup.clients.filter((clientArg) => !desiredSet.has(clientArg.id));

    for (const group of serverArg.groups) {
      group.clients = group.clients.filter((clientArg) => !desiredSet.has(clientArg.id));
    }
    targetGroup.clients = desiredClients;

    for (const client of removedClients) {
      if (!serverArg.groups.some((groupArg) => groupArg.clients.some((clientArg) => clientArg.id === client.id))) {
        serverArg.groups.push({
          id: `manual_${this.slug(client.id)}`,
          muted: false,
          stream_id: this.groupStreamId(targetGroup),
          clients: [client],
        });
      }
    }

    serverArg.groups = serverArg.groups.filter((groupArg) => groupArg.id === targetGroup.id || groupArg.clients.length > 0);
  }

  private isSnapshotMode(): boolean {
    return Boolean(this.currentSnapshot) || this.config.transport === 'snapshot';
  }

  private requireSnapshot(): ISnapcastSnapshot {
    if (!this.currentSnapshot) {
      throw new Error('Snapcast snapshot transport requires config.snapshot.');
    }
    return this.currentSnapshot;
  }

  private requireClient(serverArg: ISnapcastServerStatus, clientIdArg: string): ISnapcastClient {
    const client = this.findClient(serverArg.groups, clientIdArg);
    if (!client) {
      throw new Error(`Snapcast client not found: ${clientIdArg}`);
    }
    return client;
  }

  private requireGroup(serverArg: ISnapcastServerStatus, groupIdArg: string): ISnapcastGroup {
    const group = serverArg.groups.find((itemArg) => itemArg.id === groupIdArg);
    if (!group) {
      throw new Error(`Snapcast group not found: ${groupIdArg}`);
    }
    return group;
  }

  private findClient(groupsArg: ISnapcastGroup[], clientIdArg: string): ISnapcastClient | undefined {
    for (const group of groupsArg) {
      const client = group.clients.find((itemArg) => itemArg.id === clientIdArg);
      if (client) {
        return client;
      }
    }
    return undefined;
  }

  private allClients(serverArg: ISnapcastServerStatus): ISnapcastClient[] {
    return serverArg.groups.flatMap((groupArg) => groupArg.clients);
  }

  private groupStreamId(groupArg: ISnapcastGroup): string {
    return groupArg.stream_id || groupArg.streamId || '';
  }

  private stringParam(paramsArg: Record<string, unknown> | undefined, keyArg: string): string {
    const value = paramsArg?.[keyArg];
    if (typeof value !== 'string' || !value) {
      throw new Error(`Snapcast ${keyArg} parameter is required.`);
    }
    return value;
  }

  private numberParam(paramsArg: Record<string, unknown> | undefined, keyArg: string): number {
    const value = paramsArg?.[keyArg];
    if (typeof value !== 'number') {
      throw new Error(`Snapcast ${keyArg} number parameter is required.`);
    }
    return value;
  }

  private booleanParam(paramsArg: Record<string, unknown> | undefined, keyArg: string): boolean {
    const value = paramsArg?.[keyArg];
    if (typeof value !== 'boolean') {
      throw new Error(`Snapcast ${keyArg} boolean parameter is required.`);
    }
    return value;
  }

  private stringArrayParam(paramsArg: Record<string, unknown> | undefined, keyArg: string): string[] {
    const value = paramsArg?.[keyArg];
    if (!Array.isArray(value) || value.some((itemArg) => typeof itemArg !== 'string')) {
      throw new Error(`Snapcast ${keyArg} string array parameter is required.`);
    }
    return value as string[];
  }

  private volumeParam(paramsArg: Record<string, unknown> | undefined): ISnapcastVolume {
    const value = paramsArg?.volume;
    if (!value || typeof value !== 'object') {
      throw new Error('Snapcast volume parameter is required.');
    }
    const volume = value as Partial<ISnapcastVolume>;
    if (typeof volume.muted !== 'boolean' || typeof volume.percent !== 'number') {
      throw new Error('Snapcast volume requires muted and percent.');
    }
    return { muted: volume.muted, percent: volume.percent };
  }

  private clampPercent(valueArg: number): number {
    return Math.max(0, Math.min(100, Math.round(valueArg)));
  }

  private cloneSnapshot(snapshotArg: ISnapcastSnapshot): ISnapcastSnapshot {
    return {
      ...snapshotArg,
      server: this.cloneServer(snapshotArg.server),
    };
  }

  private cloneServer(serverArg: ISnapcastServerStatus): ISnapcastServerStatus {
    return JSON.parse(JSON.stringify(serverArg)) as ISnapcastServerStatus;
  }

  private cloneGroup(groupArg: ISnapcastGroup): ISnapcastGroup {
    return JSON.parse(JSON.stringify(groupArg)) as ISnapcastGroup;
  }

  private cloneClient(clientArg: ISnapcastClient): ISnapcastClient {
    return JSON.parse(JSON.stringify(clientArg)) as ISnapcastClient;
  }

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