/**
 * AudioPlayer - Main orchestrator for audio playback.
 *
 * Composes specialized managers following Single Responsibility Principle:
 * - AudioContextManager: Web Audio API context and routing
 * - StreamDecoder: WAV parsing and decoding
 * - PlaybackScheduler: Buffer storage and playback scheduling
 */

import { AudioContextManager } from './AudioContextManager.js';
import { StreamDecoder } from './StreamDecoder.js';
import { PlaybackScheduler } from './PlaybackScheduler.js';
import { IFormatDecoder } from './IFormatDecoder.js';
import { WavFormatDecoder } from './WavFormatDecoder.js';
import { Mp3FormatDecoder } from './Mp3FormatDecoder.js';
import { FlacFormatDecoder } from './FlacFormatDecoder.js';

export interface AudioResult {
    success: boolean;
    error?: string;
    seekBeyondBuffer?: boolean;
    byteOffset?: number;
}

export interface StreamingResult extends AudioResult {
    canStartStreaming?: boolean;
    headerParsed?: boolean;
    bufferCount?: number;
    duration?: number;
}

export interface AudioState {
    isPlaying: boolean;
    isPaused: boolean;
    currentTime: number;
    duration: number;
    volume: number;
}

type ProgressCallback = (currentTime: number) => void;
type EndCallback = () => void;

export class AudioPlayer {
    private contextManager: AudioContextManager;
    private streamDecoder: StreamDecoder;
    private scheduler: PlaybackScheduler;

    // Playback state
    private isPlaying: boolean = false;
    private isPaused: boolean = false;
    private pausePosition: number = 0;
    private duration: number = 0;

    // Streaming state
    private isStreamingMode: boolean = false;
    private streamingStarted: boolean = false;
    private streamingCompleted: boolean = false;
    private minBuffersForPlayback: number = 6;

    // Callbacks
    private onProgressCallback: ProgressCallback | null = null;
    private onEndCallback: EndCallback | null = null;
    private progressInterval: number | null = null;

    constructor() {
        this.contextManager = new AudioContextManager();
        this.streamDecoder = new StreamDecoder(this.contextManager);
        this.scheduler = new PlaybackScheduler(this.contextManager);

        // Wire up scheduler callbacks
        this.scheduler.onPlaybackEnded = () => this.handlePlaybackEnded();
    }

    // ==================== Initialization ====================

    async initialize(): Promise<AudioResult> {
        try {
            await this.contextManager.initialize();
            return { success: true };
        } catch (error) {
            return { success: false, error: (error as Error).message };
        }
    }

    async ensureAudioContextReady(): Promise<AudioResult> {
        try {
            await this.contextManager.ensureReady();
            return { success: true };
        } catch (error) {
            return { success: false, error: (error as Error).message };
        }
    }

    // ==================== Streaming ====================

    initializeStreaming(totalStreamLength: number, contentType: string): AudioResult {
        try {
            // Full cleanup before starting new stream
            this.stopProgressTracking();
            this.scheduler.clear();
            this.streamDecoder.reset();
            this.resetState();

            // Initialize new stream with the format decoder selected from Content-Type.
            this.isStreamingMode = true;
            const formatDecoder = AudioPlayer.createFormatDecoder(contentType);
            this.streamDecoder.initialize(totalStreamLength, formatDecoder);
            return { success: true };
        } catch (error) {
            return { success: false, error: (error as Error).message };
        }
    }

    /**
     * Select a format decoder from the response Content-Type.
     */
    private static createFormatDecoder(contentType: string): IFormatDecoder {
        if (contentType.includes('audio/mpeg') || contentType.includes('audio/mp3')) {
            return new Mp3FormatDecoder();
        }
        if (contentType.includes('audio/flac') || contentType.includes('audio/x-flac')) {
            return new FlacFormatDecoder();
        }
        return new WavFormatDecoder(); // default (audio/wav, unknown)
    }

    /**
     * Signal to the decoder that the C# streaming loop has finished sending bytes.
     * This sets streamComplete=true and flushes any remaining decoded tail segments.
     * Must be called after the ReadAsync loop exits, regardless of whether
     * Content-Length was known — without it the tail-decode path is dead when
     * Content-Length is absent.
     */
    async markStreamComplete(): Promise<StreamingResult> {
        try {
            const results = await this.streamDecoder.markStreamComplete();
            if (results.length > 0) {
                for (const result of results) {
                    this.scheduler.addBuffer(result.buffer);
                }
                if (this.streamingStarted && this.isPlaying) {
                    this.scheduler.scheduleNewBuffers();
                }
            }
            this.streamingCompleted = true;
            return { success: true, bufferCount: this.scheduler.getBufferCount() };
        } catch (error) {
            return { success: false, error: (error as Error).message };
        }
    }

    async processStreamingChunk(chunk: Uint8Array): Promise<StreamingResult> {
        try {
            const results = await this.streamDecoder.processChunk(chunk);

            if (results.length > 0) {
                for (const result of results) {
                    this.scheduler.addBuffer(result.buffer);
                }

                // Update duration estimate — set once only; reinitializeFromOffset does not
                // reset this.duration, so after a seek-beyond-buffer the synthesised header's
                // shorter DataSize cannot overwrite the original full-track duration.
                const estimatedDuration = this.streamDecoder.getEstimatedDuration();
                if (estimatedDuration && this.duration === 0) {
                    this.duration = estimatedDuration;
                }

                // Schedule new buffers if already playing
                if (this.streamingStarted && this.isPlaying) {
                    this.scheduler.scheduleNewBuffers();
                }
            }

            // Check if streaming is complete
            if (this.streamDecoder.isComplete) {
                this.streamingCompleted = true;
            }

            const canStart = this.streamDecoder.headerParsed &&
                this.scheduler.hasMinimumBuffers(this.minBuffersForPlayback);

            return {
                success: true,
                canStartStreaming: canStart,
                headerParsed: this.streamDecoder.headerParsed,
                bufferCount: this.scheduler.getBufferCount(),
                duration: this.duration
            };
        } catch (error) {
            return { success: false, error: (error as Error).message };
        }
    }

    async startStreamingPlayback(): Promise<AudioResult> {
        if (!this.scheduler.hasBuffers()) {
            return { success: false, error: 'No buffers available' };
        }

        try {
            // A backgrounded tab leaves AudioContext suspended. createBufferSource/start
            // against a suspended context produces no audio without throwing — the same
            // failure mode that was fixed for play() (resume path). Awaiting ensureReady()
            // here guarantees the context is running before playFromPosition schedules
            // any AudioBufferSourceNodes.
            await this.contextManager.ensureReady();

            this.streamingStarted = true;
            this.isPlaying = true;
            this.isPaused = false;
            this.pausePosition = 0;

            this.scheduler.playFromPosition(0);
            this.startProgressTracking();

            return { success: true };
        } catch (error) {
            return { success: false, error: (error as Error).message };
        }
    }

    // ==================== Playback Control ====================

    async play(): Promise<AudioResult> {
        if (!this.isStreamingMode) {
            return { success: false, error: 'Not in streaming mode' };
        }

        if (!this.streamingStarted || !this.scheduler.hasBuffers()) {
            return { success: false, error: 'Streaming not ready' };
        }

        // Don't restart if already playing
        if (this.isPlaying) {
            return { success: true };
        }

        try {
            // Must await: a backgrounded tab leaves AudioContext suspended, and
            // createBufferSource/source.start against a suspended context produces
            // no audio without throwing. Firing ensureReady() without await meant
            // play() returned success but the user heard nothing.
            await this.contextManager.ensureReady();

            this.isPlaying = true;
            this.isPaused = false;

            // Resume from pause position. pausePosition is absolute track time;
            // playFromPosition expects a buffer-relative position (excludes playbackOffset).
            const bufferRelativePosition = this.pausePosition - this.scheduler.getPlaybackOffset();
            this.scheduler.playFromPosition(Math.max(0, bufferRelativePosition));
            this.startProgressTracking();

            return { success: true };
        } catch (error) {
            return { success: false, error: (error as Error).message };
        }
    }

    pause(): AudioResult {
        if (!this.isPlaying) {
            return { success: false, error: 'Not playing' };
        }

        try {
            this.pausePosition = this.scheduler.pause();
            this.isPlaying = false;
            this.isPaused = true;
            this.stopProgressTracking();

            return { success: true };
        } catch (error) {
            return { success: false, error: (error as Error).message };
        }
    }

    stop(): AudioResult {
        try {
            this.scheduler.clear();
            this.streamDecoder.reset();
            this.resetState();
            this.stopProgressTracking();

            return { success: true };
        } catch (error) {
            return { success: false, error: (error as Error).message };
        }
    }

    unload(): AudioResult {
        return this.stop();
    }

    seek(position: number): AudioResult {
        if (!this.isStreamingMode || position < 0 || position > this.duration) {
            return { success: false, error: 'Invalid seek position' };
        }

        const bufferStart = this.scheduler.getPlaybackOffset();
        const bufferEnd = this.scheduler.getTotalDuration() + bufferStart;

        // Position must be within [bufferStart, bufferEnd] to use buffered content.
        // A lower-bound check is required: after a seek-beyond-buffer, bufferStart is
        // set to the prior seek position. Seeking to a position below bufferStart would
        // produce a negative bufferRelativePosition in seekWithinBuffer, silently
        // clamping to position 0 of the offset buffer instead of the requested time.
        if (position >= bufferStart && position <= bufferEnd) {
            return this.seekWithinBuffer(position);
        } else {
            // Seeking outside buffered window - signal C# to fetch new stream
            return this.seekBeyondBuffer(position);
        }
    }

    /**
     * Seek within currently buffered content
     */
    private seekWithinBuffer(position: number): AudioResult {
        try {
            const wasPlaying = this.isPlaying;
            this.scheduler.stopAllSources();

            // Adjust position relative to buffer start (subtract playback offset)
            const bufferRelativePosition = position - this.scheduler.getPlaybackOffset();
            this.pausePosition = position;

            if (wasPlaying) {
                this.scheduler.playFromPosition(Math.max(0, bufferRelativePosition));
            }

            return { success: true };
        } catch (error) {
            return { success: false, error: (error as Error).message };
        }
    }

    /**
     * Seek beyond buffered content - calculate byte offset for server request
     */
    private seekBeyondBuffer(position: number): AudioResult {
        try {
            // The header must be parsed for byte-offset math; without it we cannot
            // build a valid Range request.
            if (!this.streamDecoder.getFormatInfo()) {
                return { success: false, error: 'Cannot calculate byte offset' };
            }

            // calculateByteOffset returns a file-absolute offset (byte position from the
            // start of the file on disk, header included) — exactly what the Range request
            // needs. The format decoder owns the header-offset addition and frame alignment.
            const fileOffset = this.streamDecoder.calculateByteOffset(position);

            // Signal that C# needs to request a new stream from this file-absolute offset
            return {
                success: true,
                seekBeyondBuffer: true,
                byteOffset: fileOffset
            };
        } catch (error) {
            return { success: false, error: (error as Error).message };
        }
    }

    /**
     * Get the total buffered duration (for C# to check if seek is within buffer)
     */
    getBufferedDuration(): number {
        return this.scheduler.getTotalDuration() + this.scheduler.getPlaybackOffset();
    }

    /**
     * Calculate byte offset for a time position (for C# layer)
     */
    calculateByteOffset(positionSeconds: number): number {
        if (!this.streamDecoder.getFormatInfo()) return 0;
        return this.streamDecoder.calculateByteOffset(positionSeconds);
    }

    /**
     * Reinitialize for offset streaming after seek-beyond-buffer
     * Called by C# after receiving new stream from server
     */
    reinitializeFromOffset(totalStreamLength: number, seekPosition: number): AudioResult {
        try {
            // Stop current playback
            this.stopProgressTracking();
            const wasPlaying = this.isPlaying;
            this.isPlaying = false;

            // Clear buffers and set new offset
            this.scheduler.clearForSeek();
            this.scheduler.setPlaybackOffset(seekPosition);

            // Reinitialize decoder for the Range-continuation stream. totalStreamLength
            // here is the 206 Content-Length (range start → EOF), not the full file size —
            // the decoder uses it to detect stream-complete against raw audio bytes.
            this.streamDecoder.reinitializeForRangeContinuation(totalStreamLength);

            // Update state
            this.pausePosition = seekPosition;
            this.streamingStarted = false; // Will restart when new buffers arrive
            this.streamingCompleted = false;

            return { success: true };
        } catch (error) {
            return { success: false, error: (error as Error).message };
        }
    }

    // ==================== Volume ====================

    setVolume(volume: number): AudioResult {
        try {
            this.contextManager.setVolume(volume);
            return { success: true };
        } catch (error) {
            return { success: false, error: (error as Error).message };
        }
    }

    // ==================== State ====================

    getCurrentTime(): number {
        if (this.isPlaying) {
            return this.scheduler.getCurrentPosition();
        }
        return this.pausePosition;
    }

    getState(): AudioState {
        return {
            isPlaying: this.isPlaying,
            isPaused: this.isPaused,
            currentTime: this.getCurrentTime(),
            duration: this.duration,
            volume: this.contextManager.getVolume()
        };
    }

    // ==================== Callbacks ====================

    setOnProgressCallback(callback: ProgressCallback): void {
        this.onProgressCallback = callback;
    }

    setOnEndCallback(callback: EndCallback): void {
        this.onEndCallback = callback;
    }

    // ==================== Spectrum Analysis ====================

    getSpectrumData(): number[] {
        return this.contextManager.getSpectrumAnalyzer().getFrequencyData();
    }

    setSpectrumHighPass(freq: number): AudioResult {
        try {
            this.contextManager.getSpectrumAnalyzer().setHighPass(freq);
            return { success: true };
        } catch (error) {
            return { success: false, error: (error as Error).message };
        }
    }

    setSpectrumLowPass(freq: number): AudioResult {
        try {
            this.contextManager.getSpectrumAnalyzer().setLowPass(freq);
            return { success: true };
        } catch (error) {
            return { success: false, error: (error as Error).message };
        }
    }

    setSpectrumSlope(dbPerDecade: number): AudioResult {
        try {
            this.contextManager.getSpectrumAnalyzer().setSlopeCorrection(dbPerDecade);
            return { success: true };
        } catch (error) {
            return { success: false, error: (error as Error).message };
        }
    }

    startSpectrumAnimation(callbackId: string, callback: (data: number[]) => void): void {
        this.contextManager.getSpectrumAnalyzer().addCallback(callbackId, callback);
    }

    stopSpectrumAnimation(callbackId: string): void {
        this.contextManager.getSpectrumAnalyzer().removeCallback(callbackId);
    }

    getLevelDb(): number {
        return this.contextManager.getSpectrumAnalyzer().getLevelDb();
    }

    startLevelAnimation(callbackId: string, callback: (db: number) => void): void {
        this.contextManager.getSpectrumAnalyzer().addLevelCallback(callbackId, callback);
    }

    stopLevelAnimation(callbackId: string): void {
        this.contextManager.getSpectrumAnalyzer().removeLevelCallback(callbackId);
    }

    // ==================== Private Methods ====================

    private resetState(): void {
        this.isPlaying = false;
        this.isPaused = false;
        this.pausePosition = 0;
        this.duration = 0;
        this.isStreamingMode = false;
        this.streamingStarted = false;
        this.streamingCompleted = false;
    }

    private handlePlaybackEnded(): void {
        this.isPlaying = false;
        this.isPaused = false;
        this.pausePosition = 0;
        this.stopProgressTracking();
        this.onEndCallback?.();
    }

    private startProgressTracking(): void {
        this.stopProgressTracking();
        this.progressInterval = window.setInterval(() => {
            if (this.onProgressCallback && this.isPlaying) {
                this.onProgressCallback(this.getCurrentTime());
            }
        }, 100);
    }

    private stopProgressTracking(): void {
        if (this.progressInterval) {
            clearInterval(this.progressInterval);
            this.progressInterval = null;
        }
    }

    // ==================== Cleanup ====================

    dispose(): void {
        this.stop();
        this.stopProgressTracking();
        this.contextManager.dispose();
    }
}
