/**
 * ChunkedUploader - Chunked File Upload Module for Large Files
 *
 * Features:
 * - Break large files into manageable chunks
 * - SHA-256 hashing for integrity verification
 * - Resume capability for interrupted uploads
 * - Real-time progress via SSE
 * - Retry logic with exponential backoff
 * - Parallel chunk uploads (configurable)
 * - Integration with LiveComponent system
 *
 * @package Framework\LiveComponents
 */

import { getGlobalSseClient } from '../sse/index.js';

/**
 * Chunk Upload Status
 */
const ChunkStatus = {
    PENDING: 'pending',
    HASHING: 'hashing',
    UPLOADING: 'uploading',
    COMPLETE: 'complete',
    ERROR: 'error'
};

/**
 * Upload Session Status
 */
const SessionStatus = {
    INITIALIZING: 'initializing',
    INITIALIZED: 'initialized',
    UPLOADING: 'uploading',
    ASSEMBLING: 'assembling',
    COMPLETE: 'complete',
    ABORTED: 'aborted',
    ERROR: 'error'
};

/**
 * Chunk Metadata - Tracks individual chunk state
 */
class ChunkMetadata {
    constructor(index, size, file) {
        this.index = index;
        this.size = size;
        this.status = ChunkStatus.PENDING;
        this.hash = null;
        this.uploadedBytes = 0;
        this.retries = 0;
        this.error = null;
        this.xhr = null;
        this.file = file;
    }

    get progress() {
        return this.size > 0 ? (this.uploadedBytes / this.size) * 100 : 0;
    }

    reset() {
        this.status = ChunkStatus.PENDING;
        this.uploadedBytes = 0;
        this.error = null;
        this.xhr = null;
    }

    abort() {
        if (this.xhr) {
            this.xhr.abort();
            this.xhr = null;
        }
    }
}

/**
 * Upload Session - Manages complete chunked upload session
 */
class UploadSession {
    constructor(file, options) {
        this.file = file;
        this.sessionId = null;
        this.totalChunks = 0;
        this.chunkSize = options.chunkSize;
        this.chunks = [];
        this.status = SessionStatus.INITIALIZING;
        this.uploadedChunks = 0;
        this.expectedFileHash = null;
        this.error = null;
        this.startTime = null;
        this.endTime = null;
        this.expiresAt = null;
    }

    get progress() {
        if (this.totalChunks === 0) return 0;
        return (this.uploadedChunks / this.totalChunks) * 100;
    }

    get uploadedBytes() {
        return this.chunks.reduce((sum, chunk) => sum + chunk.uploadedBytes, 0);
    }

    get totalBytes() {
        return this.file.size;
    }

    get isComplete() {
        return this.status === SessionStatus.COMPLETE;
    }

    get isError() {
        return this.status === SessionStatus.ERROR;
    }

    get isAborted() {
        return this.status === SessionStatus.ABORTED;
    }

    get canResume() {
        return this.sessionId !== null && !this.isComplete && !this.isAborted;
    }

    getChunk(index) {
        return this.chunks[index];
    }

    getPendingChunks() {
        return this.chunks.filter(c => c.status === ChunkStatus.PENDING || c.status === ChunkStatus.ERROR);
    }

    getUploadingChunks() {
        return this.chunks.filter(c => c.status === ChunkStatus.UPLOADING);
    }
}

/**
 * ChunkedUploader - Main chunked upload manager
 */
export class ChunkedUploader {
    constructor(componentId, options = {}) {
        this.componentId = componentId;

        // Options
        this.chunkSize = options.chunkSize || 512 * 1024; // 512KB default
        this.maxConcurrentChunks = options.maxConcurrentChunks || 3;
        this.maxRetries = options.maxRetries || 3;
        this.retryDelay = options.retryDelay || 1000; // 1s base delay
        this.enableSSE = options.enableSSE !== false; // SSE enabled by default
        this.apiBase = options.apiBase || '/live-component/upload';

        // Callbacks
        this.onInitialized = options.onInitialized || (() => {});
        this.onChunkProgress = options.onChunkProgress || (() => {});
        this.onProgress = options.onProgress || (() => {});
        this.onComplete = options.onComplete || (() => {});
        this.onError = options.onError || (() => {});
        this.onAborted = options.onAborted || (() => {});
        this.onSSEProgress = options.onSSEProgress || (() => {});

        // State
        this.sessions = new Map(); // sessionId => UploadSession
        this.activeSession = null;
        this.sseClient = null;
        this.userId = null;

        // Initialize SSE if enabled
        if (this.enableSSE) {
            this.initializeSSE();
        }
    }

    /**
     * Initialize SSE connection for real-time progress
     */
    initializeSSE() {
        try {
            // Get user ID from meta tag or data attribute
            this.userId = this.getUserId();

            if (this.userId) {
                this.sseClient = getGlobalSseClient([`user:${this.userId}`]);

                // Listen for progress events
                this.sseClient.on('progress', (data) => {
                    this.handleSSEProgress(data);
                });

                // Connect if not already connected
                if (!this.sseClient.isConnected()) {
                    this.sseClient.connect();
                }
            }
        } catch (error) {
            console.warn('[ChunkedUploader] Failed to initialize SSE:', error);
        }
    }

    /**
     * Get user ID for SSE channel
     */
    getUserId() {
        // Try meta tag first
        const meta = document.querySelector('meta[name="user-id"]');
        if (meta) return meta.content;

        // Try data attribute on body
        if (document.body.dataset.userId) {
            return document.body.dataset.userId;
        }

        return null;
    }

    /**
     * Handle SSE progress updates
     */
    handleSSEProgress(data) {
        const sessionId = data.session_id;
        const session = this.sessions.get(sessionId);

        if (session && data.taskId === sessionId) {
            // Update session from SSE data
            if (data.data?.uploaded_chunks !== undefined) {
                session.uploadedChunks = data.data.uploaded_chunks;
            }

            if (data.data?.phase) {
                switch (data.data.phase) {
                    case 'initialized':
                        session.status = SessionStatus.INITIALIZED;
                        break;
                    case 'uploading':
                        session.status = SessionStatus.UPLOADING;
                        break;
                    case 'completed':
                        session.status = SessionStatus.COMPLETE;
                        break;
                    case 'aborted':
                        session.status = SessionStatus.ABORTED;
                        break;
                    case 'error':
                        session.status = SessionStatus.ERROR;
                        break;
                }
            }

            // Callback
            this.onSSEProgress({
                sessionId,
                percent: data.percent,
                message: data.message,
                data: data.data
            });
        }
    }

    /**
     * Upload file with chunking
     *
     * @param {File} file - File to upload
     * @param {string} targetPath - Target path for assembled file
     * @returns {Promise<UploadSession>}
     */
    async upload(file, targetPath) {
        // Create upload session
        const session = new UploadSession(file, {
            chunkSize: this.chunkSize
        });

        this.activeSession = session;

        try {
            // Calculate chunks
            session.totalChunks = Math.ceil(file.size / this.chunkSize);
            for (let i = 0; i < session.totalChunks; i++) {
                const start = i * this.chunkSize;
                const end = Math.min(start + this.chunkSize, file.size);
                const size = end - start;
                session.chunks.push(new ChunkMetadata(i, size, file));
            }

            // Initialize session with API
            await this.initializeSession(session);

            // Store session
            this.sessions.set(session.sessionId, session);

            // Start uploading chunks
            session.status = SessionStatus.UPLOADING;
            session.startTime = Date.now();
            await this.uploadChunks(session);

            // Complete upload
            await this.completeUpload(session, targetPath);

            // Success
            session.status = SessionStatus.COMPLETE;
            session.endTime = Date.now();

            this.onComplete({
                sessionId: session.sessionId,
                file: session.file,
                totalBytes: session.totalBytes,
                duration: session.endTime - session.startTime
            });

            return session;

        } catch (error) {
            session.status = SessionStatus.ERROR;
            session.error = error.message;

            this.onError({
                sessionId: session.sessionId,
                file: session.file,
                error: error.message
            });

            throw error;
        }
    }

    /**
     * Initialize upload session with API
     */
    async initializeSession(session) {
        const response = await fetch(`${this.apiBase}/init`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
                'User-Agent': navigator.userAgent
            },
            body: JSON.stringify({
                componentId: this.componentId,
                fileName: session.file.name,
                totalSize: session.file.size,
                chunkSize: this.chunkSize
            })
        });

        if (!response.ok) {
            throw new Error(`Failed to initialize upload session: ${response.status}`);
        }

        const data = await response.json();

        if (!data.success) {
            throw new Error(data.error || 'Failed to initialize upload session');
        }

        // Update session
        session.sessionId = data.session_id;
        session.totalChunks = data.total_chunks;
        session.expiresAt = new Date(data.expires_at);
        session.status = SessionStatus.INITIALIZED;

        this.onInitialized({
            sessionId: session.sessionId,
            totalChunks: session.totalChunks,
            expiresAt: session.expiresAt
        });
    }

    /**
     * Upload all chunks with parallelization
     */
    async uploadChunks(session) {
        const pending = session.getPendingChunks();

        // Upload chunks in parallel batches
        while (pending.length > 0) {
            const batch = pending.splice(0, this.maxConcurrentChunks);

            await Promise.all(
                batch.map(chunk => this.uploadChunk(session, chunk))
            );
        }
    }

    /**
     * Upload a single chunk
     */
    async uploadChunk(session, chunk) {
        let retries = 0;

        while (retries <= this.maxRetries) {
            try {
                // Hash chunk data
                chunk.status = ChunkStatus.HASHING;
                const chunkData = await this.readChunk(session.file, chunk);
                const chunkHash = await this.hashChunk(chunkData);

                chunk.hash = chunkHash;

                // Upload chunk
                chunk.status = ChunkStatus.UPLOADING;
                await this.uploadChunkData(session, chunk, chunkData, chunkHash);

                // Success
                chunk.status = ChunkStatus.COMPLETE;
                session.uploadedChunks++;

                this.onProgress({
                    sessionId: session.sessionId,
                    progress: session.progress,
                    uploadedChunks: session.uploadedChunks,
                    totalChunks: session.totalChunks,
                    uploadedBytes: session.uploadedBytes,
                    totalBytes: session.totalBytes
                });

                return;

            } catch (error) {
                retries++;

                if (retries > this.maxRetries) {
                    chunk.status = ChunkStatus.ERROR;
                    chunk.error = error.message;
                    throw new Error(`Chunk ${chunk.index} failed after ${this.maxRetries} retries: ${error.message}`);
                }

                // Exponential backoff
                const delay = this.retryDelay * Math.pow(2, retries - 1);
                await this.sleep(delay);

                chunk.reset();
            }
        }
    }

    /**
     * Read chunk data from file
     */
    readChunk(file, chunk) {
        return new Promise((resolve, reject) => {
            const start = chunk.index * this.chunkSize;
            const end = Math.min(start + chunk.size, file.size);
            const blob = file.slice(start, end);

            const reader = new FileReader();

            reader.onload = (e) => resolve(e.target.result);
            reader.onerror = (e) => reject(new Error('Failed to read chunk'));

            reader.readAsArrayBuffer(blob);
        });
    }

    /**
     * Hash chunk data with SHA-256
     */
    async hashChunk(chunkData) {
        const hashBuffer = await crypto.subtle.digest('SHA-256', chunkData);
        const hashArray = Array.from(new Uint8Array(hashBuffer));
        return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    }

    /**
     * Upload chunk data to server
     */
    uploadChunkData(session, chunk, chunkData, chunkHash) {
        return new Promise((resolve, reject) => {
            const formData = new FormData();
            formData.append('sessionId', session.sessionId);
            formData.append('chunkIndex', chunk.index);
            formData.append('chunkHash', chunkHash);
            formData.append('chunk', new Blob([chunkData]));

            const xhr = new XMLHttpRequest();
            chunk.xhr = xhr;

            // Progress tracking
            xhr.upload.addEventListener('progress', (e) => {
                if (e.lengthComputable) {
                    chunk.uploadedBytes = e.loaded;

                    this.onChunkProgress({
                        sessionId: session.sessionId,
                        chunkIndex: chunk.index,
                        uploadedBytes: e.loaded,
                        totalBytes: e.total,
                        progress: (e.loaded / e.total) * 100
                    });
                }
            });

            // Load event
            xhr.addEventListener('load', () => {
                if (xhr.status >= 200 && xhr.status < 300) {
                    try {
                        const response = JSON.parse(xhr.responseText);
                        if (response.success) {
                            resolve(response);
                        } else {
                            reject(new Error(response.error || 'Chunk upload failed'));
                        }
                    } catch (e) {
                        reject(e);
                    }
                } else {
                    reject(new Error(`Chunk upload failed with status ${xhr.status}`));
                }
            });

            // Error events
            xhr.addEventListener('error', () => {
                reject(new Error('Network error during chunk upload'));
            });

            xhr.addEventListener('abort', () => {
                reject(new Error('Chunk upload cancelled'));
            });

            // Send request
            xhr.open('POST', `${this.apiBase}/chunk`);
            xhr.setRequestHeader('Accept', 'application/json');
            xhr.setRequestHeader('User-Agent', navigator.userAgent);
            xhr.send(formData);
        });
    }

    /**
     * Complete upload and assemble file
     */
    async completeUpload(session, targetPath) {
        session.status = SessionStatus.ASSEMBLING;

        const response = await fetch(`${this.apiBase}/complete`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json',
                'User-Agent': navigator.userAgent
            },
            body: JSON.stringify({
                sessionId: session.sessionId,
                targetPath: targetPath
            })
        });

        if (!response.ok) {
            throw new Error(`Failed to complete upload: ${response.status}`);
        }

        const data = await response.json();

        if (!data.success) {
            throw new Error(data.error || 'Failed to complete upload');
        }

        return data;
    }

    /**
     * Abort upload
     */
    async abort(sessionId) {
        const session = this.sessions.get(sessionId);
        if (!session) {
            throw new Error('Session not found');
        }

        // Abort all active chunk uploads
        session.chunks.forEach(chunk => chunk.abort());

        // Notify server
        try {
            await fetch(`${this.apiBase}/abort`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Accept': 'application/json',
                    'User-Agent': navigator.userAgent
                },
                body: JSON.stringify({
                    sessionId: sessionId,
                    reason: 'User cancelled'
                })
            });
        } catch (error) {
            console.warn('[ChunkedUploader] Failed to notify server of abort:', error);
        }

        // Update session
        session.status = SessionStatus.ABORTED;

        this.onAborted({
            sessionId,
            uploadedChunks: session.uploadedChunks,
            totalChunks: session.totalChunks
        });
    }

    /**
     * Get upload status
     */
    async getStatus(sessionId) {
        const response = await fetch(`${this.apiBase}/status/${sessionId}`, {
            headers: {
                'Accept': 'application/json',
                'User-Agent': navigator.userAgent
            }
        });

        if (!response.ok) {
            throw new Error(`Failed to get upload status: ${response.status}`);
        }

        const data = await response.json();

        if (!data.success) {
            throw new Error(data.error || 'Failed to get upload status');
        }

        return data;
    }

    /**
     * Resume interrupted upload
     */
    async resume(sessionId, file, targetPath) {
        // Get current status from server
        const status = await this.getStatus(sessionId);

        // Recreate session
        const session = new UploadSession(file, {
            chunkSize: this.chunkSize
        });

        session.sessionId = sessionId;
        session.totalChunks = status.total_chunks;
        session.uploadedChunks = status.uploaded_chunks;
        session.status = SessionStatus.UPLOADING;

        // Recreate chunks
        for (let i = 0; i < session.totalChunks; i++) {
            const start = i * this.chunkSize;
            const end = Math.min(start + this.chunkSize, file.size);
            const size = end - start;
            const chunk = new ChunkMetadata(i, size, file);

            // Mark already uploaded chunks as complete
            if (i < status.uploaded_chunks) {
                chunk.status = ChunkStatus.COMPLETE;
                chunk.uploadedBytes = chunk.size;
            }

            session.chunks.push(chunk);
        }

        // Store and set as active
        this.sessions.set(sessionId, session);
        this.activeSession = session;

        // Resume uploading
        session.startTime = Date.now();
        await this.uploadChunks(session);
        await this.completeUpload(session, targetPath);

        session.status = SessionStatus.COMPLETE;
        session.endTime = Date.now();

        this.onComplete({
            sessionId: session.sessionId,
            file: session.file,
            totalBytes: session.totalBytes,
            duration: session.endTime - session.startTime,
            resumed: true
        });

        return session;
    }

    /**
     * Helper: Sleep for delay
     */
    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    /**
     * Cleanup
     */
    destroy() {
        // Abort all active sessions
        this.sessions.forEach(session => {
            if (session.canResume) {
                session.chunks.forEach(chunk => chunk.abort());
            }
        });

        this.sessions.clear();
        this.activeSession = null;
    }
}

export default ChunkedUploader;
