#!/usr/bin/env python3
"""
Instagram Story Repost Detector Module

Detects when Instagram stories are reposts/screenshots of other users' content,
then replaces low-quality reposts with high-quality originals from the source.

Features:
- OCR-based repost detection (@username extraction)
- ImgInn for downloading both stories and posts
- Perceptual hash matching for content identification
- Smart account filtering (monitored vs non-monitored)
- Automatic cleanup of temporary downloads
- Database tracking of all replacements
"""

import os
import re
import shutil
from pathlib import Path
from typing import Optional, Dict, List, Tuple
from datetime import datetime, timedelta
from modules.base_module import LoggingMixin
from modules.universal_logger import get_logger

# Module-level logger for import-time messages
_module_logger = get_logger('RepostDetector')

# Optional imports - fail gracefully if not available
from PIL import Image  # Always needed

# OCR disabled — not currently needed
EASYOCR_AVAILABLE = False
TESSERACT_AVAILABLE = False

try:
    import cv2
    import numpy as np
    CV2_AVAILABLE = True
except ImportError:
    CV2_AVAILABLE = False
    _module_logger.warning("opencv-python not available - video processing disabled", module='RepostDetector')

try:
    import imagehash
    IMAGEHASH_AVAILABLE = True
except ImportError:
    IMAGEHASH_AVAILABLE = False
    _module_logger.warning("imagehash not available - perceptual hashing disabled", module='RepostDetector')


class InstagramRepostDetector(LoggingMixin):
    """
    Detects and replaces Instagram story reposts with original content
    """

    def __init__(self, unified_db, log_callback=None):
        """
        Initialize the detector

        Args:
            unified_db: UnifiedDatabase instance
            log_callback: Optional logging callback function(message, level)
        """
        # Initialize logging via mixin
        self._init_logger('RepostDetector', log_callback, default_module='RepostDetector')

        self.db = unified_db
        self.temp_download_path = Path("/tmp/repost_detection")
        self.last_original_username = None
        self.easyocr_reader = None

        # Ensure temp directory exists
        self.temp_download_path.mkdir(parents=True, exist_ok=True)

        # Initialize EasyOCR reader (lazy loading - only when needed)
        if EASYOCR_AVAILABLE:
            try:
                # Suppress PyTorch pin_memory warning (we're using CPU anyway)
                import warnings
                warnings.filterwarnings('ignore', category=UserWarning, module='torch.utils.data.dataloader')

                self.easyocr_reader = easyocr.Reader(['en'], gpu=False, verbose=False)
                self.log("EasyOCR initialized for text detection", "info")
            except Exception as e:
                self.log(f"Failed to initialize EasyOCR: {e}", "warning")
                self.easyocr_reader = None

        # Check dependencies
        self._check_dependencies()

    def _check_dependencies(self):
        """Check if all required dependencies are available"""
        missing = []

        if not TESSERACT_AVAILABLE:
            missing.append("pytesseract/PIL (pip3 install pytesseract pillow)")
        if not CV2_AVAILABLE:
            missing.append("opencv-python (pip3 install opencv-python)")
        if not IMAGEHASH_AVAILABLE:
            missing.append("imagehash (pip3 install imagehash)")

        if missing:
            self.log(f"Missing dependencies: {', '.join(missing)}", "warning")
            self.log("Repost detection will be disabled until dependencies are installed", "warning")

    def check_and_replace_repost(self, file_path: str, source_username: str) -> Optional[str]:
        """
        Check if story is a repost and replace with original

        Args:
            file_path: Path to potential repost file
            source_username: Username who posted this story (e.g., evalongoria)

        Returns:
            Path to replacement file if found, None otherwise
        """
        # Check dependencies
        if not all([TESSERACT_AVAILABLE, CV2_AVAILABLE, IMAGEHASH_AVAILABLE]):
            self.log("Cannot process - missing dependencies", "debug")
            return None

        file_path = Path(file_path)
        if not file_path.exists():
            self.log(f"File not found: {file_path}", "error")
            return None

        self.log(f"Checking for repost: {file_path.name}", "info")

        # Step 1: OCR to extract original @username
        original_username = self._extract_username_from_repost(str(file_path))
        if not original_username:
            self.log(f"No @username detected - not a repost", "debug")
            return None

        # Check if user is reposting their own content
        if original_username.lower() == source_username.lower():
            self.log(f"@{source_username} is reposting their own content - skipping", "debug")
            return None

        self.log(f"Detected repost from @{original_username} in @{source_username}'s story", "info")
        self.last_original_username = original_username

        # Step 2: Check if original user is monitored
        is_monitored = self._is_monitored_account(original_username)

        # Step 3: Always save repost sources permanently (for face recognition + quality)
        # Even non-monitored accounts get saved - they were discovered via reposts
        download_path = Path("/opt/immich/md/instagram") / original_username
        add_to_database = True

        if is_monitored:
            self.log(f"@{original_username} is monitored - checking existing content", "info")
        else:
            self.log(f"@{original_username} NOT monitored - but saving permanently (discovered via repost)", "info")

        # Step 4: Check if we already fetched this user's content today
        if not self._already_fetched_today(original_username):
            # Step 5: Download stories + recent posts
            self.log(f"Downloading content from @{original_username} via ImgInn...", "info")
            success = self._download_content_via_imginn(
                username=original_username,
                destination=download_path,
                add_to_database=add_to_database
            )

            if not success:
                self.log(f"Failed to download content from @{original_username}", "error")
                return None
        else:
            self.log(f"Content from @{original_username} already fetched today - using cache", "info")

        # Step 6: Find matching original via perceptual hash
        original_file = self._find_matching_original(
            repost_path=str(file_path),
            search_dir=download_path
        )

        if not original_file:
            self.log(f"No matching original found for {file_path.name}", "warning")
            # Keep all downloaded files - they'll be processed by move manager (face recognition, etc.)
            self.log(f"Keeping all downloaded content from @{original_username} for processing", "info")
            return None

        # Step 7: Replace repost with original
        replacement = self._replace_repost_with_original(
            repost_path=str(file_path),
            original_path=original_file
        )

        # All files are kept permanently - move manager will process them
        self.log(f"All content from @{original_username} saved to {download_path}", "info")

        return replacement

    def _extract_username_region(self, img: Image.Image) -> Image.Image:
        """Extract just the username region (top-left) and scale up for better OCR"""
        if not CV2_AVAILABLE:
            # Fallback: just crop using PIL
            width, height = img.size
            # Crop top 8% of image where username appears
            return img.crop((0, 0, width, int(height * 0.08)))

        try:
            # Convert PIL to OpenCV format
            img_cv = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)

            # Instagram usernames appear in top-left corner
            # Crop to top 8% where username text is located
            height, width = img_cv.shape[:2]
            username_region = img_cv[0:int(height * 0.08), :]

            # Convert to grayscale for better OCR
            gray = cv2.cvtColor(username_region, cv2.COLOR_BGR2GRAY)

            # Scale up 4x for better OCR on small text
            # Instagram story usernames are quite small
            scaled = cv2.resize(gray, None, fx=4, fy=4, interpolation=cv2.INTER_CUBIC)

            # Convert back to PIL
            return Image.fromarray(scaled)
        except Exception as e:
            self.log(f"Username region extraction failed: {e}", "debug")
            return img

    def _extract_username_from_repost(self, file_path: str) -> Optional[str]:
        """
        Extract @username from repost overlay using OCR (EasyOCR primary, Tesseract fallback)

        Handles both images and videos (multi-frame extraction for videos)
        """
        # Check if we have any OCR available
        if not self.easyocr_reader and not TESSERACT_AVAILABLE:
            self.log("No OCR engine available", "warning")
            return None

        try:
            # For images: Use EasyOCR (much better than Tesseract for Instagram overlays)
            if file_path.endswith(('.jpg', '.jpeg', '.png', '.webp', '.heic')):

                # Try EasyOCR first (best for Instagram stories)
                if self.easyocr_reader:
                    try:
                        results = self.easyocr_reader.readtext(file_path)

                        # EasyOCR returns list of (bbox, text, confidence)
                        all_text = []
                        for bbox, text, conf in results:
                            if conf > 0.5:  # Only use detections with >50% confidence
                                all_text.append(text)

                        text = " ".join(all_text)
                        if text.strip():
                            self.log(f"EasyOCR detected text: {text[:100]}", "debug")
                    except Exception as e:
                        self.log(f"EasyOCR failed: {e}, falling back to Tesseract", "debug")
                        text = ""
                else:
                    text = ""

                # Fallback to Tesseract if EasyOCR didn't find anything
                if not text.strip() and TESSERACT_AVAILABLE:
                    with Image.open(file_path) as img:
                        username_region = self._extract_username_region(img)

                        for config in ['--psm 7', '--psm 11', '--psm 6']:
                            try:
                                ocr_result = pytesseract.image_to_string(username_region, config=config)
                                if ocr_result and len(ocr_result.strip()) > 2:
                                    text = ocr_result
                                    self.log(f"Tesseract (fallback) text: {text[:100]}", "debug")
                                    break
                            except Exception:
                                continue

            # For videos: extract multiple frames and OCR each
            elif file_path.endswith(('.mp4', '.mov', '.avi', '.mkv', '.webm')):
                if not CV2_AVAILABLE:
                    self.log("OpenCV not available - cannot process video", "warning")
                    return None

                video = cv2.VideoCapture(file_path)
                if not video.isOpened():
                    self.log(f"Failed to open video: {file_path}", "warning")
                    return None

                frame_count = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
                if frame_count == 0:
                    self.log(f"Video has no frames: {file_path}", "warning")
                    return None

                # Check frames at 0%, 10%, and 50% positions
                frames_to_check = [
                    0,
                    max(0, int(frame_count * 0.1)),
                    max(0, int(frame_count * 0.5))
                ]

                text = ""
                for frame_num in frames_to_check:
                    video.set(cv2.CAP_PROP_POS_FRAMES, frame_num)
                    ret, frame = video.read()
                    if ret:
                        # Try EasyOCR first
                        if self.easyocr_reader:
                            try:
                                results = self.easyocr_reader.readtext(frame)
                                for bbox, frame_text, conf in results:
                                    if conf > 0.5:
                                        text += frame_text + " "
                            except Exception as e:
                                self.log(f"EasyOCR video frame failed: {e}", "debug")

                        # Fallback to Tesseract if needed
                        if not text.strip() and TESSERACT_AVAILABLE:
                            frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                            img = Image.fromarray(frame_rgb)
                            username_region = self._extract_username_region(img)

                            for config in ['--psm 7', '--psm 11', '--psm 6']:
                                try:
                                    ocr_result = pytesseract.image_to_string(username_region, config=config)
                                    if ocr_result and len(ocr_result.strip()) > 2:
                                        text += ocr_result + "\n"
                                        break
                                except Exception:
                                    continue

                video.release()
                self.log(f"OCR text (video, {len(frames_to_check)} frames): {text[:100]}...", "debug")
            else:
                self.log(f"Unsupported file format: {file_path}", "debug")
                return None

            # Parse text to find @username or just username
            # Pattern 1: @ followed by username characters (including space which might be underscore)
            # Instagram usernames can have underscores, but OCR sometimes reads them as spaces
            matches = re.findall(r'@([a-zA-Z0-9._ ]+)', text)

            if matches:
                # Clean up: remove trailing spaces, convert spaces to underscores
                username = matches[0].strip().replace(' ', '_')
                # Remove any characters that aren't valid in Instagram usernames
                username = re.sub(r'[^a-zA-Z0-9._]', '', username)
                # Remove trailing dots/underscores
                username = username.rstrip('._')

                if len(username) >= 3:  # Valid Instagram username minimum
                    self.log(f"Extracted username (with @): @{username}", "info")
                    return username

            # Pattern 2: Instagram username without @ (at least 3 chars, lowercase letters, numbers, dots, underscores)
            # Filter out common OCR noise and make sure it's a valid Instagram username pattern
            lines = text.split('\n')
            for line in lines:
                line = line.strip().lower()
                # Match Instagram username pattern: 3-30 chars, alphanumeric + dots/underscores
                if re.match(r'^[a-z0-9._]{3,30}$', line):
                    # Additional filter: likely an Instagram username (not random text)
                    # Instagram usernames don't end with dots and must contain letters
                    if not line.endswith('.') and re.search(r'[a-z]', line):
                        self.log(f"Extracted username (without @): @{line}", "info")
                        return line

        except Exception as e:
            self.log(f"OCR extraction failed: {e}", "warning")

        return None

    def _is_monitored_account(self, username: str) -> bool:
        """
        Check if username is in search_monitors

        Returns True if user is being actively monitored for downloads
        """
        try:
            with self.db.get_connection() as conn:
                cursor = conn.cursor()

                # Check search_monitors table
                cursor.execute("""
                    SELECT 1 FROM search_monitors
                    WHERE platform IN ('instagram', 'instaloader', 'fastdl', 'imginn')
                    AND source = ?
                    AND active = 1
                    LIMIT 1
                """, (username,))

                return cursor.fetchone() is not None

        except Exception as e:
            self.log(f"Error checking monitored status: {e}", "error")
            return False

    def _already_fetched_today(self, username: str) -> bool:
        """
        Check if we already downloaded this user's content today

        Uses repost_fetch_cache table to track fetches
        """
        try:
            with self.db.get_connection() as conn:
                cursor = conn.cursor()

                # Create cache table if doesn't exist
                cursor.execute("""
                    CREATE TABLE IF NOT EXISTS repost_fetch_cache (
                        username TEXT PRIMARY KEY,
                        last_fetched TEXT NOT NULL,
                        content_count INTEGER DEFAULT 0
                    )
                """)

                # Check if fetched in last 12 hours
                cursor.execute("""
                    SELECT last_fetched FROM repost_fetch_cache
                    WHERE username = ?
                    AND datetime(last_fetched) > datetime('now', '-12 hours')
                """, (username,))

                result = cursor.fetchone()
                return result is not None

        except Exception as e:
            self.log(f"Error checking fetch cache: {e}", "error")
            return False

    def _mark_fetched(self, username: str, content_count: int = 0):
        """Mark that we fetched this user's content"""
        try:
            with self.db.get_connection(for_write=True) as conn:
                cursor = conn.cursor()

                cursor.execute("""
                    INSERT OR REPLACE INTO repost_fetch_cache
                    (username, last_fetched, content_count)
                    VALUES (?, ?, ?)
                """, (username, datetime.now().isoformat(), content_count))

                self.log(f"Marked @{username} as fetched ({content_count} items)", "debug")

        except Exception as e:
            self.log(f"Error marking fetch: {e}", "error")

    def _download_content_via_imginn(self, username: str, destination: Path, add_to_database: bool) -> bool:
        """
        Download stories AND recent posts from user via ImgInn

        Args:
            username: Instagram username
            destination: Where to save (normal path or /tmp)
            add_to_database: If False, skip database recording (temp processing)

        Returns:
            True if successful, False otherwise
        """
        try:
            # Import imginn module
            from modules.imginn_module import ImgInnDownloader

            # Initialize ImgInn with or without database
            imginn = ImgInnDownloader(
                unified_db=self.db if add_to_database else None,
                log_callback=lambda msg, lvl: self.log(msg, lvl)
            )

            # Create destination directories
            stories_dir = destination / "stories"
            posts_dir = destination / "posts"
            stories_dir.mkdir(parents=True, exist_ok=True)
            posts_dir.mkdir(parents=True, exist_ok=True)

            # Download stories
            self.log(f"Downloading stories from @{username} via ImgInn...", "info")
            stories_files = imginn.download_stories(
                username=username,
                max_stories=50,
                output_dir=stories_dir,
                skip_database=not add_to_database
            )
            stories_count = len(stories_files) if isinstance(stories_files, list) else 0

            # Download recent posts (last 24 hours)
            self.log(f"Downloading recent posts from @{username} via ImgInn...", "info")
            posts_files = imginn.download_posts(
                username=username,
                max_posts=50,
                output_dir=posts_dir,
                max_age_hours=24,
                skip_database=not add_to_database
            )
            posts_count = len(posts_files) if isinstance(posts_files, list) else 0

            total_count = stories_count + posts_count
            self.log(f"Downloaded {total_count} items ({stories_count} stories, {posts_count} posts)", "info")

            # Mark this fetch in cache
            self._mark_fetched(username, total_count)

            return total_count > 0

        except Exception as e:
            self.log(f"ImgInn download failed: {e}", "error")
            return False

    def _find_matching_original(self, repost_path: str, search_dir: Path) -> Optional[str]:
        """
        Find matching original content using perceptual hashing

        Searches both stories/ and posts/ subdirectories

        Args:
            repost_path: Path to the repost file (e.g., evalongoria story)
            search_dir: Directory to search (e.g., /tmp/.../globalgiftfoundation/)

        Returns:
            Path to best matching original, or None
        """
        if not IMAGEHASH_AVAILABLE:
            self.log("imagehash not available - cannot match", "warning")
            return None

        # Calculate hash of repost
        repost_hash = self._get_perceptual_hash(repost_path)
        if not repost_hash:
            self.log(f"Failed to calculate hash for repost: {repost_path}", "warning")
            return None

        self.log(f"Repost hash: {repost_hash}", "debug")

        # Search both stories and posts
        best_match = None
        best_distance = 999
        threshold = 10  # Hamming distance threshold (0-64 scale)

        for subdir in ["stories", "posts"]:
            content_dir = search_dir / subdir
            if not content_dir.exists():
                self.log(f"Directory not found: {content_dir}", "debug")
                continue

            files = list(content_dir.rglob("*"))
            self.log(f"Checking {len(files)} files in {content_dir}", "debug")

            for file_path in files:
                if not file_path.is_file():
                    continue

                # Skip non-media files
                if file_path.suffix.lower() not in ['.jpg', '.jpeg', '.png', '.mp4', '.mov', '.avi', '.webp']:
                    continue

                # Calculate hash
                candidate_hash = self._get_perceptual_hash(str(file_path))
                if not candidate_hash:
                    continue

                # Compare (Hamming distance)
                distance = repost_hash - candidate_hash

                self.log(f"  {file_path.name}: distance={distance}", "debug")

                if distance < threshold and distance < best_distance:
                    best_distance = distance
                    best_match = str(file_path)
                    self.log(f"Better match found: {file_path.name} (distance: {distance})", "info")

        if best_match:
            self.log(f"✓ Found original: {Path(best_match).name} (distance: {best_distance})", "success")
            return best_match
        else:
            self.log(f"✗ No matching original found for {Path(repost_path).name}", "warning")
            return None

    def _get_perceptual_hash(self, file_path: str):
        """Calculate perceptual hash for image or video"""
        if not IMAGEHASH_AVAILABLE:
            return None

        try:
            # Image: direct hash
            if file_path.endswith(('.jpg', '.jpeg', '.png', '.webp', '.heic')):
                with Image.open(file_path) as img:
                    return imagehash.dhash(img)  # Difference hash (good for cropped/resized)

            # Video: hash middle frame
            elif file_path.endswith(('.mp4', '.mov', '.avi', '.mkv', '.webm')):
                if not CV2_AVAILABLE:
                    return None

                video = cv2.VideoCapture(file_path)
                if not video.isOpened():
                    return None

                frame_count = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
                if frame_count == 0:
                    video.release()
                    return None

                # Use middle frame
                video.set(cv2.CAP_PROP_POS_FRAMES, frame_count // 2)
                ret, frame = video.read()

                video.release()

                if ret:
                    # Convert BGR to RGB
                    frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                    img = Image.fromarray(frame_rgb)
                    result = imagehash.dhash(img)
                    img.close()
                    return result

        except Exception as e:
            self.log(f"Hash calculation failed for {Path(file_path).name}: {e}", "debug")

        return None

    def _replace_repost_with_original(self, repost_path: str, original_path: str) -> str:
        """
        Replace repost file with original high-quality file

        Workflow:
        1. Move repost to recycle bin (preserves it, not deleted)
        2. Return path to ORIGINAL file with its original filename/metadata
        3. Move module processes original as if it was downloaded directly

        Args:
            repost_path: Path to repost (e.g., evalongoria_story6.mp4)
            original_path: Path to original (e.g., globalgiftfoundation_20251109_100000.jpg)

        Returns:
            Path to original file (keeps original filename and metadata)
        """
        import os
        repost_file = Path(repost_path)
        original_file = Path(original_path)

        # Move repost to recycle bin (not delete - can recover if mistake)
        if self.db:
            try:
                recycle_id = self.db.move_to_recycle_bin(
                    file_path=str(repost_file),
                    deleted_from='repost_detection',
                    deleted_by='system',
                    metadata={
                        'reason': 'replaced_with_original',
                        'original_source': str(original_file),
                        'original_username': self.last_original_username
                    }
                )
                if recycle_id:
                    self.log(f"Moved repost to recycle bin: {repost_file.name} (ID: {recycle_id[:8]}...)", "info")
                else:
                    self.log(f"Failed to move repost to recycle bin, will delete instead", "warning")
                    # Fallback: delete if recycle bin fails
                    repost_file.unlink()
            except Exception as e:
                self.log(f"Recycle bin failed: {e}, deleting repost", "warning")
                try:
                    repost_file.unlink()
                except Exception:
                    pass
        else:
            # No database - just delete
            try:
                repost_file.unlink()
                self.log(f"Deleted repost: {repost_file.name}", "debug")
            except Exception as e:
                self.log(f"Failed to delete repost: {e}", "warning")

        # Return path to ORIGINAL file with its original filename and metadata
        # Move module will process it as if it was downloaded directly from the original source
        self.log(f"Replacing repost with original: {repost_file.name} → {original_file.name}", "info")

        # Update database to track replacement
        self._record_repost_replacement(
            repost_path=str(repost_file),
            original_path=str(original_file),
            replacement_path=str(original_file)  # Same as original - keeps original filename
        )

        return str(original_file)

    def _record_repost_replacement(self, repost_path: str, original_path: str, replacement_path: str):
        """
        Track repost replacements in database

        Creates repost_replacements table to track what was replaced
        """
        try:
            with self.db.get_connection(for_write=True) as conn:
                cursor = conn.cursor()

                # Create tracking table
                cursor.execute("""
                    CREATE TABLE IF NOT EXISTS repost_replacements (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        repost_path TEXT NOT NULL,
                        repost_filename TEXT NOT NULL,
                        repost_source TEXT NOT NULL,
                        original_path TEXT NOT NULL,
                        original_username TEXT NOT NULL,
                        replacement_path TEXT NOT NULL,
                        detected_at TEXT DEFAULT CURRENT_TIMESTAMP,
                        hash_distance INTEGER
                    )
                """)

                # Extract usernames
                repost_source = Path(repost_path).parent.name
                original_username = self.last_original_username or "unknown"

                # Insert record
                cursor.execute("""
                    INSERT INTO repost_replacements
                    (repost_path, repost_filename, repost_source, original_path, original_username, replacement_path)
                    VALUES (?, ?, ?, ?, ?, ?)
                """, (
                    repost_path,
                    Path(repost_path).name,
                    repost_source,
                    original_path,
                    original_username,
                    replacement_path
                ))

                self.log(f"Recorded replacement: {repost_source} → @{original_username}", "debug")

        except Exception as e:
            self.log(f"Failed to record replacement: {e}", "error")

    def _cleanup_temp_downloads(self, temp_dir: Path, keep_file: str = None):
        """
        Clean up temporary downloads for non-monitored accounts

        Args:
            temp_dir: Directory to clean (e.g., /tmp/repost_detection/username/)
            keep_file: Optional file to preserve (the matched original)
        """
        if not temp_dir.exists():
            return

        keep_path = Path(keep_file) if keep_file else None
        deleted_count = 0

        try:
            # Delete all files except the keeper
            for file_path in temp_dir.rglob("*"):
                if not file_path.is_file():
                    continue

                if keep_path and file_path == keep_path:
                    continue  # Skip the matched file

                try:
                    file_path.unlink()
                    deleted_count += 1
                except Exception as e:
                    self.log(f"Failed to delete temp file {file_path.name}: {e}", "debug")

            # Remove empty directories
            for subdir in [temp_dir / "stories", temp_dir / "posts"]:
                if subdir.exists() and not any(subdir.iterdir()):
                    subdir.rmdir()

            if not any(temp_dir.iterdir()):
                temp_dir.rmdir()

            self.log(f"Cleaned up {deleted_count} temporary files", "info")

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


if __name__ == "__main__":
    print("Instagram Repost Detector Module")
    print("This module should be imported, not run directly")
    print("\nDependencies:")
    print(f"  - pytesseract/PIL: {'✓' if TESSERACT_AVAILABLE else '✗'}")
    print(f"  - opencv-python: {'✓' if CV2_AVAILABLE else '✗'}")
    print(f"  - imagehash: {'✓' if IMAGEHASH_AVAILABLE else '✗'}")
