"""
File scanner module - hashing, similarity detection, and metadata extraction.
"""
import hashlib
import os
from pathlib import Path
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
from collections import defaultdict
import json
from datetime import datetime


@dataclass
class FileInfo:
    """Information about a scanned file."""
    path: str
    size: int
    hash: str
    modified: float
    created: float
    extension: str
    mime_type: str = ""
    similarity_group: int = -1
    
    def to_dict(self) -> dict:
        return {
            "path": self.path,
            "size": self.size,
            "hash": self.hash,
            "modified": self.modified,
            "created": self.created,
            "extension": self.extension,
            "mime_type": self.mime_type,
            "similarity_group": self.similarity_group
        }
    
    @classmethod
    def from_dict(cls, data: dict) -> "FileInfo":
        return cls(**data)


@dataclass
class DuplicateGroup:
    """A group of duplicate files."""
    hash: str
    size: int
    files: List[FileInfo] = field(default_factory=list)
    
    def total_waste(self) -> int:
        """Calculate wasted space (keep one, rest are waste)."""
        if len(self.files) <= 1:
            return 0
        return self.size * (len(self.files) - 1)
    
    def to_dict(self) -> dict:
        return {
            "hash": self.hash,
            "size": self.size,
            "files": [f.to_dict() for f in self.files]
        }


class FileScanner:
    """Scan directories for duplicate files."""
    
    # Size thresholds for different hash strategies
    SMALL_FILE_THRESHOLD = 1024  # 1KB
    CHUNK_SIZE = 8192  # 8KB chunks for large files
    
    def __init__(self, progress_callback=None):
        self.progress_callback = progress_callback
        self.files: Dict[str, FileInfo] = {}
        self.duplicates: Dict[str, DuplicateGroup] = {}
        self.stats = {
            "total_files": 0,
            "total_size": 0,
            "duplicate_groups": 0,
            "duplicate_files": 0,
            "wasted_space": 0
        }
    
    def _calculate_hash(self, filepath: Path, quick: bool = False) -> str:
        """
        Calculate SHA256 hash of file content.
        
        Args:
            filepath: Path to file
            quick: If True, use quick hash for large files (first/last 1MB)
        
        Returns:
            Hex digest of SHA256 hash, or None on error
        """
        try:
            file_size = filepath.stat().st_size
            
            # For small files (< 1MB), hash entire content
            if file_size < 1024 * 1024:  # 1MB threshold
                with open(filepath, 'rb') as f:
                    return hashlib.sha256(f.read()).hexdigest()
            
            # For large files with quick=True, sample first + last 1MB
            if quick:
                hasher = hashlib.sha256()
                with open(filepath, 'rb') as f:
                    # First 1MB
                    hasher.update(f.read(1024 * 1024))
                    # Last 1MB
                    f.seek(max(0, file_size - 1024 * 1024))
                    hasher.update(f.read(1024 * 1024))
                return hasher.hexdigest()
            
            # Full hash for large files (streaming to avoid memory issues)
            hasher = hashlib.sha256()
            chunk_size = 128 * 1024  # 128KB chunks
            with open(filepath, 'rb') as f:
                while True:
                    chunk = f.read(chunk_size)
                    if not chunk:
                        break
                    hasher.update(chunk)
            return hasher.hexdigest()
            
        except (IOError, OSError, PermissionError) as e:
            # Log error and return None to skip file
            print(f"Warning: Skipping {filepath}: {e}")
            return None
    
    def _calculate_full_hash(self, filepath: Path) -> str:
        """Calculate full SHA256 hash (used for collision resolution)."""
        return self._calculate_hash(filepath, quick=False)
    
    def _get_mime_type(self, filepath: Path) -> str:
        """Get MIME type based on extension (simple implementation)."""
        ext_map = {
            '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png',
            '.gif': 'image/gif', '.bmp': 'image/bmp', '.webp': 'image/webp',
            '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.flac': 'audio/flac',
            '.mp4': 'video/mp4', '.avi': 'video/x-msvideo', '.mkv': 'video/x-matroska',
            '.pdf': 'application/pdf', '.doc': 'application/msword',
            '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            '.txt': 'text/plain', '.md': 'text/markdown', '.py': 'text/x-python',
            '.js': 'text/javascript', '.html': 'text/html', '.css': 'text/css',
            '.json': 'application/json', '.xml': 'application/xml',
            '.zip': 'application/zip', '.rar': 'application/vnd.rar',
            '.exe': 'application/x-msdownload', '.dll': 'application/x-msdownload',
        }
        return ext_map.get(filepath.suffix.lower(), 'application/octet-stream')
    
    def scan_directory(self, directory: str, extensions: Optional[List[str]] = None) -> Dict[str, DuplicateGroup]:
        """
        Scan a directory for duplicate files.
        
        Args:
            directory: Path to scan
            extensions: Optional list of extensions to filter (e.g., ['.jpg', '.png'])
        
        Returns:
            Dictionary of hash -> DuplicateGroup
        """
        dir_path = Path(directory).resolve()
        if not dir_path.exists():
            raise ValueError(f"Directory does not exist: {directory}")
        
        self.files = {}
        self.duplicates = {}
        self.stats = {
            "total_files": 0,
            "total_size": 0,
            "duplicate_groups": 0,
            "duplicate_files": 0,
            "wasted_space": 0
        }
        
        # Phase 1: Collect all files
        all_files = []
        for root, dirs, files in os.walk(dir_path):
            # Skip hidden directories
            dirs[:] = [d for d in dirs if not d.startswith('.')]
            
            for filename in files:
                if filename.startswith('.'):
                    continue
                
                filepath = Path(root) / filename
                
                # Filter by extension if specified
                if extensions and filepath.suffix.lower() not in extensions:
                    continue
                
                try:
                    stat = filepath.stat()
                    all_files.append((filepath, stat.st_size, stat.st_mtime, stat.st_ctime))
                    self.stats["total_size"] += stat.st_size
                except (IOError, OSError):
                    continue
        
        self.stats["total_files"] = len(all_files)
        
        if self.progress_callback:
            self.progress_callback("hashing", 0, len(all_files))
        
        # Phase 2: Group by size first (optimization)
        size_groups: Dict[int, List[Tuple[Path, int, float, float]]] = defaultdict(list)
        for filepath, size, mtime, ctime in all_files:
            size_groups[size].append((filepath, size, mtime, ctime))
        
        # Phase 3: Hash files with same size (two-phase hashing)
        # Phase 3a: Quick hash (first pass)
        quick_hash_groups: Dict[str, List[Tuple[Path, int, float, float]]] = defaultdict(list)
        processed = 0

        for size, files in size_groups.items():
            if len(files) == 1:
                # Single file with this size, no need to hash
                filepath, fsize, mtime, ctime = files[0]
                info = FileInfo(
                    path=str(filepath),
                    size=fsize,
                    hash="unique_by_size",
                    modified=mtime,
                    created=ctime,
                    extension=filepath.suffix.lower(),
                    mime_type=self._get_mime_type(filepath)
                )
                self.files[str(filepath)] = info
                continue

            # Multiple files with same size - use quick hash first
            for filepath, fsize, mtime, ctime in files:
                file_hash = self._calculate_hash(filepath, quick=True)
                if file_hash is None:
                    continue  # Skip files with hash errors
                quick_hash_groups[file_hash].append((filepath, fsize, mtime, ctime, filepath.suffix.lower()))
                
                processed += 1
                if self.progress_callback and processed % 10 == 0:
                    self.progress_callback("hashing (quick)", processed, len(all_files))

        # Phase 3b: Resolve collisions with full hash
        hash_groups: Dict[str, List[FileInfo]] = defaultdict(list)
        for quick_hash, files in quick_hash_groups.items():
            if len(files) == 1:
                # No collision, use quick hash
                filepath, fsize, mtime, ctime, ext = files[0]
                info = FileInfo(
                    path=str(filepath),
                    size=fsize,
                    hash=quick_hash,
                    modified=mtime,
                    created=ctime,
                    extension=ext,
                    mime_type=self._get_mime_type(Path(filepath))
                )
                hash_groups[quick_hash].append(info)
                self.files[str(filepath)] = info
            else:
                # Collision detected - use full hash to verify
                if self.progress_callback:
                    self.progress_callback("hashing (full)", processed, len(all_files))
                
                full_hash_groups: Dict[str, List[Tuple[Path, int, float, float, str]]] = defaultdict(list)
                for filepath, fsize, mtime, ctime, ext in files:
                    full_hash = self._calculate_full_hash(filepath)
                    if full_hash is None:
                        continue  # Skip files with hash errors
                    full_hash_groups[full_hash].append((filepath, fsize, mtime, ctime, ext))
                
                # Add to final hash groups
                for full_hash, file_list in full_hash_groups.items():
                    for filepath, fsize, mtime, ctime, ext in file_list:
                        info = FileInfo(
                            path=str(filepath),
                            size=fsize,
                            hash=full_hash,
                            modified=mtime,
                            created=ctime,
                            extension=ext,
                            mime_type=self._get_mime_type(Path(filepath))
                        )
                        hash_groups[full_hash].append(info)
                        self.files[str(filepath)] = info

        # Phase 4: Build duplicate groups
        for file_hash, files in hash_groups.items():
            if len(files) > 1:
                group = DuplicateGroup(hash=file_hash, size=files[0].size, files=files)
                self.duplicates[file_hash] = group
                self.stats["duplicate_groups"] += 1
                self.stats["duplicate_files"] += len(files)
                self.stats["wasted_space"] += group.total_waste()

        if self.progress_callback:
            self.progress_callback("complete", len(all_files), len(all_files))

        return self.duplicates
    
    def get_statistics(self) -> dict:
        """Get scanning statistics."""
        return self.stats.copy()
    
    def save_results(self, filepath: str) -> None:
        """Save scan results to JSON file."""
        data = {
            "scan_date": datetime.now().isoformat(),
            "statistics": self.stats,
            "duplicates": {k: v.to_dict() for k, v in self.duplicates.items()}
        }
        with open(filepath, 'w') as f:
            json.dump(data, f, indent=2)
    
    def load_results(self, filepath: str) -> None:
        """Load scan results from JSON file."""
        with open(filepath, 'r') as f:
            data = json.load(f)
        
        self.stats = data["statistics"]
        for hash_val, group_data in data["duplicates"].items():
            files = [FileInfo.from_dict(f) for f in group_data["files"]]
            self.duplicates[hash_val] = DuplicateGroup(
                hash=group_data["hash"],
                size=group_data["size"],
                files=files
            )
