import type { ServerWebSocket } from "bun";

export interface Track {
  id: string;        // Content hash (primary key)
  filename: string;  // Original filename
  title: string;     // Display title
  duration: number;
  replayGainDb?: number | null;
  replayPeak?: number | null;
}

export type PlaybackMode = "once" | "repeat-all" | "repeat-one" | "shuffle";

export interface ChannelConfig {
  id: string;
  name: string;
  description?: string;
  tracks: Track[];
  createdBy?: number | null;
  isDefault?: boolean;
  currentIndex?: number;
  startedAt?: number;
  paused?: boolean;
  pausedAt?: number;
  playbackMode?: PlaybackMode;
}

export type WsData = { channelId: string; userId: number | null; username: string };

export type PersistenceCallback = (channel: Channel, type: "state" | "queue") => void;

export class Channel {
  id: string;
  name: string;
  description: string;
  queue: Track[];
  currentIndex: number = 0;
  startedAt: number = Date.now();
  clients: Set<ServerWebSocket<WsData>> = new Set();
  paused: boolean = false;
  pausedAt: number = 0;
  createdBy: number | null;
  createdAt: number;
  isDefault: boolean;
  playbackMode: PlaybackMode = "repeat-all";
  private lastQueueBroadcast: number = 0;
  private queueDirty: boolean = false;
  private onPersist: PersistenceCallback | null = null;

  constructor(config: ChannelConfig) {
    this.id = config.id;
    this.name = config.name;
    this.description = config.description || "";
    this.queue = config.tracks;
    this.createdBy = config.createdBy ?? null;
    this.createdAt = Date.now();
    this.isDefault = config.isDefault ?? false;
    this.currentIndex = config.currentIndex ?? 0;
    this.startedAt = config.startedAt ?? Date.now();
    this.paused = config.paused ?? false;
    this.pausedAt = config.pausedAt ?? 0;
    this.playbackMode = config.playbackMode ?? "repeat-all";
  }

  setPersistenceCallback(callback: PersistenceCallback) {
    this.onPersist = callback;
  }

  private persistState() {
    this.onPersist?.(this, "state");
  }

  private persistQueue() {
    this.onPersist?.(this, "queue");
  }

  get currentTrack(): Track | null {
    if (this.queue.length === 0) return null;
    return this.queue[this.currentIndex];
  }

  get currentTimestamp(): number {
    if (this.queue.length === 0) return 0;
    if (this.paused) return this.pausedAt;
    return (Date.now() - this.startedAt) / 1000;
  }

  tick(): boolean {
    if (this.paused) return false;
    const track = this.currentTrack;
    if (!track) return false;
    if (this.currentTimestamp >= track.duration) {
      this.advance();
      return true;
    }
    return false;
  }

  advance() {
    if (this.queue.length === 0) return;
    
    switch (this.playbackMode) {
      case "once":
        // Play through once, stop at end
        if (this.currentIndex < this.queue.length - 1) {
          this.currentIndex++;
        } else {
          // At end of playlist - pause
          this.paused = true;
        }
        break;
      case "repeat-one":
        // Stay on same track, just reset timestamp
        break;
      case "shuffle":
        // Pick a random track (different from current if possible)
        if (this.queue.length > 1) {
          let newIndex;
          do {
            newIndex = Math.floor(Math.random() * this.queue.length);
          } while (newIndex === this.currentIndex);
          this.currentIndex = newIndex;
        }
        break;
      case "repeat-all":
      default:
        this.currentIndex = (this.currentIndex + 1) % this.queue.length;
        break;
    }
    
    this.startedAt = Date.now();
    this.persistState();
    this.broadcast();
  }
  
  setPlaybackMode(mode: PlaybackMode) {
    this.playbackMode = mode;
    this.persistState();
    this.broadcast();
  }

  getState(includeQueue: boolean = false) {
    const state: Record<string, unknown> = {
      track: this.currentTrack,
      currentTimestamp: this.currentTimestamp,
      channelName: this.name,
      channelId: this.id,
      description: this.description,
      paused: this.paused,
      currentIndex: this.currentIndex,
      listenerCount: this.clients.size,
      isDefault: this.isDefault,
      playbackMode: this.playbackMode,
    };
    if (includeQueue) {
      state.queue = this.queue;
    }
    return state;
  }

  pause() {
    if (this.paused) return;
    this.pausedAt = this.currentTimestamp;
    this.paused = true;
    this.persistState();
    this.broadcast();
  }

  unpause() {
    if (!this.paused) return;
    this.paused = false;
    this.startedAt = Date.now() - this.pausedAt * 1000;
    this.persistState();
    this.broadcast();
  }

  jumpTo(index: number) {
    if (index < 0 || index >= this.queue.length) return;
    this.currentIndex = index;
    if (this.paused) {
      this.pausedAt = 0;
    } else {
      this.startedAt = Date.now();
    }
    this.persistState();
    this.broadcast();
  }

  seek(timestamp: number) {
    const track = this.currentTrack;
    if (!track) return;
    const clamped = Math.max(0, Math.min(timestamp, track.duration));
    if (this.paused) {
      this.pausedAt = clamped;
    } else {
      this.startedAt = Date.now() - clamped * 1000;
    }
    this.persistState();
    this.broadcast();
  }

  markQueueDirty() {
    this.queueDirty = true;
  }

  setQueue(tracks: Track[]) {
    // Remember current track and timestamp to preserve playback position
    const currentTrackId = this.currentTrack?.id;
    const currentTimestampValue = this.currentTimestamp;
    const wasPaused = this.paused;
    
    this.queue = tracks;
    
    // Try to find the current track in the new queue
    if (currentTrackId) {
      const newIndex = this.queue.findIndex(t => t.id === currentTrackId);
      if (newIndex !== -1) {
        // Found the track - preserve playback position
        this.currentIndex = newIndex;
        if (wasPaused) {
          this.pausedAt = currentTimestampValue;
        } else {
          this.startedAt = Date.now() - currentTimestampValue * 1000;
        }
      } else {
        // Track not found in new queue - reset to start
        this.currentIndex = 0;
        this.startedAt = Date.now();
        this.pausedAt = 0;
      }
    } else {
      // No current track - reset to start
      this.currentIndex = 0;
      this.startedAt = Date.now();
      this.pausedAt = 0;
    }
    
    this.queueDirty = true;
    this.persistQueue();
    this.persistState();
    this.broadcast();
  }

  addTracks(tracks: Track[]) {
    if (tracks.length === 0) return;
    this.queue.push(...tracks);
    this.queueDirty = true;
    this.persistQueue();
    this.broadcast();
  }

  insertTracksAt(tracks: Track[], position: number) {
    if (tracks.length === 0) return;
    // Clamp position to valid range
    const insertPos = Math.max(0, Math.min(position, this.queue.length));
    this.queue.splice(insertPos, 0, ...tracks);
    // Adjust currentIndex if insertion is at or before current
    if (insertPos <= this.currentIndex) {
      this.currentIndex += tracks.length;
    }
    this.queueDirty = true;
    this.persistQueue();
    this.broadcast();
  }

  removeTracksByIndex(indices: number[]) {
    if (indices.length === 0) return;
    
    // Sort descending to remove from end first (preserve indices)
    const sorted = [...indices].sort((a, b) => b - a);
    const currentTrackId = this.currentTrack?.id;
    
    for (const idx of sorted) {
      if (idx >= 0 && idx < this.queue.length) {
        this.queue.splice(idx, 1);
        // Adjust currentIndex if we removed a track before it
        if (idx < this.currentIndex) {
          this.currentIndex--;
        } else if (idx === this.currentIndex) {
          // Removed currently playing track - stay at same index (next track slides in)
          // If we removed the last track, wrap to start
          if (this.currentIndex >= this.queue.length) {
            this.currentIndex = 0;
            this.startedAt = Date.now();
            this.pausedAt = 0;
          }
        }
      }
    }
    
    // If queue is now empty, reset state
    if (this.queue.length === 0) {
      this.currentIndex = 0;
      this.startedAt = Date.now();
      this.pausedAt = 0;
    }
    
    // If current track changed, reset playback position
    if (this.queue.length > 0 && this.currentTrack?.id !== currentTrackId) {
      this.startedAt = Date.now();
      this.pausedAt = 0;
    }
    
    this.queueDirty = true;
    this.persistQueue();
    this.persistState();
    this.broadcast();
  }

  moveTracks(indices: number[], targetIndex: number) {
    if (indices.length === 0) return;
    
    // Get the tracks being moved
    const sorted = [...indices].sort((a, b) => a - b);
    const tracksToMove = sorted.map(i => this.queue[i]).filter(Boolean);
    if (tracksToMove.length === 0) return;
    
    const currentTrackId = this.currentTrack?.id;
    
    // Remove tracks from their current positions (from end to preserve indices)
    for (let i = sorted.length - 1; i >= 0; i--) {
      this.queue.splice(sorted[i], 1);
    }
    
    // Adjust target index for removed items that were before it
    let adjustedTarget = targetIndex;
    for (const idx of sorted) {
      if (idx < targetIndex) adjustedTarget--;
    }
    
    // Insert at new position
    this.queue.splice(adjustedTarget, 0, ...tracksToMove);
    
    // Update currentIndex to follow the currently playing track
    if (currentTrackId) {
      const newIndex = this.queue.findIndex(t => t.id === currentTrackId);
      if (newIndex !== -1) {
        this.currentIndex = newIndex;
      }
    }
    
    this.queueDirty = true;
    this.persistQueue();
    this.persistState();
    this.broadcast();
  }

  broadcast() {
    const now = Date.now();
    const includeQueue = this.queueDirty || (now - this.lastQueueBroadcast >= 60000);
    if (includeQueue) {
      this.lastQueueBroadcast = now;
      this.queueDirty = false;
    }
    const msg = JSON.stringify(this.getState(includeQueue));

    for (const ws of this.clients) {
      ws.send(msg);
    }
  }

  addClient(ws: ServerWebSocket<WsData>) {
    this.clients.add(ws);
    console.log(`[Channel] "${this.name}" added client, now ${this.clients.size} clients`);

    // Always send full state with queue on connect
    ws.send(JSON.stringify(this.getState(true)));
    // Reset timer so next queue broadcast is in 60s
    this.lastQueueBroadcast = Date.now();
  }

  removeClient(ws: ServerWebSocket<WsData>) {
    this.clients.delete(ws);
    console.log(`[Channel] "${this.name}" removed client, now ${this.clients.size} clients`);
  }

  getListInfo() {
    const listeners = Array.from(this.clients).map(ws => ws.data?.username ?? 'Unknown');
    return {
      id: this.id,
      name: this.name,
      description: this.description,
      trackCount: this.queue.length,
      listenerCount: this.clients.size,
      listeners,
      isDefault: this.isDefault,
      createdBy: this.createdBy,
    };
  }
}
