"""
Actions module - safe file operations with undo capability.
"""
import os
import shutil
from pathlib import Path
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
from datetime import datetime
import json


@dataclass
class FileOperation:
    """Record of a file operation for undo capability."""
    operation_type: str  # 'move', 'delete'
    source_path: str
    dest_path: str
    timestamp: float
    size: int
    hash: str
    backup_path: Optional[str] = None
    
    def to_dict(self) -> dict:
        return {
            "operation_type": self.operation_type,
            "source_path": self.source_path,
            "dest_path": self.dest_path,
            "timestamp": self.timestamp,
            "size": self.size,
            "hash": self.hash,
            "backup_path": self.backup_path
        }
    
    @classmethod
    def from_dict(cls, data: dict) -> "FileOperation":
        return cls(**data)


@dataclass
class OperationBatch:
    """A batch of operations that can be undone together."""
    batch_id: str
    timestamp: float
    operations: List[FileOperation] = field(default_factory=list)
    description: str = ""
    
    def to_dict(self) -> dict:
        return {
            "batch_id": self.batch_id,
            "timestamp": self.timestamp,
            "operations": [op.to_dict() for op in self.operations],
            "description": self.description
        }
    
    @classmethod
    def from_dict(cls, data: dict) -> "OperationBatch":
        ops = [FileOperation.from_dict(op) for op in data["operations"]]
        return cls(
            batch_id=data["batch_id"],
            timestamp=data["timestamp"],
            operations=ops,
            description=data.get("description", "")
        )


class UndoManager:
    """Manage undo operations."""
    
    def __init__(self, data_dir: str = ".dedup_data"):
        self.data_dir = Path(data_dir)
        self.data_dir.mkdir(parents=True, exist_ok=True)
        self.operations_log = self.data_dir / "operations.json"
        self.batches: Dict[str, OperationBatch] = {}
        self._load_operations()
    
    def _load_operations(self) -> None:
        """Load operations from disk."""
        if self.operations_log.exists():
            try:
                with open(self.operations_log, 'r') as f:
                    data = json.load(f)
                for batch_data in data.get("batches", []):
                    batch = OperationBatch.from_dict(batch_data)
                    self.batches[batch.batch_id] = batch
            except (json.JSONDecodeError, KeyError):
                self.batches = {}
    
    def _save_operations(self) -> None:
        """Save operations to disk."""
        data = {
            "last_updated": datetime.now().isoformat(),
            "batches": [b.to_dict() for b in self.batches.values()]
        }
        with open(self.operations_log, 'w') as f:
            json.dump(data, f, indent=2)
    
    def create_batch(self, description: str = "") -> str:
        """Create a new operation batch."""
        import time
        # Use microsecond precision to ensure unique batch IDs
        batch_id = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
        batch = OperationBatch(
            batch_id=batch_id,
            timestamp=time.time(),
            description=description
        )
        self.batches[batch_id] = batch
        return batch_id
    
    def add_operation(self, batch_id: str, operation: FileOperation) -> None:
        """Add an operation to a batch."""
        if batch_id not in self.batches:
            raise ValueError(f"Batch {batch_id} does not exist")
        self.batches[batch_id].operations.append(operation)
        self._save_operations()
    
    def get_recent_batches(self, limit: int = 10) -> List[OperationBatch]:
        """Get recent operation batches."""
        sorted_batches = sorted(
            self.batches.values(),
            key=lambda b: b.timestamp,
            reverse=True
        )
        return sorted_batches[:limit]
    
    def undo_batch(self, batch_id: str, dry_run: bool = False) -> Tuple[bool, List[str]]:
        """
        Undo a batch of operations.
        
        Returns:
            Tuple of (success, list of messages)
        """
        if batch_id not in self.batches:
            return False, [f"Batch {batch_id} not found"]
        
        batch = self.batches[batch_id]
        messages = []
        success = True
        restored_count = 0
        failed_count = 0
        
        # Process operations in reverse order
        for op in reversed(batch.operations):
            try:
                if op.operation_type == "move":
                    # Move file back from dest to source
                    dest = Path(op.dest_path)
                    source = Path(op.source_path)
                    
                    if dry_run:
                        messages.append(f"Would restore: {op.dest_path} -> {op.source_path}")
                        continue
                    
                    if dest.exists():
                        source.parent.mkdir(parents=True, exist_ok=True)
                        shutil.move(str(dest), str(source))
                        
                        # VERIFY: Check file actually appeared at source
                        if source.exists():
                            messages.append(f"Restored: {op.source_path}")
                            restored_count += 1
                        else:
                            messages.append(f"ERROR: Move succeeded but file not found at {op.source_path}")
                            failed_count += 1
                            success = False
                    else:
                        messages.append(f"Warning: {op.dest_path} not found")
                        failed_count += 1
                
                elif op.operation_type == "delete":
                    # Restore from backup
                    if op.backup_path and Path(op.backup_path).exists():
                        backup = Path(op.backup_path)
                        source = Path(op.source_path)
                        
                        if dry_run:
                            messages.append(f"Would restore from backup: {op.backup_path} -> {op.source_path}")
                            continue
                        
                        source.parent.mkdir(parents=True, exist_ok=True)
                        shutil.move(str(backup), str(source))
                        
                        # VERIFY: Check file actually appeared at source
                        if source.exists():
                            messages.append(f"Restored from backup: {op.source_path}")
                            restored_count += 1
                        else:
                            messages.append(f"ERROR: Restore succeeded but file not found at {op.source_path}")
                            failed_count += 1
                            success = False
                    else:
                        messages.append(f"Warning: No backup found for {op.source_path}")
                        failed_count += 1
                        success = False
                        
            except Exception as e:
                messages.append(f"Error restoring {op.source_path}: {str(e)}")
                failed_count += 1
                success = False
        
        # Add summary
        messages.append(f"\nUndo summary: {restored_count} restored, {failed_count} failed")
        
        # Remove batch after successful undo
        if success and not dry_run:
            del self.batches[batch_id]
            self._save_operations()
        
        return success, messages


class FileManager:
    """Safe file operations manager."""
    
    def __init__(self, undo_manager: Optional[UndoManager] = None, dry_run: bool = False):
        self.undo_manager = undo_manager or UndoManager()
        self.dry_run = dry_run
        self.current_batch: Optional[str] = None
        self.messages: List[str] = []
    
    def start_batch(self, description: str = "") -> str:
        """Start a new operation batch."""
        self.current_batch = self.undo_manager.create_batch(description)
        self.messages = []
        return self.current_batch
    
    def move_file(self, source: str, dest: str, keep_original: bool = False) -> bool:
        """
        Move a file safely.
        
        Args:
            source: Source file path
            dest: Destination path
            keep_original: If True, copy instead of move
        
        Returns:
            True if successful
        """
        if not self.current_batch:
            raise ValueError("No active batch. Call start_batch() first.")
        
        source_path = Path(source)
        dest_path = Path(dest)
        
        if not source_path.exists():
            self.messages.append(f"Source does not exist: {source}")
            return False
        
        if self.dry_run:
            action = "copy" if keep_original else "move"
            self.messages.append(f"Would {action}: {source} -> {dest}")
            return True
        
        try:
            # Get file info for undo
            stat = source_path.stat()
            
            # Create destination directory
            dest_path.parent.mkdir(parents=True, exist_ok=True)
            
            if keep_original:
                shutil.copy2(str(source_path), str(dest_path))
                self.messages.append(f"Copied: {source} -> {dest}")
            else:
                shutil.move(str(source_path), str(dest_path))
                self.messages.append(f"Moved: {source} -> {dest}")
            
            # Record operation for undo
            op = FileOperation(
                operation_type="move",
                source_path=str(source_path),
                dest_path=str(dest_path),
                timestamp=datetime.now().timestamp(),
                size=stat.st_size,
                hash=""  # Could calculate if needed
            )
            self.undo_manager.add_operation(self.current_batch, op)
            return True
            
        except Exception as e:
            self.messages.append(f"Error moving {source}: {str(e)}")
            return False
    
    def delete_file(self, filepath: str, use_trash: bool = True) -> bool:
        """
        Delete a file safely (with backup for undo).
        
        Args:
            filepath: File to delete
            use_trash: If True, move to backup folder instead of permanent delete
        
        Returns:
            True if successful
        """
        if not self.current_batch:
            raise ValueError("No active batch. Call start_batch() first.")
        
        file_path = Path(filepath)
        
        if not file_path.exists():
            self.messages.append(f"File does not exist: {filepath}")
            return False
        
        if self.dry_run:
            self.messages.append(f"Would delete: {filepath}")
            return True
        
        try:
            # Get file info for undo
            stat = file_path.stat()
            backup_path = None
            
            if use_trash:
                # Move to backup folder with collision-resistant naming
                backup_dir = self.undo_manager.data_dir / "trash" / datetime.now().strftime("%Y%m%d")
                backup_dir.mkdir(parents=True, exist_ok=True)
                
                base_name = file_path.name
                backup_path = self._get_unique_trash_path(backup_dir, base_name)
                
                shutil.move(str(file_path), backup_path)
                self.messages.append(f"Moved to trash: {filepath}")
            else:
                file_path.unlink()
                self.messages.append(f"Deleted: {filepath}")
            
            # Record operation for undo
            op = FileOperation(
                operation_type="delete",
                source_path=str(file_path),
                dest_path="",
                timestamp=datetime.now().timestamp(),
                size=stat.st_size,
                hash="",
                backup_path=backup_path
            )
            self.undo_manager.add_operation(self.current_batch, op)
            return True
            
        except Exception as e:
            self.messages.append(f"Error deleting {filepath}: {str(e)}")
            return False
    
    def _get_unique_trash_path(self, backup_dir: Path, base_name: str) -> str:
        """Generate a unique trash path with collision-resistant naming."""
        import random
        import string
        
        name = base_name
        ext = ""
        if "." in base_name:
            name_parts = base_name.rsplit(".", 1)
            name = name_parts[0]
            ext = "." + name_parts[1]
        
        # Try original name first, then add suffixes
        for i in range(100):
            if i == 0:
                candidate = base_name
            elif i == 1:
                # Add PID + random suffix for collision resistance
                random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
                candidate = f"{name}_{os.getpid()}_{random_suffix}{ext}"
            else:
                # Fall back to counter
                candidate = f"{name}_{i}{ext}"
            
            candidate_path = backup_dir / candidate
            if not candidate_path.exists():
                return str(candidate_path)
        
        # Last resort: timestamp + random
        ts = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
        random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
        candidate = f"{name}_{ts}_{random_suffix}{ext}"
        return str(backup_dir / candidate)
    
    def get_messages(self) -> List[str]:
        """Get operation messages."""
        return self.messages.copy()
    
    def format_size(self, size: int) -> str:
        """Format size in human-readable format."""
        for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
            if size < 1024:
                return f"{size:.2f} {unit}"
            size /= 1024
        return f"{size:.2f} PB"
