import os
import hashlib
import threading
import time
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed
from flask import current_app
from werkzeug.utils import secure_filename
from app import db
from app.utils.security import sanitize_filename, validate_file_type, validate_file_size, detect_malicious_content
from app.utils.logging import log_file_operation, log_upload_progress
import logging

logger = logging.getLogger(__name__)

class FileUploadHandler:
    """Handle file uploads with progress tracking and security validation"""
    
    def __init__(self):
        self.upload_folder = None
        self.chunk_size = 8192
        self.max_concurrent_uploads = 5
        self.executor = None
        self.active_uploads = {}
        self.upload_lock = threading.Lock()
        self._initialized = False
    
    def _ensure_initialized(self):
        """Ensure handler is initialized with Flask app config"""
        if not self._initialized:
            from flask import current_app
            # Use the persistent ZIP uploads directory for all uploads
            self.upload_folder = current_app.config.get('ZIP_UPLOADS_DIR')
            if self.upload_folder is None:
                # Fallback to legacy UPLOAD_FOLDER if ZIP_UPLOADS_DIR not set
                self.upload_folder = current_app.config.get('UPLOAD_FOLDER')
            self.chunk_size = current_app.config.get('CHUNK_SIZE', 8192)
            self.max_concurrent_uploads = current_app.config.get('MAX_CONCURRENT_UPLOADS', 5)
            if self.executor is None:
                self.executor = ThreadPoolExecutor(max_workers=self.max_concurrent_uploads)
            self._initialized = True
    
    def validate_upload(self, file, file_type):
        """
        Validate file upload
        
        Args:
            file: Werkzeug FileStorage object
            file_type: Expected file type ('fixture' or 'zip')
        
        Returns:
            tuple: (is_valid, error_message)
        """
        try:
            # Check if file is present
            if not file or not file.filename:
                return False, "No file provided"
            
            # Validate file type
            if file_type == 'fixture':
                allowed_extensions = current_app.config['ALLOWED_FIXTURE_EXTENSIONS']
            elif file_type == 'zip':
                allowed_extensions = current_app.config['ALLOWED_ZIP_EXTENSIONS']
            else:
                return False, "Invalid file type specified"
            
            if not validate_file_type(file.filename, allowed_extensions):
                return False, f"File type not allowed. Allowed types: {', '.join(allowed_extensions)}"
            
            # Validate file size
            file.seek(0, os.SEEK_END)
            file_size = file.tell()
            file.seek(0)
            
            if not validate_file_size(file_size):
                max_size_mb = current_app.config.get('MAX_CONTENT_LENGTH', 500 * 1024 * 1024) // (1024 * 1024)
                return False, f"File too large. Maximum size: {max_size_mb}MB"
            
            return True, None
            
        except Exception as e:
            logger.error(f"File validation error: {str(e)}")
            return False, "File validation failed"
    
    def calculate_sha1(self, file_path):
        """
        Calculate SHA1 checksum of file
        
        Args:
            file_path: Path to file
        
        Returns:
            str: SHA1 checksum in hexadecimal
        """
        sha1_hash = hashlib.sha1()
        try:
            with open(file_path, 'rb') as f:
                for chunk in iter(lambda: f.read(self.chunk_size), b""):
                    sha1_hash.update(chunk)
            return sha1_hash.hexdigest()
        except Exception as e:
            logger.error(f"SHA1 calculation error: {str(e)}")
            return None
    
    def save_file_chunked(self, file, file_path, upload_record):
        """
        Save file in chunks with progress tracking and memory optimization
        
        Args:
            file: Werkzeug FileStorage object
            file_path: Destination file path
            upload_record: FileUpload database record
        
        Returns:
            bool: True if successful
        """
        try:
            # Get file size without loading entire file into memory
            file.seek(0, os.SEEK_END)
            total_size = file.tell()
            file.seek(0)
            
            bytes_written = 0
            last_progress_update = 0
            progress_update_threshold = max(1024 * 1024, total_size // 100)  # Update every 1MB or 1%
            
            # Use larger chunk size for big files to improve performance
            dynamic_chunk_size = self.chunk_size
            if total_size > 100 * 1024 * 1024:  # Files > 100MB
                dynamic_chunk_size = min(1024 * 1024, self.chunk_size * 128)  # Up to 1MB chunks
            elif total_size > 10 * 1024 * 1024:  # Files > 10MB
                dynamic_chunk_size = min(256 * 1024, self.chunk_size * 32)   # Up to 256KB chunks
            
            logger.info(f"Starting chunked upload: {total_size} bytes, chunk size: {dynamic_chunk_size}")
            
            with open(file_path, 'wb') as f:
                while True:
                    try:
                        chunk = file.read(dynamic_chunk_size)
                        if not chunk:
                            break
                        
                        f.write(chunk)
                        bytes_written += len(chunk)
                        
                        # Update progress less frequently for large files to reduce database load
                        if bytes_written - last_progress_update >= progress_update_threshold:
                            progress = (bytes_written / total_size) * 100 if total_size > 0 else 100
                            upload_record.update_progress(progress)
                            last_progress_update = bytes_written
                            
                            # Log progress for large files (every 5%)
                            if total_size > 50 * 1024 * 1024 and int(progress) % 5 == 0:
                                log_upload_progress(
                                    upload_record.id,
                                    round(progress, 2),
                                    'uploading',
                                    user_id=upload_record.uploaded_by,
                                    match_id=upload_record.match_id
                                )
                                logger.info(f"Upload progress: {progress:.1f}% ({bytes_written}/{total_size} bytes)")
                        
                        # Force flush for large files to prevent memory buildup
                        if bytes_written % (10 * 1024 * 1024) == 0:  # Every 10MB
                            f.flush()
                            os.fsync(f.fileno())
                            
                    except MemoryError:
                        logger.error("Memory error during file upload - file too large")
                        upload_record.mark_failed("File too large - insufficient memory")
                        return False
                    except IOError as e:
                        logger.error(f"IO error during file upload: {str(e)}")
                        upload_record.mark_failed(f"File write error: {str(e)}")
                        return False
            
            # Final progress update
            upload_record.update_progress(100.0)
            logger.info(f"Upload completed successfully: {bytes_written} bytes written")
            return True
            
        except Exception as e:
            logger.error(f"File save error: {str(e)}")
            upload_record.mark_failed(f"File save failed: {str(e)}")
            return False
    
    def process_upload(self, file, file_type, user_id, match_id=None):
        """
        Process file upload with validation and progress tracking
        
        Args:
            file: Werkzeug FileStorage object
            file_type: Type of file ('fixture' or 'zip')
            user_id: ID of user uploading file
            match_id: Associated match ID (for ZIP files)
        
        Returns:
            tuple: (upload_record, error_message)
        """
        try:
            # Ensure handler is initialized
            self._ensure_initialized()
            
            if self.upload_folder is None:
                logger.error("Upload folder is None after initialization")
                return None, "Upload folder not configured"
            # Validate upload
            is_valid, error_message = self.validate_upload(file, file_type)
            if not is_valid:
                return None, error_message
            
            # Generate secure filename
            original_filename = file.filename
            sanitized_name = sanitize_filename(original_filename)
            
            if file_type == 'zip':
                # For ZIP files, use SHA1 of unix timestamp + original filename
                import time
                unix_timestamp = str(int(time.time()))
                hash_input = unix_timestamp + original_filename
                sha1_hash = hashlib.sha1(hash_input.encode('utf-8')).hexdigest()
                
                # Extract file extension from original filename
                _, ext = os.path.splitext(sanitized_name)
                filename = sha1_hash + (ext or '.zip')
                
                # Use ZIP uploads directory
                from flask import current_app
                upload_dir = current_app.config.get('ZIP_UPLOADS_DIR', self.upload_folder)
            else:
                # For other files, use timestamp-based naming
                timestamp = datetime.utcnow().strftime('%Y%m%d_%H%M%S')
                filename = "{}_{}".format(timestamp, sanitized_name)
                
                # Use general uploads directory or fixtures directory
                from flask import current_app
                if file_type == 'fixture':
                    upload_dir = current_app.config.get('FIXTURES_DIR', self.upload_folder)
                else:
                    upload_dir = self.upload_folder
            
            # Create upload directory if it doesn't exist
            if upload_dir is None:
                logger.error("Upload directory is None - cannot create directory")
                return None, "Upload directory not configured"
            
            try:
                os.makedirs(upload_dir, exist_ok=True)
                file_path = os.path.join(upload_dir, filename)
            except OSError as e:
                logger.error(f"Failed to create upload directory {upload_dir}: {str(e)}")
                return None, f"Failed to create upload directory: {str(e)}"
            
            # Get file size and MIME type
            file.seek(0, os.SEEK_END)
            file_size = file.tell()
            file.seek(0)
            mime_type = file.content_type or 'application/octet-stream'
            
            # Create upload record
            from app.models import FileUpload
            upload_record = FileUpload(
                filename=filename,
                original_filename=original_filename,
                file_path=file_path,
                file_size=file_size,
                file_type=file_type,
                mime_type=mime_type,
                sha1sum='',  # Will be calculated after upload
                upload_status='uploading',
                match_id=match_id,
                uploaded_by=user_id
            )
            
            db.session.add(upload_record)
            db.session.commit()
            
            # Save file with progress tracking
            if not self.save_file_chunked(file, file_path, upload_record):
                return upload_record, "File save failed"
            
            # Calculate SHA1 checksum
            sha1_checksum = self.calculate_sha1(file_path)
            if not sha1_checksum:
                upload_record.mark_failed("Checksum calculation failed")
                return upload_record, "Checksum calculation failed"
            
            # Check for malicious content
            if detect_malicious_content(file_path):
                os.remove(file_path)
                upload_record.mark_failed("Potentially malicious content detected")
                log_file_operation(
                    'MALICIOUS_CONTENT_DETECTED',
                    filename,
                    user_id=user_id,
                    upload_id=upload_record.id,
                    status='BLOCKED'
                )
                return upload_record, "File blocked due to security concerns"
            
            # Update upload record with checksum
            upload_record.sha1sum = sha1_checksum
            upload_record.mark_completed()
            
            log_file_operation(
                'UPLOAD_COMPLETED',
                filename,
                user_id=user_id,
                match_id=match_id,
                upload_id=upload_record.id,
                extra_data={
                    'file_size': file_size,
                    'file_type': file_type,
                    'sha1sum': sha1_checksum
                }
            )
            
            return upload_record, None
            
        except Exception as e:
            logger.error(f"Upload processing error: {str(e)}")
            if 'upload_record' in locals():
                upload_record.mark_failed(f"Upload processing failed: {str(e)}")
            return None, f"Upload processing failed: {str(e)}"
    
    def upload_file_async(self, file, file_type, user_id, match_id=None):
        """
        Upload file asynchronously
        
        Args:
            file: Werkzeug FileStorage object
            file_type: Type of file ('fixture' or 'zip')
            user_id: ID of user uploading file
            match_id: Associated match ID (for ZIP files)
        
        Returns:
            str: Upload ID for tracking
        """
        upload_id = f"{user_id}_{int(time.time())}"
        
        with self.upload_lock:
            if len(self.active_uploads) >= self.max_concurrent_uploads:
                return None, "Maximum concurrent uploads reached"
            
            future = self.executor.submit(
                self.process_upload, file, file_type, user_id, match_id
            )
            self.active_uploads[upload_id] = future
        
        return upload_id, None
    
    def get_upload_status(self, upload_id):
        """
        Get status of async upload
        
        Args:
            upload_id: Upload ID
        
        Returns:
            dict: Upload status information
        """
        with self.upload_lock:
            if upload_id not in self.active_uploads:
                return {'status': 'not_found'}
            
            future = self.active_uploads[upload_id]
            
            if future.done():
                try:
                    upload_record, error_message = future.result()
                    del self.active_uploads[upload_id]
                    
                    if error_message:
                        return {
                            'status': 'failed',
                            'error': error_message,
                            'upload_record': upload_record.to_dict() if upload_record else None
                        }
                    else:
                        return {
                            'status': 'completed',
                            'upload_record': upload_record.to_dict()
                        }
                except Exception as e:
                    del self.active_uploads[upload_id]
                    return {
                        'status': 'failed',
                        'error': str(e)
                    }
            else:
                return {'status': 'uploading'}
    
    def cancel_upload(self, upload_id):
        """
        Cancel async upload
        
        Args:
            upload_id: Upload ID
        
        Returns:
            bool: True if cancelled successfully
        """
        with self.upload_lock:
            if upload_id in self.active_uploads:
                future = self.active_uploads[upload_id]
                cancelled = future.cancel()
                if cancelled:
                    del self.active_uploads[upload_id]
                return cancelled
            return False
    
    def cleanup_failed_uploads(self):
        """Clean up failed upload files"""
        try:
            from app.models import FileUpload
            failed_uploads = FileUpload.query.filter_by(upload_status='failed').all()
            
            for upload in failed_uploads:
                if os.path.exists(upload.file_path):
                    try:
                        os.remove(upload.file_path)
                        logger.info(f"Cleaned up failed upload file: {upload.filename}")
                    except Exception as e:
                        logger.error(f"Failed to clean up file {upload.filename}: {str(e)}")
            
        except Exception as e:
            logger.error(f"Upload cleanup error: {str(e)}")
    
    def get_upload_statistics(self):
        """Get upload statistics"""
        try:
            from app.models import FileUpload
            stats = {
                'total_uploads': FileUpload.query.count(),
                'completed_uploads': FileUpload.query.filter_by(upload_status='completed').count(),
                'failed_uploads': FileUpload.query.filter_by(upload_status='failed').count(),
                'active_uploads': len(self.active_uploads),
                'fixture_uploads': FileUpload.query.filter_by(file_type='fixture').count(),
                'zip_uploads': FileUpload.query.filter_by(file_type='zip').count()
            }
            
            # Calculate total storage used
            completed_uploads = FileUpload.query.filter_by(upload_status='completed').all()
            total_storage = sum(upload.file_size for upload in completed_uploads)
            stats['total_storage_bytes'] = total_storage
            stats['total_storage_mb'] = round(total_storage / (1024 * 1024), 2)
            
            return stats
            
        except Exception as e:
            logger.error(f"Statistics calculation error: {str(e)}")
            return {}

    def save_file_streaming(self, file_stream, file_path, total_size, upload_record):
        """
        Save file from stream with memory optimization for very large files
        
        Args:
            file_stream: File stream object
            file_path: Destination file path
            total_size: Total expected file size
            upload_record: FileUpload database record
        
        Returns:
            bool: True if successful
        """
        try:
            bytes_written = 0
            last_progress_update = 0
            progress_update_threshold = max(1024 * 1024, total_size // 100)  # Update every 1MB or 1%
            
            # Use very large chunk size for streaming to minimize I/O operations
            stream_chunk_size = min(2 * 1024 * 1024, self.chunk_size * 256)  # Up to 2MB chunks
            
            logger.info(f"Starting streaming upload: {total_size} bytes, chunk size: {stream_chunk_size}")
            
            with open(file_path, 'wb') as f:
                while bytes_written < total_size:
                    try:
                        remaining = total_size - bytes_written
                        chunk_size = min(stream_chunk_size, remaining)
                        
                        chunk = file_stream.read(chunk_size)
                        if not chunk:
                            break
                        
                        f.write(chunk)
                        bytes_written += len(chunk)
                        
                        # Update progress less frequently for streaming
                        if bytes_written - last_progress_update >= progress_update_threshold:
                            progress = (bytes_written / total_size) * 100 if total_size > 0 else 100
                            upload_record.update_progress(progress)
                            last_progress_update = bytes_written
                            
                            # Log progress for large files (every 10%)
                            if total_size > 100 * 1024 * 1024 and int(progress) % 10 == 0:
                                logger.info(f"Streaming progress: {progress:.1f}% ({bytes_written}/{total_size} bytes)")
                        
                        # Force flush periodically to prevent memory buildup
                        if bytes_written % (20 * 1024 * 1024) == 0:  # Every 20MB
                            f.flush()
                            os.fsync(f.fileno())
                            
                    except MemoryError:
                        logger.error("Memory error during streaming upload")
                        upload_record.mark_failed("File too large - insufficient memory")
                        return False
                    except IOError as e:
                        logger.error(f"IO error during streaming upload: {str(e)}")
                        upload_record.mark_failed(f"File write error: {str(e)}")
                        return False
            
            # Final progress update
            upload_record.update_progress(100.0)
            logger.info(f"Streaming upload completed: {bytes_written} bytes written")
            return True
            
        except Exception as e:
            logger.error(f"Streaming upload error: {str(e)}")
            upload_record.mark_failed(f"Streaming upload failed: {str(e)}")
            return False
    
    def resume_upload(self, file, file_path, upload_record):
        """
        Resume a partially uploaded file
        
        Args:
            file: Werkzeug FileStorage object
            file_path: Destination file path
            upload_record: FileUpload database record
        
        Returns:
            bool: True if successful
        """
        try:
            # Check if partial file exists
            if not os.path.exists(file_path):
                logger.info("No partial file found, starting fresh upload")
                return self.save_file_chunked(file, file_path, upload_record)
            
            # Get current file size
            current_size = os.path.getsize(file_path)
            
            # Get expected total size
            file.seek(0, os.SEEK_END)
            total_size = file.tell()
            
            if current_size >= total_size:
                logger.info("File already completely uploaded")
                upload_record.update_progress(100.0)
                return True
            
            # Resume from where we left off
            file.seek(current_size)
            bytes_written = current_size
            last_progress_update = current_size
            progress_update_threshold = max(1024 * 1024, total_size // 100)
            
            # Use dynamic chunk size based on file size
            dynamic_chunk_size = self.chunk_size
            if total_size > 100 * 1024 * 1024:
                dynamic_chunk_size = min(1024 * 1024, self.chunk_size * 128)
            
            logger.info(f"Resuming upload from {current_size} bytes ({(current_size/total_size)*100:.1f}%)")
            
            with open(file_path, 'ab') as f:  # Append mode
                while bytes_written < total_size:
                    try:
                        remaining = total_size - bytes_written
                        chunk_size = min(dynamic_chunk_size, remaining)
                        
                        chunk = file.read(chunk_size)
                        if not chunk:
                            break
                        
                        f.write(chunk)
                        bytes_written += len(chunk)
                        
                        # Update progress
                        if bytes_written - last_progress_update >= progress_update_threshold:
                            progress = (bytes_written / total_size) * 100
                            upload_record.update_progress(progress)
                            last_progress_update = bytes_written
                            
                            logger.info(f"Resume progress: {progress:.1f}% ({bytes_written}/{total_size} bytes)")
                        
                        # Force flush periodically
                        if (bytes_written - current_size) % (10 * 1024 * 1024) == 0:
                            f.flush()
                            os.fsync(f.fileno())
                            
                    except Exception as e:
                        logger.error(f"Error during resume: {str(e)}")
                        upload_record.mark_failed(f"Resume failed: {str(e)}")
                        return False
            
            # Final progress update
            upload_record.update_progress(100.0)
            logger.info(f"Resume completed: {bytes_written} total bytes")
            return True
            
        except Exception as e:
            logger.error(f"Resume upload error: {str(e)}")
            upload_record.mark_failed(f"Resume failed: {str(e)}")
            return False

# Global file upload handler instance (lazy initialization)
file_upload_handler = None

def get_file_upload_handler():
    """Get file upload handler instance"""
    global file_upload_handler
    if file_upload_handler is None:
        file_upload_handler = FileUploadHandler()
    return file_upload_handler