#!/usr/bin/env python3
"""
Move Module - Handles file moving and timestamp management
"""

import os
import shutil
import time
import pwd
import grp
import gc
import sqlite3
from pathlib import Path
from datetime import datetime
from typing import Optional, List, Dict, Union, Any
from modules.base_module import LoggingMixin
from modules.universal_logger import get_logger

logger = get_logger('MoveManager')  # For module-level functions

# Import UnifiedDatabase for file hash deduplication
try:
    from .unified_database import UnifiedDatabase
except ImportError:
    try:
        from unified_database import UnifiedDatabase
    except ImportError:
        UnifiedDatabase = None
        logger.warning("UnifiedDatabase not available - file hash deduplication disabled")

# Import date utilities for EXIF timestamp updates
try:
    from modules.date_utils import DateHandler
    DATE_UTILS_AVAILABLE = True
except ImportError:
    try:
        from .date_utils import DateHandler
        DATE_UTILS_AVAILABLE = True
    except ImportError:
        DATE_UTILS_AVAILABLE = False
        logger.debug("DateHandler not available - EXIF updates disabled")


def _extract_exif_date(filepath: Path) -> Optional[datetime]:
    """Extract date from EXIF metadata using exiftool

    Checks DateTimeOriginal, CreateDate, then DateCreated in order of preference.
    Returns None if no valid date found.
    """
    import subprocess
    try:
        result = subprocess.run([
            'exiftool', '-s', '-s', '-s',
            '-DateTimeOriginal', '-CreateDate', '-DateCreated',
            str(filepath)
        ], capture_output=True, text=True, timeout=10)

        if result.returncode == 0 and result.stdout.strip():
            # exiftool returns dates in format "YYYY:MM:DD HH:MM:SS"
            for line in result.stdout.strip().split('\n'):
                date_str = line.strip()
                if date_str and date_str != '-':
                    try:
                        # Try parsing EXIF date format
                        return datetime.strptime(date_str, "%Y:%m:%d %H:%M:%S")
                    except ValueError:
                        try:
                            # Try alternate format without time
                            return datetime.strptime(date_str, "%Y:%m:%d")
                        except ValueError:
                            continue
    except (subprocess.TimeoutExpired, FileNotFoundError):
        pass
    except Exception as e:
        logger.debug(f"EXIF date extraction failed for {filepath}: {e}")
    return None


# Pushover notifier handled by scheduler/notification system

class MoveManager(LoggingMixin):
    """Manages file moves with proper timestamp handling and notifications"""

    def __init__(self, log_callback=None, notifier=None, unified_db=None, face_recognition_enabled=True, on_download_complete=None, event_emitter=None):
        """
        Initialize the MoveManager

        Args:
            log_callback: Optional callback function for logging (tag, level, message)
            notifier: Optional PushoverNotifier instance for batch notifications
            unified_db: Optional UnifiedDatabase instance for file hash deduplication
            face_recognition_enabled: Enable face recognition filtering (default: True)
            on_download_complete: Optional callback when downloads complete (platform, source, count)
            event_emitter: Optional ScraperEventEmitter instance for real-time scraping monitor
        """
        # Initialize logging via mixin
        self._init_logger('MoveManager', log_callback, default_module='Move')

        self.notifier = notifier
        self.unified_db = unified_db
        self.face_recognition_enabled = face_recognition_enabled
        self.on_download_complete = on_download_complete
        self.event_emitter = event_emitter
        self.current_session = {}  # Store session context for event emission
        self.face_module = None
        self.stats = {
            'moved': 0,
            'skipped': 0,
            'failed': 0,
            'duplicates': 0,  # Track files skipped due to duplicate content
            'review_queue': 0  # Track files moved to review queue (no face match)
        }
        # Track moved files for batch notifications
        self.batch_context = None
        self.moved_files = []
        self.review_queue_files = []  # Separate tracking for review queue items
        self.repost_queue = []  # Queue for repost detection processing (done after moves complete)
        self._last_move_had_face_recognition = False  # Tracks if last move_file ran face recognition

        # Background thread pool for non-blocking post-processing (thumbnails, dimensions)
        from concurrent.futures import ThreadPoolExecutor
        self._bg_executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix='move_bg')

        # Initialize face recognition if enabled
        if self.face_recognition_enabled and self.unified_db:
            try:
                from modules.face_recognition_module import FaceRecognitionModule
                self.face_module = FaceRecognitionModule(
                    unified_db=self.unified_db,
                    log_callback=lambda msg, lvl: self.log(msg, lvl)
                )
                self.log("Face recognition module initialized", "info")
            except Exception as e:
                self.log(f"Failed to initialize face recognition: {e}", "warning")
                self.face_module = None

        # Cache face recognition settings (read once from DB, reused per file)
        self._face_recognition_tolerance = None
        self._review_path = None
        self._face_recognition_settings_cache = None  # Raw JSON settings blob
        self._video_face_settings_cache = None  # Parsed video settings dict

        # Initialize activity status manager for real-time updates
        from modules.activity_status import get_activity_manager
        self.activity_manager = get_activity_manager(unified_db)

        # Log database status for debugging hash deduplication
        if self.unified_db:
            self.log("MoveManager initialized with UnifiedDatabase - hash deduplication ENABLED", "info")
        else:
            self.log("MoveManager initialized WITHOUT UnifiedDatabase - hash deduplication DISABLED", "warning")

        if not UnifiedDatabase:
            self.log("UnifiedDatabase class not imported - hash deduplication DISABLED", "warning")

    def set_session_context(self, platform: str, account: str, session_id: str):
        """Set context for current scraping session for event emission

        Args:
            platform: Platform name (e.g., 'instagram', 'snapchat')
            account: Account/username being scraped
            session_id: Unique session identifier for this scraping run
        """
        self.current_session = {
            'platform': platform,
            'account': account,
            'session_id': session_id
        }
        self.log(f"Session context set: {platform}/{account} ({session_id})", "debug")

    def release_models(self):
        """
        Release ML models to free memory.
        Call this after batch processing to prevent OOM in long-running services.
        Models will be lazy-loaded again when needed.
        """
        if self.face_module is not None:
            self.face_module.release_model()
            self.log("Face recognition model released to free memory", "info")
        gc.collect()

    def _is_image_file(self, file_path: Path) -> bool:
        """Check if file is an image (not video)"""
        image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.heic'}
        return file_path.suffix.lower() in image_extensions

    def _is_video_file(self, file_path: Path) -> bool:
        """Check if file is a video"""
        video_extensions = {'.mp4', '.mov', '.avi', '.mkv', '.webm', '.flv', '.m4v'}
        return file_path.suffix.lower() in video_extensions

    def _generate_thumbnail_cache(self, file_path: Path, content_hash: str = None) -> None:
        """
        Pre-generate and cache thumbnail for a file.
        This speeds up page loading by having thumbnails ready in the database.

        Uses content hash (SHA256 of file content) as cache key so thumbnails
        survive file moves (e.g., to recycle bin).

        Args:
            file_path: Path to the media file
            content_hash: Optional pre-computed SHA256 content hash
        """
        try:
            import hashlib
            import io
            import subprocess
            from PIL import Image

            thumb_db_path = Path('/opt/media-downloader/database/thumbnails.db')

            # Calculate content hash for cache key (survives file moves)
            if content_hash:
                file_hash = content_hash
            else:
                # Calculate SHA256 of file content
                sha256 = hashlib.sha256()
                with open(file_path, 'rb') as f:
                    for chunk in iter(lambda: f.read(65536), b''):
                        sha256.update(chunk)
                file_hash = sha256.hexdigest()

            file_mtime = file_path.stat().st_mtime

            # Check if already cached
            conn = sqlite3.connect(str(thumb_db_path), timeout=10.0)
            try:
                cursor = conn.cursor()
                cursor.execute("SELECT 1 FROM thumbnails WHERE file_hash = ?", (file_hash,))
                if cursor.fetchone():
                    return  # Already cached

                # Generate thumbnail
                thumbnail_data = None
                max_size = (300, 300)

                if self._is_video_file(file_path):
                    # Video thumbnail using ffmpeg
                    try:
                        result = subprocess.run([
                            'ffmpeg', '-i', str(file_path),
                            '-ss', '00:00:01.000', '-vframes', '1',
                            '-f', 'image2pipe', '-vcodec', 'mjpeg', '-'
                        ], capture_output=True, timeout=10)

                        if result.returncode == 0 and result.stdout:
                            img = Image.open(io.BytesIO(result.stdout))
                            img.thumbnail(max_size, Image.Resampling.LANCZOS)
                            if img.mode != 'RGB':
                                img = img.convert('RGB')
                            buffer = io.BytesIO()
                            img.save(buffer, format='JPEG', quality=85)
                            thumbnail_data = buffer.getvalue()
                    except Exception as e:
                        self.log(f"Video thumbnail generation failed: {e}", "debug")

                elif self._is_image_file(file_path):
                    # Image thumbnail
                    try:
                        img = Image.open(file_path)
                        img.thumbnail(max_size, Image.Resampling.LANCZOS)

                        if img.mode in ('RGBA', 'LA', 'P'):
                            background = Image.new('RGB', img.size, (255, 255, 255))
                            if img.mode == 'P':
                                img = img.convert('RGBA')
                            background.paste(img, mask=img.split()[-1] if img.mode in ('RGBA', 'LA') else None)
                            img = background

                        buffer = io.BytesIO()
                        img.save(buffer, format='JPEG', quality=85)
                        thumbnail_data = buffer.getvalue()
                    except Exception as e:
                        self.log(f"Image thumbnail generation failed: {e}", "debug")

                # Cache the thumbnail
                if thumbnail_data:
                    from datetime import datetime
                    cursor.execute("""
                        INSERT OR REPLACE INTO thumbnails
                        (file_hash, file_path, thumbnail_data, created_at, file_mtime)
                        VALUES (?, ?, ?, ?, ?)
                    """, (file_hash, str(file_path), thumbnail_data, datetime.now().isoformat(), file_mtime))
                    conn.commit()
                    self.log(f"Cached thumbnail: {file_path.name}", "debug")
            finally:
                conn.close()

        except Exception as e:
            self.log(f"Thumbnail cache generation failed for {file_path.name}: {e}", "debug")

    def _record_file_inventory_bg(self, destination: Path, source_name_file: str,
                                   platform: str, source_name: str,
                                   moved_to_review: bool, file_hash: str = None,
                                   timestamp: 'datetime' = None) -> None:
        """Record file in inventory table with dimensions (runs in background thread).

        Args:
            destination: Final destination path
            source_name_file: Original source filename
            platform: Platform name
            source_name: Source/username
            moved_to_review: Whether file was sent to review queue
            file_hash: Pre-computed file hash (or None)
            timestamp: Post date extracted from filename (or None to use current time)
        """
        try:
            location = 'review' if moved_to_review else 'final'
            content_type_val = 'image' if self._is_image_file(destination) else 'video'

            from datetime import datetime, timezone
            file_stat = destination.stat()
            if timestamp:
                inventory_created = timestamp.strftime("%Y-%m-%dT%H:%M:%SZ")
            else:
                inventory_created = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

            # Look up download method from downloads table
            method = None
            if platform == 'instagram':
                try:
                    with self.unified_db.get_connection() as conn:
                        cursor = conn.cursor()
                        cursor.execute('''
                            SELECT method FROM downloads
                            WHERE filename = ? AND platform = 'instagram'
                            LIMIT 1
                        ''', (source_name_file,))
                        row = cursor.fetchone()
                        if row and row[0]:
                            method = row[0]
                except Exception:
                    pass

            # Extract media dimensions
            width, height = None, None
            try:
                if content_type_val == 'image':
                    from PIL import Image
                    with Image.open(destination) as img:
                        width, height = img.size
                elif content_type_val == 'video':
                    import subprocess
                    result = subprocess.run(
                        ['ffprobe', '-v', 'error', '-select_streams', 'v:0',
                         '-show_entries', 'stream=width,height', '-of', 'csv=p=0',
                         str(destination)],
                        capture_output=True, text=True, timeout=10
                    )
                    if result.returncode == 0 and result.stdout.strip():
                        parts = result.stdout.strip().split(',')
                        if len(parts) >= 2:
                            width, height = int(parts[0]), int(parts[1])
            except Exception as e:
                self.log(f"Could not extract dimensions for {destination.name}: {e}", "debug")

            self.unified_db.upsert_file_inventory(
                file_path=str(destination),
                filename=destination.name,
                platform=platform,
                source=source_name,
                content_type=content_type_val,
                file_size=file_stat.st_size,
                file_hash=file_hash if file_hash else None,
                width=width,
                height=height,
                location=location,
                created_date=inventory_created,
                method=method
            )
            self.log(f"Recorded in file_inventory: {destination.name} (location={location})", "debug")

            # Queue for discovery scans (embedding) for files going to final
            if location == 'final':
                try:
                    self.unified_db.queue_file_for_discovery(
                        str(destination),
                        scan_types=['embedding'],
                        priority=5
                    )
                    self.log(f"Queued {destination.name} for discovery scans", "debug")
                except Exception as e:
                    self.log(f"Failed to queue for discovery: {e}", "debug")

        except Exception as e:
            self.log(f"Failed to record in file_inventory: {e}", "debug")

    def _get_face_recognition_settings(self) -> dict:
        """Get face recognition settings from database (cached per session).

        Returns:
            dict: Face recognition settings, or empty dict if unavailable.
        """
        if self._face_recognition_settings_cache is not None:
            return self._face_recognition_settings_cache

        if self.unified_db:
            try:
                import json
                with self.unified_db.get_connection() as conn:
                    cursor = conn.cursor()
                    cursor.execute("SELECT value FROM settings WHERE key = 'face_recognition'")
                    result = cursor.fetchone()
                    if result:
                        self._face_recognition_settings_cache = json.loads(result[0])
                        return self._face_recognition_settings_cache
            except Exception as e:
                self.log(f"Failed to read face recognition settings: {e}", "debug")

        self._face_recognition_settings_cache = {}
        return self._face_recognition_settings_cache

    def _get_face_recognition_tolerance(self, is_video: bool = False, source: str = None) -> float:
        """Get face recognition tolerance from database settings

        Args:
            is_video: If True, get video_tolerance instead of image tolerance
            source: Optional source username (e.g., 'evalongoria') for source-based tolerance

        Returns:
            float: Tolerance value (0.0-1.0), defaults to 0.15 for images, 0.30 for videos
        """
        settings = self._get_face_recognition_settings()

        if settings:
            # Get base tolerance (video or image)
            if is_video:
                base_tolerance = float(settings.get('video_tolerance', settings.get('tolerance', 0.30)))
            else:
                base_tolerance = float(settings.get('tolerance', 0.15))

            # Check for source-specific tolerance
            if source:
                source_tolerances = settings.get('source_tolerances', {})
                if source in source_tolerances:
                    source_tolerance = float(source_tolerances[source])
                    if is_video:
                        tolerance = max(source_tolerance, base_tolerance)
                        self.log(f"Using max of source ({source_tolerance}) and video ({base_tolerance}) tolerance for '{source}': {tolerance}", "debug")
                    else:
                        tolerance = source_tolerance
                        self.log(f"Using source-based tolerance for '{source}': {tolerance}", "debug")
                    return tolerance

            return base_tolerance

        # Default: 0.15 for images, 0.30 for videos
        return 0.30 if is_video else 0.15

    def _get_review_path(self) -> str:
        """Get review path from database settings

        Returns:
            str: Review path, defaults to '/opt/immich/review' if not set
        """
        # Return cached value if available
        if self._review_path is not None:
            return self._review_path

        # Try to read from database
        if self.unified_db:
            try:
                import json
                with self.unified_db.get_connection() as conn:
                    cursor = conn.cursor()
                    cursor.execute("SELECT value FROM settings WHERE key = 'face_recognition'")
                    result = cursor.fetchone()
                    if result:
                        settings = json.loads(result[0])
                        review_path = settings.get('review_path', '/opt/immich/review')
                        self._review_path = review_path
                        self.log(f"Using review path: {review_path}", "debug")
                        return review_path
            except Exception as e:
                self.log(f"Failed to read review path from database: {e}", "debug")

        # Default to /opt/immich/review if not found
        self._review_path = '/opt/immich/review'
        return '/opt/immich/review'

    def _get_video_face_recognition_settings(self) -> dict:
        """Get video face recognition settings from database (cached per session).

        Returns:
            dict: Settings with enable_video_recognition, video_face_frames, frame_positions
        """
        if self._video_face_settings_cache is not None:
            return self._video_face_settings_cache

        default_settings = {
            'enable_video_recognition': False,
            'video_face_frames': 3,
            'frame_positions': [0.1, 0.5, 0.9]
        }

        settings = self._get_face_recognition_settings()
        if settings:
            self._video_face_settings_cache = {
                'enable_video_recognition': bool(settings.get('enable_video_recognition', False)),
                'video_face_frames': int(settings.get('video_face_frames', 3)),
                'frame_positions': settings.get('frame_positions', [0.1, 0.5, 0.9])
            }
        else:
            self._video_face_settings_cache = default_settings

        return self._video_face_settings_cache

    def _apply_ownership(self, file_path: Path) -> None:
        """Apply configured file ownership to moved files

        Args:
            file_path: Path to file to change ownership
        """
        if not self.unified_db:
            return

        try:
            # Get ownership settings from database
            with self.unified_db.get_connection() as conn:
                cursor = conn.cursor()
                cursor.execute("SELECT value FROM settings WHERE key = 'file_ownership'")
                result = cursor.fetchone()

                if not result:
                    return  # No ownership configured

                import json
                settings = json.loads(result[0])

                # Check if ownership is enabled
                if not settings.get('enabled', False):
                    return

                owner = settings.get('owner', '').strip()
                group = settings.get('group', '').strip()

                if not owner and not group:
                    return  # Nothing to change

                # Get current file ownership
                stat_info = file_path.stat()
                uid = stat_info.st_uid
                gid = stat_info.st_gid

                # Resolve owner username to UID
                if owner:
                    try:
                        uid = pwd.getpwnam(owner).pw_uid
                    except KeyError:
                        self.log(f"Warning: User '{owner}' not found, skipping ownership change", "warning")
                        return

                # Resolve group name to GID
                if group:
                    try:
                        gid = grp.getgrnam(group).gr_gid
                    except KeyError:
                        self.log(f"Warning: Group '{group}' not found, skipping ownership change", "warning")
                        return

                # Change ownership
                os.chown(file_path, uid, gid)
                self.log(f"Changed ownership: {file_path.name} → {owner}:{group}", "debug")

        except PermissionError:
            self.log(f"Permission denied changing ownership of {file_path.name} (run as root/sudo)", "warning")
        except Exception as e:
            self.log(f"Failed to apply ownership to {file_path.name}: {e}", "debug")

    def _apply_ownership_to_path(self, dir_path: Path) -> None:
        """Apply configured file ownership to a directory and all parent directories

        Args:
            dir_path: Path to directory to change ownership (and parents)
        """
        if not self.unified_db or not dir_path.exists():
            return

        try:
            # Get ownership settings from database
            with self.unified_db.get_connection() as conn:
                cursor = conn.cursor()
                cursor.execute("SELECT value FROM settings WHERE key = 'file_ownership'")
                result = cursor.fetchone()

                if not result:
                    return  # No ownership configured

                import json
                settings = json.loads(result[0])

                # Check if ownership is enabled
                if not settings.get('enabled', False):
                    return

                owner = settings.get('owner', '').strip()
                group = settings.get('group', '').strip()

                if not owner and not group:
                    return  # Nothing to change

                # Resolve owner username to UID
                uid = -1  # -1 means don't change
                gid = -1  # -1 means don't change

                if owner:
                    try:
                        uid = pwd.getpwnam(owner).pw_uid
                    except KeyError:
                        self.log(f"Warning: User '{owner}' not found", "warning")
                        return

                if group:
                    try:
                        gid = grp.getgrnam(group).gr_gid
                    except KeyError:
                        self.log(f"Warning: Group '{group}' not found", "warning")
                        return

                # Apply ownership to this directory and all parents up to /opt/immich/md
                base_path = Path("/opt/immich/md")
                current_path = dir_path

                while current_path != current_path.parent:  # Stop at filesystem root
                    # Stop if we've gone above the base path
                    if not current_path.is_relative_to(base_path):
                        break

                    # Change ownership of this directory
                    try:
                        os.chown(current_path, uid, gid)
                        self.log(f"Changed directory ownership: {current_path} → {owner}:{group}", "debug")
                    except PermissionError:
                        self.log(f"Permission denied changing ownership of {current_path}", "debug")
                        break
                    except Exception as e:
                        self.log(f"Failed to change ownership of {current_path}: {e}", "debug")
                        break

                    # Move to parent directory
                    current_path = current_path.parent

        except Exception as e:
            self.log(f"Failed to apply ownership to directories: {e}", "debug")

    def move_file(self,
                  source: Union[str, Path],
                  destination: Union[str, Path],
                  timestamp: Optional[datetime] = None,
                  preserve_if_no_timestamp: bool = True,
                  content_type: str = None) -> bool:
        """
        Move a single file with optional timestamp setting

        Args:
            source: Source file path
            destination: Destination file path
            timestamp: Optional datetime to set on the file
            preserve_if_no_timestamp: If True and no timestamp provided, preserve original
            content_type: Optional content type for tracking

        Returns:
            True if successful, False otherwise
        """
        source = Path(source)
        destination = Path(destination)

        if not source.exists():
            self.log(f"Source file not found: {source}", "error")
            self.stats['failed'] += 1
            return False

        if destination.exists():
            self.log(f"Skipping existing file: {destination.name}", "info")
            self.stats['skipped'] += 1

            # Update database even when skipping to prevent re-downloads
            if self.unified_db and self.batch_context:
                platform = self.batch_context.get('platform')
                source_name = self.batch_context.get('source')
                if platform and source_name:
                    try:
                        # Update database with final path (using existing destination)
                        updated = self.unified_db.update_file_location_by_filename(
                            filename=source.name,
                            platform=platform,
                            source=source_name,
                            final_path=str(destination)
                        )
                        if updated:
                            self.log(f"Updated database for skipped file: {destination.name}", "debug")
                    except Exception as e:
                        self.log(f"Failed to update database for skipped file: {e}", "debug")

            return False

        # Initialize file_hash before conditional block (used later in upsert_file_inventory)
        file_hash = None

        # File hash deduplication check (checks downloads, recycle_bin, and file_inventory)
        if self.unified_db and UnifiedDatabase:
            try:
                self.activity_manager.update_status(f"Checking file hash: {source.name}")
                file_hash = UnifiedDatabase.get_file_hash(str(source))
                self.log(f"[HASH_CHECK] Calculated hash for {source.name}: {file_hash[:16] if file_hash else 'None'}...", "debug")
                if file_hash:
                    # Check if this file hash already exists anywhere (downloads, recycle_bin, file_inventory)
                    is_duplicate = self.unified_db.is_file_hash_downloaded(file_hash)
                    self.log(f"[HASH_CHECK] Hash exists in system: {is_duplicate}", "debug")

                    if is_duplicate:
                        # Get details from downloads table first (most common case)
                        existing = self.unified_db.get_download_by_file_hash(file_hash)

                        if existing:
                            existing_path = existing.get('file_path', 'unknown')
                            existing_filename = existing.get('filename', 'unknown')
                            existing_platform = existing.get('platform', 'unknown')
                            existing_source = existing.get('source', 'unknown')

                            self.log(f"[HASH_CHECK] Existing file: {existing_filename} at {existing_path}", "debug")
                            self.log(f"[HASH_CHECK] Source file: {source.name} at {source}", "debug")
                            self.log(f"[HASH_CHECK] Paths match: {str(source) == existing_path}", "debug")

                            if existing_path and str(source) != existing_path:
                                # Check if existing file actually exists at that path
                                existing_file_exists = Path(existing_path).exists() if existing_path != 'unknown' else False
                                self.log(f"[HASH_CHECK] Existing file exists on disk: {existing_file_exists}", "debug")

                                if existing_file_exists:
                                    # Duplicate found - keep first file, delete new
                                    self.log(
                                        f"Skipping duplicate file (same content): {source.name} "
                                        f"[Already exists: {existing_filename} from {existing_platform}/{existing_source} at {existing_path}]",
                                        "warning"
                                    )
                                    self.stats['duplicates'] += 1

                                    # Delete the duplicate source file to save space
                                    try:
                                        source.unlink()
                                        self.log(f"Deleted duplicate: {source.name}", "debug")
                                    except Exception as e:
                                        self.log(f"Failed to delete duplicate {source.name}: {e}", "warning")

                                    return False
                                else:
                                    # Existing file path in DB but file doesn't exist - allow move and update DB
                                    self.log(f"[HASH_CHECK] Existing record found but file missing at {existing_path}, allowing move to proceed", "debug")
                            else:
                                self.log(f"[HASH_CHECK] Source and existing paths match or no existing path, allowing move", "debug")
                        else:
                            # Hash exists in recycle_bin or file_inventory but not downloads
                            self.log(f"Skipping duplicate file: {source.name} [Hash exists in recycle bin or review queue]", "warning")
                            self.stats['duplicates'] += 1

                            # Delete the duplicate source file
                            try:
                                source.unlink()
                                self.log(f"Deleted duplicate: {source.name}", "debug")
                            except Exception as e:
                                self.log(f"Failed to delete duplicate {source.name}: {e}", "warning")

                            return False
            except Exception as e:
                # Don't fail the move if hash check fails, just log and continue
                self.log(f"File hash check failed for {source.name}: {e}", "debug")

        # Instagram perceptual duplicate detection (visually similar content with overlays)
        if self.batch_context:
            platform = self.batch_context.get('platform', '')
            source_name = self.batch_context.get('source', '')
            content_type = self.batch_context.get('content_type', '')

            # Check if this is an Instagram downloader (instagram, fastdl, imginn, instaloader, toolzu)
            platform_lower = platform.lower()
            is_instagram = any(ig_platform in platform_lower for ig_platform in ['instagram', 'fastdl', 'imginn', 'instaloader', 'toolzu'])

            if is_instagram:
                self.activity_manager.update_status(f"Checking perceptual hash: {source.name}")
                perceptual_result = self._check_perceptual_duplicate(str(source), platform, source_name, content_type)
                if perceptual_result == "skip":
                    # This file is a lower quality perceptual duplicate - skip it
                    self.log(f"Skipping perceptual duplicate (has overlays or lower quality): {source.name}", "info")
                    return False
                # If perceptual_result is None or file_path, continue processing

        # Track if file is being moved to review queue
        moved_to_review = False
        original_intended_path = str(destination)
        queued_for_repost_check = False  # Track if we added this file to repost queue

        # Instagram story repost detection (only if enabled in settings)
        if self._is_instagram_story(source) and self.batch_context:
            source_username = self.batch_context.get('source', '')
            platform = self.batch_context.get('platform', '')

            # Check if this is an Instagram downloader (instagram, fastdl, imginn, instaloader, toolzu)
            platform_lower = platform.lower()
            is_instagram = any(ig_platform in platform_lower for ig_platform in ['instagram', 'fastdl', 'imginn', 'instaloader', 'toolzu'])

            if is_instagram:
                self.activity_manager.update_status(f"Checking repost detection: {source.name}")
                result = self._check_repost_and_replace(str(source), source_username, str(destination))
                if result == "queued":
                    queued_for_repost_check = True
                elif result:
                    source = Path(result)
                    self.log(f"Replaced repost with original: {source.name}", "info")

                    # Update batch_context to reflect the ORIGINAL source
                    # This ensures database records it correctly as from the original user
                    replacement_filename = source.name.lower()

                    # Extract original username from filename (e.g., globalgiftfoundation_20251109_...)
                    import re
                    match = re.match(r'^([a-z0-9._]+)_\d{8}', replacement_filename)
                    if match:
                        original_source = match.group(1)
                        self.batch_context['source'] = original_source

                        # Update content_type based on filename
                        if 'story' in replacement_filename:
                            self.batch_context['content_type'] = 'story'
                        elif 'post' in replacement_filename:
                            self.batch_context['content_type'] = 'post'
                        elif 'reel' in replacement_filename:
                            self.batch_context['content_type'] = 'reel'

                        self.log(f"Updated batch_context: source={original_source}", "debug")

        # Face recognition check
        if self.face_module:
            # Check if video face recognition is enabled
            video_settings = self._get_video_face_recognition_settings()

            # Process videos with multi-frame face recognition (if enabled)
            if self._is_video_file(source):
                if video_settings['enable_video_recognition']:
                    try:
                        # Get source username from batch context for source-based tolerance
                        source_username = self.batch_context.get('source') if self.batch_context else None
                        tolerance = self._get_face_recognition_tolerance(is_video=True, source=source_username)
                        frame_positions = video_settings['frame_positions']

                        self.log(f"Checking video with {len(frame_positions)} frames (tolerance: {tolerance}): {source.name}", "info")
                        self.activity_manager.update_status(f"Checking facial recognition: {source.name}")

                        # Use multi-frame checking for videos
                        result = self.face_module.check_video_multiframe(
                            str(source),
                            tolerance=tolerance,
                            positions=frame_positions
                        )

                        # Store result for event emission
                        self._last_face_result = result

                        # Log face recognition result to database
                        # Use best_candidate for matched_person when no match found (to show who was closest)
                        if self.unified_db:
                            try:
                                self.unified_db.log_face_recognition_scan(
                                    file_path=str(destination),
                                    has_match=result['has_match'],
                                    matched_person=result.get('person_name') or result.get('best_candidate'),
                                    confidence=result.get('confidence'),
                                    face_count=result.get('face_count', 0),
                                    scan_type='auto'
                                )
                            except Exception as log_err:
                                self.log(f"Failed to log face recognition result: {log_err}", "debug")

                        if not result['has_match']:
                            # No face match - move to review queue
                            review_path = self._get_review_path()
                            base_path = Path("/opt/immich/md")
                            if destination.is_relative_to(base_path):
                                relative_path = destination.relative_to(base_path)
                                review_dest = Path(review_path) / relative_path
                            else:
                                review_dest = Path(review_path) / source.name

                            best_info = f" (best: {result.get('best_candidate')} at {result.get('confidence', 0):.1%})" if result.get('best_candidate') else ""
                            self.log(f"No face match in video {source.name} (checked {result['frames_checked']} frames){best_info} - moving to review queue", "info")
                            destination = review_dest
                            moved_to_review = True
                            self.stats['review_queue'] += 1
                        else:
                            # Face matched - continue to original destination
                            self.log(f"Face match in video: {result['person_name']} ({result['confidence']:.1%}, frame {result['best_frame_index']+1}/{result['frames_checked']}) - proceeding to final destination", "info")

                        # Track that face recognition ran (batch loop handles pacing)
                        self._last_move_had_face_recognition = True

                    except Exception as e:
                        self.log(f"Video face recognition failed for {source.name}: {e} - moving to review queue", "warning")
                        # Re-check file existence - if file was deleted, skip the move
                        if not source.exists():
                            self.log(f"Source file no longer exists after video face check failure: {source.name}", "warning")
                            self.stats['failed'] += 1
                            return False
                        # On error, move to review queue as safety measure
                        review_path = self._get_review_path()
                        base_path = Path("/opt/immich/md")
                        if destination.is_relative_to(base_path):
                            relative_path = destination.relative_to(base_path)
                            review_dest = Path(review_path) / relative_path
                        else:
                            review_dest = Path(review_path) / source.name
                        destination = review_dest
                        moved_to_review = True
                        self.stats['review_queue'] += 1
                else:
                    # Video face recognition disabled - skip and move to review queue
                    review_path = self._get_review_path()
                    base_path = Path("/opt/immich/md")
                    if destination.is_relative_to(base_path):
                        relative_path = destination.relative_to(base_path)
                        review_dest = Path(review_path) / relative_path
                    else:
                        review_dest = Path(review_path) / source.name

                    self.log(f"Video face recognition disabled - moving {source.name} to review queue", "debug")
                    destination = review_dest
                    moved_to_review = True
                    self.stats['review_queue'] += 1

            # Process images with face recognition
            elif self._is_image_file(source):
                try:
                    is_video = False  # Only processing images now
                    # Get source username from batch context for source-based tolerance
                    source_username = self.batch_context.get('source') if self.batch_context else None
                    tolerance = self._get_face_recognition_tolerance(is_video=False, source=source_username)
                    self.log(f"Checking image (tolerance: {tolerance}): {source.name}", "debug")
                    result = self.face_module.check_image(str(source), tolerance=tolerance, is_video=is_video)

                    # Store result for event emission
                    self._last_face_result = result

                    # Log face recognition result to database
                    # Use best_candidate for matched_person when no match found (to show who was closest)
                    if self.unified_db:
                        try:
                            self.unified_db.log_face_recognition_scan(
                                file_path=str(destination),  # Use final destination path
                                has_match=result['has_match'],
                                matched_person=result.get('person_name') or result.get('best_candidate'),
                                confidence=result.get('confidence'),
                                face_count=result.get('face_count', 0),
                                scan_type='auto'
                            )
                        except Exception as log_err:
                            self.log(f"Failed to log face recognition result: {log_err}", "debug")

                    if not result['has_match']:
                        # No face match - move to review queue instead
                        # Maintain folder structure in review queue
                        review_path = self._get_review_path()
                        base_path = Path("/opt/immich/md")
                        if destination.is_relative_to(base_path):
                            # Get relative path from base
                            relative_path = destination.relative_to(base_path)
                            # Recreate under review directory
                            review_dest = Path(review_path) / relative_path
                        else:
                            # Fallback to flat structure if not under base path
                            review_dest = Path(review_path) / source.name

                        file_type = "video" if is_video else "image"
                        best_info = f" (best: {result.get('best_candidate')} at {result.get('confidence', 0):.1%})" if result.get('best_candidate') else ""
                        self.log(f"No face match for {file_type} {source.name}{best_info} - moving to review queue at {review_dest}", "info")

                        # Update destination to review path
                        destination = review_dest
                        moved_to_review = True
                        self.stats['review_queue'] += 1
                    else:
                        # Face matched - continue to original destination
                        file_type = "video" if is_video else "image"
                        self.log(f"Face match in {file_type}: {result['person_name']} ({result['confidence']:.1%}) - proceeding to final destination", "debug")
                except Exception as e:
                    # Don't fail the move if face check fails, just log and continue
                    self.log(f"Face recognition check failed for {source.name}: {e}", "debug")
                    # Re-check file existence - if file was deleted, skip the move
                    if not source.exists():
                        self.log(f"Source file no longer exists after face check failure: {source.name}", "warning")
                        self.stats['failed'] += 1
                        return False
                finally:
                    # Track that face recognition ran
                    self._last_move_had_face_recognition = True

        try:
            # Ensure destination directory exists
            destination.parent.mkdir(parents=True, exist_ok=True)

            # Apply ownership to created directories
            self._apply_ownership_to_path(destination.parent)
            
            if preserve_if_no_timestamp and not timestamp:
                # Try to extract EXIF date from source file first
                # This fixes issues where filesystem mtime is wrong but EXIF has correct date
                exif_date = None
                if self._is_image_file(source):
                    exif_date = _extract_exif_date(source)
                    if exif_date:
                        self.log(f"Extracted EXIF date for {source.name}: {exif_date.strftime('%Y-%m-%d %H:%M:%S')}", "debug")

                # Copy file with metadata preserved (single pass)
                shutil.copy2(str(source), str(destination))

                if exif_date:
                    # Override timestamps with EXIF date (more accurate than filesystem)
                    if DATE_UTILS_AVAILABLE:
                        DateHandler.update_file_timestamps(destination, exif_date)
                        self.log(f"Set timestamps from EXIF: {exif_date.strftime('%Y-%m-%d %H:%M:%S')}", "debug")
                    else:
                        timestamp_unix = exif_date.timestamp()
                        os.utime(destination, (timestamp_unix, timestamp_unix))
                        self.log(f"Set filesystem timestamp from EXIF: {exif_date.strftime('%Y-%m-%d %H:%M:%S')}", "debug")
            else:
                # Copy file content
                shutil.copy(str(source), str(destination))

                # Set timestamp if provided - use DateHandler for comprehensive update
                # This sets EXIF metadata (including MetadataDate for Immich) AND filesystem times
                if timestamp:
                    if DATE_UTILS_AVAILABLE:
                        # Use centralized date handler for EXIF + filesystem timestamps
                        DateHandler.update_file_timestamps(destination, timestamp)
                        self.log(f"Set all timestamps to {timestamp.strftime('%Y-%m-%d %H:%M:%S')}", "debug")
                    else:
                        # Fallback to filesystem-only timestamps
                        timestamp_unix = timestamp.timestamp()
                        os.utime(destination, (timestamp_unix, timestamp_unix))
                        self.log(f"Set filesystem timestamp to {timestamp.strftime('%Y-%m-%d %H:%M:%S')}", "debug")

            self.log(f"Moved: {source.name} → {destination.name}", "info")
            self.stats['moved'] += 1
            self.activity_manager.update_status(f"Moving images: {source.name}")

            # Pre-generate thumbnail in background (non-blocking)
            self._bg_executor.submit(self._generate_thumbnail_cache, destination, file_hash)

            # Apply file ownership if configured
            self._apply_ownership(destination)

            # Update database with final file location and hash
            if self.unified_db and self.batch_context:
                platform = self.batch_context.get('platform')
                source_name = self.batch_context.get('source')
                content_type_ctx = self.batch_context.get('content_type')

                # For tagged content, extract actual poster from filename (e.g., "rtlliving_20251124_..." -> "rtlliving")
                if content_type_ctx == 'tagged' and source.name:
                    import re
                    # Use date pattern to correctly extract usernames that may contain underscores
                    # Pattern: username_YYYYMMDD_...
                    date_pattern = re.match(r'^(.+?)_(\d{8})_', source.name)
                    if date_pattern:
                        extracted_source = date_pattern.group(1).lower()
                        # Validate: Instagram usernames are 1-30 chars, alphanumeric + underscore + period
                        if extracted_source and re.match(r'^[a-z0-9_.]{1,30}$', extracted_source):
                            if extracted_source != source_name:
                                self.log(f"Tagged content: using poster @{extracted_source} instead of @{source_name}", "debug")
                                source_name = extracted_source
                        else:
                            self.log(f"Tagged content: extracted '{extracted_source}' doesn't look like valid username, keeping @{source_name}", "debug")

                if platform and source_name:
                    try:
                        # Update database with final path and hash
                        updated = self.unified_db.update_file_location_by_filename(
                            filename=source.name,
                            platform=platform,
                            source=source_name,
                            final_path=str(destination)
                        )

                        # Batch all path updates in a single transaction
                        with self.unified_db.get_connection(for_write=True) as conn:
                            cursor = conn.cursor()

                            # If moved to review queue, add intended_path to metadata
                            if updated and moved_to_review:
                                import json
                                cursor.execute('''
                                    SELECT metadata FROM downloads
                                    WHERE filename = ? AND platform = ? AND source = ?
                                ''', (source.name, platform, source_name))
                                row = cursor.fetchone()

                                if row:
                                    metadata = json.loads(row['metadata']) if row['metadata'] else {}
                                    metadata['intended_path'] = original_intended_path
                                    cursor.execute('''
                                        UPDATE downloads
                                        SET metadata = ?
                                        WHERE filename = ? AND platform = ? AND source = ?
                                    ''', (json.dumps(metadata), source.name, platform, source_name))
                                    self.log(f"Saved intended destination to metadata: {original_intended_path}", "debug")

                            # Update perceptual hash path
                            try:
                                cursor.execute('''
                                    UPDATE instagram_perceptual_hashes
                                    SET file_path = ?
                                    WHERE filename = ? AND platform = ? AND source = ?
                                ''', (str(destination), source.name, platform, source_name))
                                if cursor.rowcount > 0:
                                    self.log(f"Updated perceptual hash path: {destination}", "debug")
                            except Exception as e:
                                self.log(f"Failed to update perceptual hash path: {e}", "debug")

                            # Update face recognition scans path
                            try:
                                cursor.execute('''
                                    UPDATE face_recognition_scans
                                    SET file_path = ?
                                    WHERE file_path = ?
                                ''', (str(destination), original_intended_path))
                                if cursor.rowcount > 0:
                                    self.log(f"Updated face recognition scan path: {original_intended_path} -> {destination}", "debug")
                            except Exception as e:
                                self.log(f"Failed to update face recognition scan path: {e}", "debug")

                        if updated:
                            self.log(f"Updated database with final location: {destination}", "debug")
                        else:
                            self.log(f"No database record found to update for {source.name}", "debug")

                    except Exception as e:
                        self.log(f"Failed to update database location: {e}", "debug")

                    # Record in file_inventory in background (dimensions + discovery queue)
                    # This avoids blocking the move pipeline on ffprobe/PIL calls
                    self._bg_executor.submit(
                        self._record_file_inventory_bg,
                        destination, source.name, platform, source_name,
                        moved_to_review, file_hash, timestamp
                    )

            # Track for batch notification if in batch mode
            # Auto-detect content type from path if not provided
            if not content_type and self.batch_context:
                # Prefer the batch-level content_type (set by caller who knows what's being downloaded)
                # This prevents path-based inference from overriding e.g. 'media' with 'post'
                # just because the temp dir contains "posts/"
                batch_ct = self.batch_context.get('content_type')
                if batch_ct:
                    # Normalize plural forms to singular for proper notification grammar
                    _SINGULAR = {'posts': 'post', 'stories': 'story', 'reels': 'reel',
                                 'videos': 'video', 'images': 'image', 'items': 'item'}
                    content_type = _SINGULAR.get(batch_ct, batch_ct)
                else:
                    # No batch content_type set, try to infer from source path
                    path_str = str(source).lower()
                    if 'story' in path_str or 'stories' in path_str:
                        content_type = 'story'
                    elif 'reel' in path_str:
                        content_type = 'reel'
                    elif 'post' in path_str:
                        content_type = 'post'
                    elif 'video' in path_str:
                        content_type = 'video'
                    elif 'image' in path_str or 'photo' in path_str:
                        content_type = 'image'

            # Track with full path for image attachment in notifications
            self.track_moved_file(str(destination), content_type=content_type, is_review=moved_to_review)

            # Update repost queue with actual final destination (in case it changed due to review queue)
            if queued_for_repost_check and self.repost_queue:
                # Find the most recent queue entry for this file and update with actual final destination
                for entry in reversed(self.repost_queue):
                    if Path(entry['file_path']).name == destination.name:
                        # Update with actual final path (might be review queue)
                        entry['file_path'] = str(destination)
                        self.log(f"Updated repost queue entry with final destination: {destination.name}", "debug")
                        break

            # Emit WebSocket event for real-time scraping monitor
            if self.event_emitter and self.current_session:
                try:
                    # Determine media type
                    media_type = 'video' if self._is_video_file(destination) else 'image'

                    # Determine destination type (media/review/recycle)
                    dest_str = str(destination).lower()
                    if moved_to_review or '/review' in dest_str:
                        destination_type = 'review'
                    elif '/recycle' in dest_str:
                        destination_type = 'recycle'
                    else:
                        destination_type = 'media'

                    # Generate thumbnail URL
                    import urllib.parse
                    thumbnail_url = f"/api/files/thumbnail?path={urllib.parse.quote(str(destination))}"

                    # Get face match info if available
                    face_match = {'matched': False}
                    if hasattr(self, '_last_face_result') and self._last_face_result:
                        if self._last_face_result.get('has_match'):
                            face_match = {
                                'matched': True,
                                'person_name': self._last_face_result.get('person_name'),
                                'confidence': self._last_face_result.get('confidence')
                            }

                    # Emit file_moved event
                    self.event_emitter.emit_file_moved(
                        session_id=self.current_session.get('session_id', 'unknown'),
                        platform=self.current_session.get('platform', 'unknown'),
                        account=self.current_session.get('account', 'unknown'),
                        filename=destination.name,
                        media_type=media_type,
                        destination_type=destination_type,
                        destination_path=str(destination),
                        thumbnail_url=thumbnail_url,
                        face_match=face_match
                    )
                except Exception as emit_err:
                    self.log(f"Failed to emit file_moved event: {emit_err}", "debug")

            return True
            
        except Exception as e:
            self.log(f"Failed to move {source.name}: {e}", "error")
            self.stats['failed'] += 1
            return False
    
    def move_files_batch(self, 
                        source_dir: Union[str, Path],
                        dest_dir: Union[str, Path],
                        file_timestamps: Dict[str, Optional[datetime]] = None,
                        extensions: List[str] = None,
                        preserve_if_no_timestamp: bool = True) -> Dict:
        """
        Move multiple files from source to destination with timestamp management
        
        Args:
            source_dir: Source directory
            dest_dir: Destination directory
            file_timestamps: Dict mapping filename to datetime (or None to preserve)
            extensions: List of file extensions to move (e.g., ['.jpg', '.mp4'])
            preserve_if_no_timestamp: If True, preserve timestamp for files not in dict
        
        Returns:
            Statistics dictionary
        """
        source_dir = Path(source_dir)
        dest_dir = Path(dest_dir)
        file_timestamps = file_timestamps or {}
        
        # Skip if source and destination are the same
        if source_dir.resolve() == dest_dir.resolve():
            self.log("Source and destination are the same, skipping move", "info")
            return {"moved": 0, "failed": 0, "errors": []}
        
        if not source_dir.exists():
            self.log(f"Source directory not found: {source_dir}", "error")
            return self.stats
        
        # Find all files to move
        files_to_move = []
        for pattern in ['*'] if not extensions else [f'*{ext}' for ext in extensions]:
            files_to_move.extend(source_dir.rglob(pattern))
        
        # Filter to only files (not directories)
        files_to_move = [f for f in files_to_move if f.is_file()]
        
        # Filter by extension if specified
        if extensions:
            files_to_move = [f for f in files_to_move if f.suffix.lower() in extensions]
        
        self.log(f"Found {len(files_to_move)} files to move", "info")
        total_files = len(files_to_move)

        for file_idx, source_file in enumerate(files_to_move):
            # Determine destination path
            relative_path = source_file.relative_to(source_dir)
            dest_file = dest_dir / relative_path

            # Get timestamp for this file
            timestamp = file_timestamps.get(source_file.name)

            # Update file-level progress
            self.activity_manager.update_status(
                f"Processing {source_file.name}",
                progress_current=file_idx + 1,
                progress_total=total_files
            )

            # Move the file
            self._last_move_had_face_recognition = False
            self.move_file(
                source_file,
                dest_file,
                timestamp=timestamp,
                preserve_if_no_timestamp=preserve_if_no_timestamp
            )

            # Periodic GC after face recognition batches to free ML tensors
            if self._last_move_had_face_recognition and (file_idx + 1) % 10 == 0:
                gc.collect()

        return self.stats
    
    def move_with_metadata(self, file_info: Dict) -> bool:
        """
        Move a file using metadata dictionary
        
        Args:
            file_info: Dictionary containing:
                - source: Source file path
                - destination: Destination file path  
                - timestamp: Optional datetime object
                - preserve_original: If True and no timestamp, preserve original
        
        Returns:
            True if successful
        """
        source = file_info.get('source')
        destination = file_info.get('destination')
        timestamp = file_info.get('timestamp')
        preserve = file_info.get('preserve_original', True)
        
        if not source or not destination:
            self.log("Missing source or destination in file_info", "error")
            return False
        
        return self.move_file(source, destination, timestamp, preserve)
    
    def get_stats(self) -> Dict:
        """Get movement statistics"""
        return self.stats.copy()
    
    def reset_stats(self):
        """Reset statistics"""
        self.stats = {
            'moved': 0,
            'skipped': 0,
            'failed': 0,
            'duplicates': 0
        }

    def start_batch(self, platform: str, source: str = None, content_type: str = None, search_term: str = None):
        """
        Start a batch move operation for notifications

        Args:
            platform: Platform name (instagram, tiktok, forum, etc.)
            source: Source/username
            content_type: Type of content (post, story, reel, thread, etc.)
            search_term: Optional search term (for forum searches)
        """
        self.batch_context = {
            'platform': platform,
            'source': source,
            'content_type': content_type,
            'search_term': search_term
        }
        self.moved_files = []
        # Clear per-batch settings caches so DB changes take effect between batches
        self._face_recognition_settings_cache = None
        self._video_face_settings_cache = None
        self._review_path = None
        self.log(f"Started batch move for {platform}/{source or 'unknown'}/{content_type or 'items'}", "debug")

    def end_batch(self):
        """
        End batch move operation and send notification

        Returns:
            Number of files moved in this batch
        """
        if not self.batch_context:
            return 0

        # Wait for background tasks (thumbnails, inventory) to complete
        # before sending notifications
        self._bg_executor.shutdown(wait=True)
        from concurrent.futures import ThreadPoolExecutor
        self._bg_executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix='move_bg')

        moved_count = len(self.moved_files)
        review_count = len(self.review_queue_files)

        platform = self.batch_context.get('platform', 'unknown')
        source = self.batch_context.get('source')
        content_type = self.batch_context.get('content_type') or 'item'  # Handle None explicitly
        search_term = self.batch_context.get('search_term')

        # Send batch notification for matched files (moved to final destination)
        if self.notifier and moved_count > 0:
            try:
                # Prepare download list for notification
                # Use individual file content types if available, otherwise fall back to batch content type
                downloads = []
                for file_info in self.moved_files:
                    downloads.append({
                        'source': source,
                        'content_type': file_info.get('content_type') or content_type,
                        'filename': file_info.get('filename'),
                        'file_path': file_info.get('file_path')  # Full path for image attachment
                    })

                # Send batch notification
                success = self.notifier.notify_batch_download(
                    platform=platform,
                    downloads=downloads,
                    search_term=search_term
                )

                if success:
                    self.log(f"Sent notification: {moved_count} {content_type}(s) from {source or platform}", "info")
                else:
                    self.log("Failed to send notification", "warning")

            except Exception as e:
                self.log(f"Failed to send batch notification: {e}", "error")

        # Send separate notification for review queue items (no face match)
        if self.notifier and review_count > 0:
            try:
                # Prepare review queue list for notification
                downloads = []
                for file_info in self.review_queue_files:
                    downloads.append({
                        'source': source,
                        'content_type': file_info.get('content_type') or content_type,
                        'filename': file_info.get('filename'),
                        'file_path': file_info.get('file_path')  # Full path for image attachment
                    })

                # Send review queue notification with lower priority
                success = self.notifier.notify_batch_download(
                    platform=platform,
                    downloads=downloads,
                    search_term=search_term,
                    is_review_queue=True  # Flag to indicate this is review queue notification
                )

                if success:
                    self.log(f"Sent review queue notification: {review_count} {content_type}(s) from {source or platform}", "info")
                else:
                    self.log("Review queue notification not sent (may be disabled in settings)", "debug")

            except Exception as e:
                self.log(f"Failed to send review queue notification: {e}", "error")

        # Process repost queue (download originals, find matches, replace)
        # This happens after all files are moved and notifications sent
        if platform in ['instagram', 'fastdl', 'imginn', 'toolzu', 'instaloader']:
            self.process_repost_queue()

        # Call download complete callback to trigger UI updates
        total_count = moved_count + review_count
        if total_count > 0 and self.on_download_complete:
            try:
                self.on_download_complete(platform, source, total_count)
            except Exception as e:
                self.log(f"Error in download complete callback: {e}", "error")

        # Clear batch context
        self.batch_context = None
        self.moved_files = []
        self.review_queue_files = []

        return moved_count + review_count

    def _is_instagram_story(self, file_path: Path) -> bool:
        """Check if file is an Instagram story based on path"""
        path_str = str(file_path).lower()
        return 'story' in path_str or 'stories' in path_str

    def _is_repost_detection_enabled(self) -> bool:
        """Check if repost detection is enabled in settings"""
        try:
            if not self.unified_db:
                return False

            with self.unified_db.get_connection() as conn:
                cursor = conn.cursor()
                cursor.execute("SELECT value FROM settings WHERE key = 'repost_detection'")
                result = cursor.fetchone()
                if result:
                    import json
                    settings = json.loads(result[0])
                    return settings.get('enabled', False)  # Default: DISABLED
        except (sqlite3.Error, json.JSONDecodeError, KeyError) as e:
            self.log(f"Error checking repost detection setting: {e}", "debug")
        return False  # Default: DISABLED

    def _check_repost_and_replace(self, file_path: str, source_username: str, destination_path: str = None) -> Optional[str]:
        """
        Queue file for repost detection (processed after batch completes)

        Args:
            file_path: Current temp file path (for OCR scanning)
            source_username: Username who posted this
            destination_path: Final permanent storage path (used in queue)

        Returns None (queueing only, no immediate replacement)
        """
        # Check if feature is enabled
        if not self._is_repost_detection_enabled():
            return None  # Feature disabled - skip

        try:
            from modules.instagram_repost_detector import InstagramRepostDetector

            # Initialize detector for OCR check only
            detector = InstagramRepostDetector(
                unified_db=self.unified_db,
                log_callback=lambda msg, lvl: self.log(msg, lvl)
            )

            # Quick OCR check to see if it's a repost (using temp file)
            original_username = detector._extract_username_from_repost(file_path)

            if original_username and original_username.lower() != source_username.lower():
                # Queue for processing after moves complete
                # IMPORTANT: Store destination_path (permanent location) NOT file_path (temp)
                path_to_queue = destination_path if destination_path else file_path

                self.repost_queue.append({
                    'file_path': path_to_queue,
                    'source_username': source_username,
                    'detected_username': original_username
                })
                self.log(f"Queued repost detection: {Path(path_to_queue).name} → @{original_username}", "info")
                return "queued"  # Signal that file was queued

            return None  # No repost detected, no immediate replacement

        except Exception as e:
            self.log(f"Repost queue check failed: {e}", "debug")
            return None

    def process_repost_queue(self):
        """Process all queued reposts (called after batch completes)"""
        if not self.repost_queue:
            return

        self.log(f"Processing {len(self.repost_queue)} queued reposts...", "info")

        try:
            from modules.instagram_repost_detector import InstagramRepostDetector

            detector = InstagramRepostDetector(
                unified_db=self.unified_db,
                log_callback=lambda msg, lvl: self.log(msg, lvl)
            )

            # Group by detected username to avoid re-downloading same user
            users_to_process = {}
            for item in self.repost_queue:
                username = item['detected_username']
                if username not in users_to_process:
                    users_to_process[username] = []
                users_to_process[username].append(item)

            # Process each user's reposts
            processed = 0
            replaced = 0

            for username, items in users_to_process.items():
                self.log(f"Processing {len(items)} repost(s) from @{username}", "info")

                for item in items:
                    try:
                        # Process the repost (download if needed, find match, replace)
                        replacement = detector.check_and_replace_repost(
                            item['file_path'],
                            item['source_username']
                        )

                        if replacement:
                            self.log(f"✓ Replaced repost with original from @{username}", "success")
                            replaced += 1

                        processed += 1

                    except Exception as e:
                        self.log(f"Failed to process repost for @{username}: {e}", "error")

            self.log(f"Repost queue processed: {processed} checked, {replaced} replaced", "info")

        except Exception as e:
            self.log(f"Repost queue processing failed: {e}", "error")

        finally:
            # Clear the queue
            self.repost_queue = []

    def _check_perceptual_duplicate(self, file_path: str, platform: str, source: str, content_type: str = None) -> Optional[str]:
        """
        Check if file is a perceptual duplicate (visually similar with overlays) for Instagram

        Returns:
            - None if not a duplicate or feature disabled
            - "skip" if this file should be skipped (lower quality duplicate)
            - file_path if this file should be kept
        """
        try:
            from modules.instagram_perceptual_duplicate_detector import InstagramPerceptualDuplicateDetector

            # Initialize detector (uses its own universal logger now)
            detector = InstagramPerceptualDuplicateDetector(
                unified_db=self.unified_db
            )

            # Run detection
            result = detector.check_and_handle_duplicate(file_path, platform, source, content_type)

            return result

        except Exception as e:
            self.log(f"Perceptual duplicate detection failed: {e}", "error")
            import traceback
            self.log(traceback.format_exc(), "error")
            return None

    def track_moved_file(self, file_path: str, metadata: Dict[str, Any] = None, content_type: str = None, is_review: bool = False):
        """
        Track a moved file for batch notification

        Args:
            file_path: Full path to the moved file (for image attachment)
            metadata: Optional metadata dictionary
            content_type: Optional specific content type for this file (overrides batch default)
            is_review: True if file was moved to review queue (no face match)
        """
        if self.batch_context:
            file_info = {
                'file_path': file_path,
                'filename': Path(file_path).name,  # Extract filename for logging
                'metadata': metadata or {},
                'content_type': content_type  # Track individual file's content type
            }

            if is_review:
                self.review_queue_files.append(file_info)
            else:
                self.moved_files.append(file_info)


def move_files_simple(source_dir: Union[str, Path],
                     dest_dir: Union[str, Path],
                     extensions: List[str] = None,
                     file_timestamps: Dict[str, datetime] = None,
                     log_callback=None) -> Dict:
    """
    Simple function interface for moving files
    
    Args:
        source_dir: Source directory
        dest_dir: Destination directory
        extensions: List of file extensions to move
        file_timestamps: Optional dict mapping filenames to timestamps
        log_callback: Optional logging callback
    
    Returns:
        Statistics dict
    """
    manager = MoveManager(log_callback=log_callback)
    return manager.move_files_batch(
        source_dir,
        dest_dir,
        file_timestamps=file_timestamps,
        extensions=extensions
    )


if __name__ == "__main__":
    # Test the module
    import tempfile
    
    print("Testing MoveManager...")
    
    # Create test environment
    with tempfile.TemporaryDirectory() as tmpdir:
        test_dir = Path(tmpdir)
        src_dir = test_dir / "source"
        dst_dir = test_dir / "destination"
        src_dir.mkdir()
        
        # Create test files
        test_file1 = src_dir / "test1.jpg"
        test_file2 = src_dir / "test2.mp4"
        test_file1.write_text("content1")
        test_file2.write_text("content2")
        
        # Test moving with timestamps
        manager = MoveManager()
        
        # Move with specific timestamp
        specific_time = datetime(2025, 8, 26, 17, 2, 24)
        manager.move_file(test_file1, dst_dir / "test1.jpg", timestamp=specific_time)
        
        # Move preserving original
        manager.move_file(test_file2, dst_dir / "test2.mp4", timestamp=None)
        
        # Check results
        dst_file1 = dst_dir / "test1.jpg"
        if dst_file1.exists():
            mtime = datetime.fromtimestamp(os.stat(dst_file1).st_mtime)
            print(f"✅ File 1 moved with timestamp: {mtime}")
        
        print(f"Stats: {manager.get_stats()}")