/**
 * Batch Uploader for Caihong Disk
 * Supports concurrent uploads, chunking, and resume.
 */
class BatchUploader {
    constructor(options) {
        this.options = Object.assign({
            concurrency: 3,
            chunkSize: 2 * 1024 * 1024, // 2MB default, will be overridden by server
            endpoints: {
                pre_upload: 'upload.php?act=pre_upload',
                upload_part: 'upload.php?act=upload_part',
                complete_upload: 'upload.php?act=complete_upload'
            },
            showInHome: true, // Default to show in home page
            onQueueUpdate: (queue) => {},
            onProgress: (file, progress, speed) => {},
            onSuccess: (file, res) => {},
            onError: (file, err) => {},
            onComplete: () => {}
        }, options);

        this.queue = [];
        this.activeUploads = 0;
        this.isUploading = false;
        this.csrf_token = document.getElementById('csrf_token') ? document.getElementById('csrf_token').value : '';
    }

    setShowInHome(flag) {
        this.options.showInHome = !!flag;
    }

    addFiles(fileList) {
        for (let i = 0; i < fileList.length; i++) {
            const file = fileList[i];
            // Unique ID based on timestamp and random
            const id = 'file_' + new Date().getTime() + '_' + Math.random().toString(36).substr(2, 9);
            this.queue.push({
                id: id,
                file: file,
                status: 'pending', // pending, hashing, uploading, completed, error
                progress: 0,
                speed: '0 B/s',
                hash: null,
                message: 'Waiting...'
            });
        }
        // Trigger UI update if needed
        if (this.options.onQueueUpdate) this.options.onQueueUpdate(this.queue);
    }

    removeFile(id) {
        const index = this.queue.findIndex(f => f.id === id);
        if (index > -1) {
            if (this.queue[index].status === 'uploading') {
                // Should abort xhr if possible, but for now just remove
            }
            this.queue.splice(index, 1);
            if (this.options.onQueueUpdate) this.options.onQueueUpdate(this.queue);
        }
    }

    start() {
        if (this.isUploading) return;
        this.isUploading = true;
        this.processQueue();
    }

    processQueue() {
        if (!this.isUploading) return;

        // Check if all done
        if (this.queue.every(f => f.status === 'completed' || f.status === 'error')) {
            this.isUploading = false;
            this.options.onComplete();
            return;
        }

        // Fill active slots
        while (this.activeUploads < this.options.concurrency) {
            const fileItem = this.queue.find(f => f.status === 'pending');
            if (!fileItem) break;
            
            this.activeUploads++;
            this.uploadFile(fileItem).finally(() => {
                this.activeUploads--;
                this.processQueue();
            });
        }
    }

    async uploadFile(fileItem) {
        try {
            // 1. Calculate Hash
            fileItem.status = 'hashing';
            fileItem.message = 'Calculating Hash...';
            this.updateUI();
            
            fileItem.hash = await this.calculateHash(fileItem.file);
            
            // 2. Pre Upload
            fileItem.status = 'uploading';
            fileItem.message = 'Preparing...';
            this.updateUI();

            const preRes = await this.preUpload(fileItem);
            
            if (preRes.code === 1) {
                // Instant upload (seconds-transfer)
                fileItem.status = 'completed';
                fileItem.progress = 100;
                fileItem.message = 'Instant Upload Success';
                this.options.onSuccess(fileItem, preRes);
                this.updateUI();
                return;
            }

            // 3. Upload Parts
            const chunkSize = preRes.chunksize || this.options.chunkSize;
            const chunks = preRes.chunks || Math.ceil(fileItem.file.size / chunkSize);
            
            let loaded = 0;
            const startTime = new Date().getTime();
            let lastTime = startTime;
            let lastLoaded = 0;
            let lastRes = null;
            let uploadErrors = [];

            // 串行上传，避免并发竞争
            for (let chunk = 1; chunk <= chunks; chunk++) {
                if (fileItem.status === 'error') {
                    console.error('Upload aborted due to error');
                    break;
                }

                const start = (chunk - 1) * chunkSize;
                const end = Math.min(start + chunkSize, fileItem.file.size);
                const blob = fileItem.file.slice(start, end);

                try {
                    lastRes = await this.uploadPart(blob, chunk, fileItem.hash, (e) => {
                        // Progress for current chunk
                        const chunkLoaded = e.loaded;
                        const totalLoaded = loaded + chunkLoaded;
                        const percent = Math.min(100, Math.round((totalLoaded / fileItem.file.size) * 100));
                        
                        fileItem.progress = percent;
                        
                        // Speed calc
                        const now = new Date().getTime();
                        if (now - lastTime >= 1000) {
                            const diff = totalLoaded - lastLoaded;
                            const spd = diff / ((now - lastTime) / 1000);
                            fileItem.speed = this.formatSize(spd) + '/s';
                            lastTime = now;
                            lastLoaded = totalLoaded;
                        }
                        
                        this.options.onProgress(fileItem, percent, fileItem.speed);
                        this.updateUI();
                    });
                    
                    loaded += blob.size;
                } catch (err) {
                    uploadErrors.push(`Chunk ${chunk}: ${err.message}`);
                    console.error(`Chunk ${chunk} upload failed:`, err);
                    continue;
                }
            }

            // 检查是否有上传错误
            if (uploadErrors.length > 0) {
                throw new Error(`Upload failed: ${uploadErrors.join('; ')}`);
            }

            // 4. Complete Upload
            fileItem.message = 'Finalizing...';
            this.updateUI();
            
            try {
                const compRes = await this.completeUpload(fileItem.hash);
                
                fileItem.status = 'completed';
                fileItem.progress = 100;
                fileItem.message = 'Success';
                this.options.onSuccess(fileItem, compRes);
            } catch (err) {
                throw new Error(`Finalization failed: ${err.message}`);
            }

        } catch (err) {
            console.error('File upload failed:', err);
            fileItem.status = 'error';
            fileItem.message = err.message || 'Upload Failed';
            
            // 重试机制
            if (err.message && err.message.includes('Session') && err.retries < 2) {
                console.log('Retrying upload due to session issue...');
                fileItem.retries = (err.retries || 0) + 1;
                await new Promise(resolve => setTimeout(resolve, 1000));
                return this.uploadFile(fileItem);
            }
            
            this.options.onError(fileItem, err);
        } finally {
            this.updateUI();
        }
    }

    calculateHash(file) {
        return new Promise((resolve, reject) => {
            const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
            const chunkSize = 2097152; // 2MB
            const chunks = Math.ceil(file.size / chunkSize);
            let currentChunk = 0;
            const spark = new SparkMD5.ArrayBuffer();
            const fileReader = new FileReader();

            fileReader.onload = function (e) {
                spark.append(e.target.result);
                currentChunk++;
                if (currentChunk < chunks) {
                    loadNext();
                } else {
                    resolve(spark.end());
                }
            };

            fileReader.onerror = function () {
                reject(new Error('Hash calculation failed'));
            };

            function loadNext() {
                const start = currentChunk * chunkSize;
                const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
                fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
            }

            loadNext();
        });
    }

    preUpload(fileItem) {
        return new Promise((resolve, reject) => {
            const formData = new FormData();
            formData.append('act', 'pre_upload');
            formData.append('name', fileItem.file.name);
            formData.append('hash', fileItem.hash);
            formData.append('size', fileItem.file.size);
            formData.append('csrf_token', this.csrf_token);
            // Use cached option instead of DOM query which might fail if modal is closed
            formData.append('show', this.options.showInHome ? 1 : 0);
            if(window.currentFolderId) formData.append('folder_id', window.currentFolderId);

            fetch(this.options.endpoints.pre_upload, {
                method: 'POST',
                body: formData,
                credentials: 'same-origin'
            })
            .then(res => res.json())
            .then(res => {
                if (res.code === 0 || res.code === 1) resolve(res);
                else reject(new Error(res.msg));
            })
            .catch(err => reject(err));
        });
    }

    uploadPart(blob, chunk, hash, onProgress) {
        return new Promise((resolve, reject) => {
            const formData = new FormData();
            formData.append('act', 'upload_part');
            formData.append('file', blob);
            formData.append('hash', hash);
            formData.append('chunk', chunk);
            formData.append('csrf_token', this.csrf_token);

            const xhr = new XMLHttpRequest();
            xhr.open('POST', this.options.endpoints.upload_part);
            xhr.withCredentials = true;
            
            xhr.upload.onprogress = onProgress;
            
            xhr.onload = () => {
                if (xhr.status === 200) {
                    try {
                        const res = JSON.parse(xhr.responseText);
                        if (res.code === 0 || res.code === 1) {
                            resolve(res);
                        }
                        else reject(new Error(res.msg));
                    } catch (e) {
                        reject(new Error('Invalid JSON response'));
                    }
                } else {
                    reject(new Error('HTTP Error ' + xhr.status));
                }
            };
            
            xhr.onerror = () => reject(new Error('Network Error'));
            
            xhr.send(formData);
        });
    }

    completeUpload(hash) {
        return new Promise((resolve, reject) => {
            const formData = new FormData();
            formData.append('act', 'complete_upload');
            formData.append('hash', hash);
            formData.append('csrf_token', this.csrf_token);

            const tryComplete = (retryCount) => {
                fetch(this.options.endpoints.complete_upload, {
                    method: 'POST',
                    body: formData,
                    credentials: 'same-origin'
                })
                .then(res => res.json())
                .then(res => {
                    if (res.code === 1) resolve(res);
                    else {
                        // Session相关错误时重试
                        if (retryCount < 2 && res.msg && res.msg.includes('参数校验失败')) {
                            console.log(`Retrying completeUpload (attempt ${retryCount + 1})...`);
                            setTimeout(() => tryComplete(retryCount + 1), 500);
                        } else {
                            reject(new Error(res.msg));
                        }
                    }
                })
                .catch(err => {
                    if (retryCount < 2) {
                        console.log(`Retrying completeUpload due to network error (attempt ${retryCount + 1})...`);
                        setTimeout(() => tryComplete(retryCount + 1), 500);
                    } else {
                        reject(err);
                    }
                });
            };
            
            tryComplete(0);
        });
    }

    updateUI() {
        if (this.options.onQueueUpdate) this.options.onQueueUpdate(this.queue);
    }

    formatSize(size) {
        if (size < 1024) return size.toFixed(2) + ' B';
        if (size < 1024 * 1024) return (size / 1024).toFixed(2) + ' KB';
        if (size < 1024 * 1024 * 1024) return (size / 1024 / 1024).toFixed(2) + ' MB';
        return (size / 1024 / 1024 / 1024).toFixed(2) + ' GB';
    }
}
