"""
Easynews Monitor Module

Monitors Easynews for new media files matching saved search terms.
Uses TMDB for metadata enrichment and organizes downloads properly.
"""

import json
import os
import sqlite3
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional

from modules.universal_logger import get_logger
from modules.easynews_client import EasynewsClient, EasynewsResult
from modules.media_identifier import MediaIdentifier
from modules.activity_status import get_activity_manager

logger = get_logger('EasynewsMonitor')

# Common words to remove from search queries
STOPWORDS = {
    'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
    'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'been',
    'be', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would',
    'could', 'should', 'may', 'might', 'must', 'shall', 'can', 'need',
    'no', 'not', 'very', 'just', 'only', 'also', 'too', 'so', 'than',
}

def clean_search_query(title: str, max_words: int = 5) -> str:
    """
    Clean a title for Easynews search by removing stopwords and limiting length.
    Keeps the most significant words for better search results.
    """
    import re
    # Remove punctuation and extra spaces
    cleaned = re.sub(r'[^\w\s]', ' ', title)
    words = cleaned.split()

    # Filter out stopwords and short words
    significant_words = [w for w in words if w.lower() not in STOPWORDS and len(w) > 2]

    # If we filtered too much, use original words (but still limit)
    if len(significant_words) < 2:
        significant_words = [w for w in words if len(w) > 2]

    # Take first N significant words
    return ' '.join(significant_words[:max_words])


# Show name overrides for ambiguous titles
# Maps (show_name, tmdb_id) or just show_name to a more specific search term
SHOW_NAME_OVERRIDES = {
    # Grand Hotel (Eva Longoria's ABC show) - search for "Grand Hotel US" to avoid
    # matching French, Norwegian, or Spanish versions
    ('Grand Hotel', 82092): 'Grand Hotel US',
}


class EasynewsMonitor:
    """
    Monitor for Easynews searches.
    Checks configured search terms and stores discovered results.
    """

    def __init__(self, db_path: str, activity_manager=None):
        """
        Initialize the Easynews Monitor.

        Args:
            db_path: Path to the SQLite database
            activity_manager: Optional activity manager for status updates
        """
        self.db_path = db_path
        self.activity_manager = activity_manager
        self.default_output_path = '/opt/immich/md/easynews/'
        self._ensure_tables()

    def _get_connection(self) -> sqlite3.Connection:
        """Get a database connection with row factory."""
        conn = sqlite3.connect(self.db_path)
        conn.row_factory = sqlite3.Row
        return conn

    def _ensure_tables(self):
        """Ensure all required tables exist."""
        conn = sqlite3.connect(self.db_path)
        try:
            cursor = conn.cursor()

            # Easynews configuration (singleton, id=1)
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS easynews_config (
                    id INTEGER PRIMARY KEY CHECK (id = 1),
                    username TEXT,
                    password TEXT,
                    enabled INTEGER DEFAULT 0,
                    check_interval_hours INTEGER DEFAULT 12,
                    last_check TEXT,
                    auto_download INTEGER DEFAULT 0,
                    min_quality TEXT DEFAULT '720p',
                    proxy_enabled INTEGER DEFAULT 0,
                    proxy_type TEXT DEFAULT 'http',
                    proxy_host TEXT,
                    proxy_port INTEGER,
                    proxy_username TEXT,
                    proxy_password TEXT,
                    notifications_enabled INTEGER DEFAULT 1,
                    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
                    updated_at TEXT DEFAULT CURRENT_TIMESTAMP
                )
            ''')

            # Add notifications_enabled column if it doesn't exist (migration)
            cursor.execute("PRAGMA table_info(easynews_config)")
            columns = [col[1] for col in cursor.fetchall()]
            if 'notifications_enabled' not in columns:
                cursor.execute('ALTER TABLE easynews_config ADD COLUMN notifications_enabled INTEGER DEFAULT 1')

            # Insert default config row if not exists
            cursor.execute('''
                INSERT OR IGNORE INTO easynews_config (id, enabled, check_interval_hours, auto_download, min_quality)
                VALUES (1, 0, 12, 0, '720p')
            ''')

            # Easynews results table (linked to celebrity_profiles)
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS easynews_results (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    celebrity_id INTEGER,
                    celebrity_name TEXT,
                    filename TEXT NOT NULL,
                    download_url TEXT NOT NULL,
                    size_bytes INTEGER,
                    post_date TEXT,
                    discovered_at TEXT DEFAULT CURRENT_TIMESTAMP,
                    parsed_title TEXT,
                    parsed_season INTEGER,
                    parsed_episode INTEGER,
                    parsed_year INTEGER,
                    tmdb_id INTEGER,
                    tmdb_title TEXT,
                    poster_url TEXT,
                    quality TEXT,
                    status TEXT DEFAULT 'new',
                    download_path TEXT,
                    FOREIGN KEY (celebrity_id) REFERENCES celebrity_profiles(id)
                )
            ''')

            conn.commit()
            logger.debug("Easynews tables ensured")
        except Exception as e:
            logger.error(f"Failed to ensure Easynews tables: {e}")
        finally:
            conn.close()

    # =========================================================================
    # CONFIGURATION METHODS
    # =========================================================================

    def get_config(self) -> Dict[str, Any]:
        """Get the Easynews configuration."""
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            cursor.execute('''
                SELECT username, password, enabled, check_interval_hours, last_check,
                       auto_download, min_quality, proxy_enabled, proxy_type,
                       proxy_host, proxy_port, proxy_username, proxy_password,
                       notifications_enabled, created_at, updated_at
                FROM easynews_config
                WHERE id = 1
            ''')
            row = cursor.fetchone()
            if row:
                return {
                    'username': row['username'],
                    'password': row['password'],
                    'enabled': bool(row['enabled']),
                    'check_interval_hours': row['check_interval_hours'],
                    'last_check': row['last_check'],
                    'auto_download': bool(row['auto_download']),
                    'min_quality': row['min_quality'],
                    'proxy_enabled': bool(row['proxy_enabled']),
                    'proxy_type': row['proxy_type'],
                    'proxy_host': row['proxy_host'],
                    'proxy_port': row['proxy_port'],
                    'proxy_username': row['proxy_username'],
                    'proxy_password': row['proxy_password'],
                    'notifications_enabled': bool(row['notifications_enabled']) if row['notifications_enabled'] is not None else True,
                    'has_credentials': bool(row['username'] and row['password']),
                }
            return {
                'username': None,
                'password': None,
                'enabled': False,
                'check_interval_hours': 12,
                'last_check': None,
                'auto_download': False,
                'min_quality': '720p',
                'proxy_enabled': False,
                'proxy_type': 'http',
                'proxy_host': None,
                'proxy_port': None,
                'proxy_username': None,
                'proxy_password': None,
                'notifications_enabled': True,
                'has_credentials': False,
            }
        finally:
            conn.close()

    def update_config(
        self,
        username: str = None,
        password: str = None,
        enabled: bool = None,
        check_interval_hours: int = None,
        auto_download: bool = None,
        min_quality: str = None,
        proxy_enabled: bool = None,
        proxy_type: str = None,
        proxy_host: str = None,
        proxy_port: int = None,
        proxy_username: str = None,
        proxy_password: str = None,
        notifications_enabled: bool = None,
    ) -> bool:
        """Update Easynews configuration."""
        conn = self._get_connection()
        try:
            cursor = conn.cursor()

            updates = []
            values = []

            if username is not None:
                updates.append('username = ?')
                values.append(username)
            if password is not None:
                updates.append('password = ?')
                values.append(password)
            if enabled is not None:
                updates.append('enabled = ?')
                values.append(1 if enabled else 0)
            if check_interval_hours is not None:
                updates.append('check_interval_hours = ?')
                values.append(check_interval_hours)
            if auto_download is not None:
                updates.append('auto_download = ?')
                values.append(1 if auto_download else 0)
            if min_quality is not None:
                updates.append('min_quality = ?')
                values.append(min_quality)
            if proxy_enabled is not None:
                updates.append('proxy_enabled = ?')
                values.append(1 if proxy_enabled else 0)
            if proxy_type is not None:
                updates.append('proxy_type = ?')
                values.append(proxy_type)
            if proxy_host is not None:
                updates.append('proxy_host = ?')
                values.append(proxy_host)
            if proxy_port is not None:
                updates.append('proxy_port = ?')
                values.append(proxy_port)
            if proxy_username is not None:
                updates.append('proxy_username = ?')
                values.append(proxy_username)
            if proxy_password is not None:
                updates.append('proxy_password = ?')
                values.append(proxy_password)
            if notifications_enabled is not None:
                updates.append('notifications_enabled = ?')
                values.append(1 if notifications_enabled else 0)

            if not updates:
                return False

            updates.append('updated_at = ?')
            values.append(datetime.now().isoformat())

            cursor.execute(f'''
                UPDATE easynews_config
                SET {', '.join(updates)}
                WHERE id = 1
            ''', values)
            conn.commit()

            logger.info("Updated Easynews configuration")
            return cursor.rowcount > 0
        finally:
            conn.close()

    def _update_last_checked(self):
        """Update the last_check timestamp."""
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            cursor.execute('''
                UPDATE easynews_config
                SET last_check = ?
                WHERE id = 1
            ''', (datetime.now().isoformat(),))
            conn.commit()
        finally:
            conn.close()

    def _get_client(self) -> Optional[EasynewsClient]:
        """Create an Easynews client from current configuration."""
        config = self.get_config()
        if not config['username'] or not config['password']:
            return None

        return EasynewsClient(
            username=config['username'],
            password=config['password'],
            proxy_enabled=config['proxy_enabled'],
            proxy_type=config['proxy_type'],
            proxy_host=config['proxy_host'],
            proxy_port=config['proxy_port'],
            proxy_username=config['proxy_username'],
            proxy_password=config['proxy_password'],
        )

    def test_connection(self) -> Dict[str, Any]:
        """Test the Easynews connection with current credentials."""
        client = self._get_client()
        if not client:
            return {
                'success': False,
                'message': 'No credentials configured'
            }
        return client.test_connection()

    # =========================================================================
    # SEARCH MANAGEMENT METHODS
    # =========================================================================

    def _ensure_searches_table(self):
        """Ensure the easynews_searches table exists."""
        conn = sqlite3.connect(self.db_path)
        try:
            cursor = conn.cursor()
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS easynews_searches (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    search_term TEXT NOT NULL,
                    media_type TEXT DEFAULT 'any',
                    tmdb_id INTEGER,
                    tmdb_title TEXT,
                    poster_url TEXT,
                    enabled INTEGER DEFAULT 1,
                    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
                    updated_at TEXT DEFAULT CURRENT_TIMESTAMP
                )
            ''')
            cursor.execute('CREATE INDEX IF NOT EXISTS idx_ens_enabled ON easynews_searches(enabled)')
            cursor.execute('CREATE INDEX IF NOT EXISTS idx_ens_term ON easynews_searches(search_term)')
            conn.commit()
        except Exception as e:
            logger.error(f"Failed to ensure easynews_searches table: {e}")
        finally:
            conn.close()

    def get_all_searches(self) -> List[Dict[str, Any]]:
        """Get all saved search terms."""
        self._ensure_searches_table()
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            cursor.execute('''
                SELECT id, search_term, media_type, tmdb_id, tmdb_title,
                       poster_url, enabled, created_at, updated_at
                FROM easynews_searches
                ORDER BY created_at DESC
            ''')
            return [dict(row) for row in cursor.fetchall()]
        except Exception as e:
            logger.error(f"Failed to get searches: {e}")
            return []
        finally:
            conn.close()

    def get_search(self, search_id: int) -> Optional[Dict[str, Any]]:
        """Get a specific search by ID."""
        self._ensure_searches_table()
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            cursor.execute('''
                SELECT id, search_term, media_type, tmdb_id, tmdb_title,
                       poster_url, enabled, created_at, updated_at
                FROM easynews_searches
                WHERE id = ?
            ''', (search_id,))
            row = cursor.fetchone()
            return dict(row) if row else None
        except Exception as e:
            logger.error(f"Failed to get search {search_id}: {e}")
            return None
        finally:
            conn.close()

    def add_search(
        self,
        search_term: str,
        media_type: str = 'any',
        tmdb_id: Optional[int] = None,
        tmdb_title: Optional[str] = None,
        poster_url: Optional[str] = None
    ) -> Optional[int]:
        """Add a new search term."""
        self._ensure_searches_table()
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            cursor.execute('''
                INSERT INTO easynews_searches
                (search_term, media_type, tmdb_id, tmdb_title, poster_url)
                VALUES (?, ?, ?, ?, ?)
            ''', (search_term, media_type, tmdb_id, tmdb_title, poster_url))
            conn.commit()
            logger.info(f"Added Easynews search: {search_term}")
            return cursor.lastrowid
        except Exception as e:
            logger.error(f"Failed to add search: {e}")
            return None
        finally:
            conn.close()

    def update_search(
        self,
        search_id: int,
        search_term: Optional[str] = None,
        media_type: Optional[str] = None,
        enabled: Optional[bool] = None,
        tmdb_id: Optional[int] = None,
        tmdb_title: Optional[str] = None,
        poster_url: Optional[str] = None
    ) -> bool:
        """Update an existing search term."""
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            updates = []
            values = []

            if search_term is not None:
                updates.append('search_term = ?')
                values.append(search_term)
            if media_type is not None:
                updates.append('media_type = ?')
                values.append(media_type)
            if enabled is not None:
                updates.append('enabled = ?')
                values.append(1 if enabled else 0)
            if tmdb_id is not None:
                updates.append('tmdb_id = ?')
                values.append(tmdb_id)
            if tmdb_title is not None:
                updates.append('tmdb_title = ?')
                values.append(tmdb_title)
            if poster_url is not None:
                updates.append('poster_url = ?')
                values.append(poster_url)

            if not updates:
                return False

            updates.append('updated_at = ?')
            values.append(datetime.now().isoformat())
            values.append(search_id)

            cursor.execute(f'''
                UPDATE easynews_searches
                SET {', '.join(updates)}
                WHERE id = ?
            ''', values)
            conn.commit()
            logger.info(f"Updated Easynews search {search_id}")
            return cursor.rowcount > 0
        except Exception as e:
            logger.error(f"Failed to update search {search_id}: {e}")
            return False
        finally:
            conn.close()

    def delete_search(self, search_id: int) -> bool:
        """Delete a search term."""
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            cursor.execute('DELETE FROM easynews_searches WHERE id = ?', (search_id,))
            conn.commit()
            logger.info(f"Deleted Easynews search {search_id}")
            return cursor.rowcount > 0
        except Exception as e:
            logger.error(f"Failed to delete search {search_id}: {e}")
            return False
        finally:
            conn.close()

    def check_single_search(self, search_id: int) -> Dict[str, Any]:
        """Check Easynews for a specific search term."""
        search = self.get_search(search_id)
        if not search:
            return {'success': False, 'message': 'Search not found', 'results_found': 0}

        config = self.get_config()
        if not config['has_credentials']:
            return {'success': False, 'message': 'No credentials configured', 'results_found': 0}

        client = self._get_client()
        if not client:
            return {'success': False, 'message': 'Failed to create client', 'results_found': 0}

        try:
            results = client.search(search['search_term'], results_per_page=250)
            # Store results - simplified version
            results_found = len(results) if results else 0
            logger.info(f"Easynews search '{search['search_term']}' found {results_found} results")
            return {
                'success': True,
                'message': f"Found {results_found} results",
                'results_found': results_found
            }
        except Exception as e:
            logger.error(f"Error searching Easynews for '{search['search_term']}': {e}")
            return {'success': False, 'message': str(e), 'results_found': 0}

    # =========================================================================
    # CELEBRITY METHODS (uses tracked celebrities from Appearances)
    # =========================================================================

    def get_celebrities(self) -> List[Dict[str, Any]]:
        """Get all enabled celebrity profiles for Easynews searching."""
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            cursor.execute('''
                SELECT id, name, slug, image_url, enabled
                FROM celebrity_profiles
                WHERE enabled = 1
                ORDER BY name ASC
            ''')
            return [dict(row) for row in cursor.fetchall()]
        finally:
            conn.close()

    def get_celebrity_count(self) -> int:
        """Get count of enabled celebrities."""
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            cursor.execute('SELECT COUNT(*) FROM celebrity_profiles WHERE enabled = 1')
            return cursor.fetchone()[0]
        except Exception:
            return 0
        finally:
            conn.close()

    def get_celebrity_appearances(self, celebrity_id: int) -> List[Dict[str, Any]]:
        """Get appearances (TV shows, movies) for a celebrity with TMDB data."""
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            cursor.execute('''
                SELECT DISTINCT
                    show_name, tmdb_show_id, appearance_type,
                    season_number, episode_number, episode_title,
                    celebrity_name
                FROM celebrity_appearances
                WHERE celebrity_id = ?
                AND tmdb_show_id IS NOT NULL
                ORDER BY show_name, season_number, episode_number
            ''', (celebrity_id,))
            return [dict(row) for row in cursor.fetchall()]
        finally:
            conn.close()

    def get_tmdb_poster(self, tmdb_id: int, media_type: str) -> Optional[str]:
        """Get TMDB backdrop URL (widescreen) for a show/movie."""
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            # Try to get from appearance_config (has TMDB API key)
            cursor.execute('SELECT tmdb_api_key FROM appearance_config WHERE id = 1')
            row = cursor.fetchone()
            if not row or not row[0]:
                return None

            api_key = row[0]
            endpoint = 'tv' if media_type == 'TV' else 'movie'

            import requests
            response = requests.get(
                f"https://api.themoviedb.org/3/{endpoint}/{tmdb_id}",
                params={'api_key': api_key},
                timeout=10
            )
            if response.status_code == 200:
                data = response.json()
                # Prefer backdrop (widescreen) over poster (portrait)
                backdrop_path = data.get('backdrop_path')
                if backdrop_path:
                    return f"https://image.tmdb.org/t/p/w780{backdrop_path}"
                # Fall back to poster if no backdrop
                poster_path = data.get('poster_path')
                if poster_path:
                    return f"https://image.tmdb.org/t/p/w500{poster_path}"
            return None
        except Exception as e:
            logger.error(f"Failed to get TMDB backdrop: {e}")
            return None
        finally:
            conn.close()

    # =========================================================================
    # RESULTS MANAGEMENT METHODS
    # =========================================================================

    def get_results(
        self,
        status: str = None,
        celebrity_id: int = None,
        limit: int = 100,
        offset: int = 0,
    ) -> List[Dict[str, Any]]:
        """
        Get discovered results.

        Args:
            status: Filter by status ('new', 'downloaded', 'ignored', 'failed')
            celebrity_id: Filter by celebrity ID
            limit: Maximum results to return
            offset: Offset for pagination

        Returns:
            List of result dictionaries
        """
        conn = self._get_connection()
        try:
            cursor = conn.cursor()

            query = '''
                SELECT r.*, c.name as celebrity_display_name, c.image_url as celebrity_image
                FROM easynews_results r
                LEFT JOIN celebrity_profiles c ON r.celebrity_id = c.id
            '''
            conditions = []
            values = []

            if status:
                conditions.append('r.status = ?')
                values.append(status)
            if celebrity_id:
                conditions.append('r.celebrity_id = ?')
                values.append(celebrity_id)

            if conditions:
                query += ' WHERE ' + ' AND '.join(conditions)

            query += ' ORDER BY r.discovered_at DESC LIMIT ? OFFSET ?'
            values.extend([limit, offset])

            cursor.execute(query, values)
            return [dict(row) for row in cursor.fetchall()]
        finally:
            conn.close()

    def get_result_count(self, status: str = None) -> int:
        """Get count of results, optionally filtered by status."""
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            if status:
                cursor.execute('SELECT COUNT(*) FROM easynews_results WHERE status = ?', (status,))
            else:
                cursor.execute('SELECT COUNT(*) FROM easynews_results')
            return cursor.fetchone()[0]
        finally:
            conn.close()

    def update_result_status(self, result_id: int, status: str, download_path: str = None) -> bool:
        """Update a result's status."""
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            if download_path:
                cursor.execute('''
                    UPDATE easynews_results
                    SET status = ?, download_path = ?
                    WHERE id = ?
                ''', (status, download_path, result_id))
            else:
                cursor.execute('''
                    UPDATE easynews_results
                    SET status = ?
                    WHERE id = ?
                ''', (status, result_id))
            conn.commit()
            return cursor.rowcount > 0
        finally:
            conn.close()

    def _get_or_create_easynews_preset(self, celebrity_id: int, celebrity_name: str) -> int:
        """Get or create an Easynews preset for a celebrity."""
        conn = self._get_connection()
        try:
            cursor = conn.cursor()

            # Check if preset exists
            cursor.execute('''
                SELECT id FROM celebrity_search_presets
                WHERE celebrity_id = ? AND source_type = 'easynews'
            ''', (celebrity_id,))
            row = cursor.fetchone()
            if row:
                return row[0]

            # Create new preset
            cursor.execute('''
                INSERT INTO celebrity_search_presets (
                    celebrity_id, name, source_type, source_value, content_type, enabled
                ) VALUES (?, ?, 'easynews', ?, 'other', 1)
            ''', (celebrity_id, f'Easynews - {celebrity_name}', celebrity_name))
            conn.commit()
            return cursor.lastrowid
        finally:
            conn.close()

    def _save_result(
        self,
        celebrity_id: int,
        celebrity_name: str,
        easynews_result: EasynewsResult,
        parsed_info: Dict[str, Any],
        tmdb_match: Optional[Dict[str, Any]],
    ) -> Optional[int]:
        """Save a discovered result to celebrity_discovered_videos (same as YouTube videos)."""
        import hashlib

        # Get or create the Easynews preset for this celebrity
        preset_id = self._get_or_create_easynews_preset(celebrity_id, celebrity_name)

        conn = self._get_connection()
        try:
            cursor = conn.cursor()

            # Create a unique video_id from the filename and URL
            video_id = hashlib.md5(f"{easynews_result.filename}:{easynews_result.download_url}".encode()).hexdigest()[:16]

            # Check for duplicate in celebrity_discovered_videos
            cursor.execute('''
                SELECT id FROM celebrity_discovered_videos
                WHERE platform = 'easynews' AND video_id = ?
            ''', (video_id,))

            if cursor.fetchone():
                return None  # Already exists

            # Prepare values
            parsed_title = parsed_info.get('title') if parsed_info else None
            parsed_season = parsed_info.get('season') if parsed_info else None
            parsed_episode = parsed_info.get('episode') if parsed_info else None
            parsed_year = parsed_info.get('year') if parsed_info else None
            quality = parsed_info.get('quality') if parsed_info else EasynewsClient.detect_quality(easynews_result.filename)

            tmdb_title = tmdb_match.get('title') if tmdb_match else None
            poster_url = tmdb_match.get('poster_path') if tmdb_match else None

            # Build display title
            if tmdb_title:
                if parsed_season and parsed_episode:
                    display_title = f"{tmdb_title} S{parsed_season:02d}E{parsed_episode:02d}"
                elif parsed_year:
                    display_title = f"{tmdb_title} ({parsed_year})"
                else:
                    display_title = tmdb_title
            elif parsed_title:
                display_title = parsed_title
            else:
                display_title = easynews_result.filename

            # Build description with file info
            size_mb = easynews_result.size_bytes / (1024 * 1024) if easynews_result.size_bytes else 0
            description = f"Quality: {quality or 'Unknown'} | Size: {size_mb:.1f} MB | File: {easynews_result.filename}"

            # Insert into celebrity_discovered_videos (same table as YouTube)
            cursor.execute('''
                INSERT INTO celebrity_discovered_videos (
                    preset_id, celebrity_id, video_id, platform, url, title,
                    channel_name, thumbnail, upload_date, description,
                    content_type, status
                ) VALUES (?, ?, ?, 'easynews', ?, ?, ?, ?, ?, ?, 'other', 'new')
            ''', (
                preset_id,
                celebrity_id,
                video_id,
                easynews_result.download_url,
                display_title,
                f"Easynews - {celebrity_name}",  # channel_name
                poster_url,  # thumbnail (TMDB poster)
                easynews_result.post_date,
                description,
            ))
            conn.commit()
            logger.info(f"Saved Easynews result for {celebrity_name}: {display_title}")
            return cursor.lastrowid
        except Exception as e:
            logger.error(f"Failed to save result: {e}")
            return None
        finally:
            conn.close()

    # =========================================================================
    # CHECK METHODS
    # =========================================================================

    def get_status(self) -> Dict[str, Any]:
        """Get current check status from database (works across processes)."""
        activity_manager = get_activity_manager()
        task_status = activity_manager.get_background_task("easynews_check")

        if task_status and task_status.get('active'):
            extra = task_status.get('extra_data', {}) or {}
            return {
                "is_running": True,
                "current_search": extra.get('current_search'),
                "searches_processed": task_status.get('progress', {}).get('current', 0),
                "total_searches": task_status.get('progress', {}).get('total', 0),
                "results_found": extra.get('results_found', 0),
                "last_check": extra.get('last_check'),
                "error": None,
            }

        # Not running - return default state
        return {
            "is_running": False,
            "current_search": None,
            "searches_processed": 0,
            "total_searches": 0,
            "results_found": 0,
            "last_check": None,
            "error": None,
        }

    def check_all_celebrities(
        self,
        progress_callback: Callable[[str, int, int], None] = None,
        from_scheduler: bool = False,
    ) -> Dict[str, Any]:
        """
        Check Easynews for all tracked celebrities by searching for their appearances.

        Searches for TV shows and movies the celebrity appears in (from TMDB data),
        not the celebrity name directly.

        Args:
            progress_callback: Optional callback(search_term, processed, total)
            from_scheduler: If True, send push notifications (scheduler runs only)

        Returns:
            Dict with check results
        """
        # Get configuration
        config = self.get_config()
        if not config['has_credentials']:
            return {
                'success': False,
                'message': 'No Easynews credentials configured',
                'results_found': 0,
            }

        # Get enabled celebrities
        celebrities = self.get_celebrities()
        if not celebrities:
            return {
                'success': True,
                'message': 'No celebrities tracked',
                'results_found': 0,
            }

        # Build list of all shows/movies to search (deduplicated)
        searches_to_perform = []  # List of (celebrity_id, celebrity_name, show_name, tmdb_id, media_type, season, episode)
        seen_searches = set()
        poster_cache = {}  # Cache TMDB posters

        # First pass: identify TV shows that have episode-specific entries
        # so we can skip generic entries for those shows
        shows_with_episodes = set()
        for celebrity in celebrities:
            appearances = self.get_celebrity_appearances(celebrity['id'])
            for app in appearances:
                show_name = app.get('show_name')
                media_type = app.get('appearance_type', 'TV')
                season = app.get('season_number')
                episode = app.get('episode_number')
                if media_type != 'Movie' and season and episode:
                    shows_with_episodes.add(show_name)

        for celebrity in celebrities:
            appearances = self.get_celebrity_appearances(celebrity['id'])
            for app in appearances:
                show_name = app.get('show_name')
                tmdb_id = app.get('tmdb_show_id')
                media_type = app.get('appearance_type', 'TV')
                season = app.get('season_number')
                episode = app.get('episode_number')

                if not show_name:
                    continue

                # Build search term (clean to remove stopwords for better results)
                # Check for show name overrides (for ambiguous titles)
                override_key = (show_name, tmdb_id)
                if override_key in SHOW_NAME_OVERRIDES:
                    clean_name = SHOW_NAME_OVERRIDES[override_key]
                else:
                    clean_name = clean_search_query(show_name)
                if media_type == 'Movie':
                    search_term = clean_name
                    search_key = f"movie:{show_name}"
                elif season and episode:
                    search_term = f"{clean_name} S{season:02d}E{episode:02d}"
                    search_key = f"tv:{show_name}:S{season}E{episode}"
                elif season:
                    search_term = f"{clean_name} S{season:02d}"
                    search_key = f"tv:{show_name}:S{season}"
                else:
                    # Skip generic TV entries if we have episode-specific entries for this show
                    if show_name in shows_with_episodes:
                        logger.debug(f"Skipping generic search for '{show_name}' - has episode-specific entries")
                        continue
                    search_term = clean_name
                    search_key = f"tv:{show_name}"

                if search_key not in seen_searches:
                    seen_searches.add(search_key)
                    searches_to_perform.append({
                        'celebrity_id': celebrity['id'],
                        'celebrity_name': celebrity['name'],
                        'show_name': show_name,
                        'search_term': search_term,
                        'tmdb_id': tmdb_id,
                        'media_type': media_type,
                        'season': season,
                        'episode': episode,
                    })

        if not searches_to_perform:
            return {
                'success': True,
                'message': 'No appearances to search for',
                'results_found': 0,
            }

        # Initialize status tracking in database (works across processes)
        activity_manager = get_activity_manager()
        easynews_extra_data = {
            "current_search": None,
            "results_found": 0,
            "last_check": None,
        }
        activity_manager.start_background_task(
            task_id="easynews_check",
            task_type="easynews_check",
            display_name="Easynews",
            status="Starting",
            extra_data=easynews_extra_data
        )

        # Create client
        client = self._get_client()
        if not client:
            activity_manager.stop_background_task("easynews_check")
            return {
                'success': False,
                'message': 'Failed to create Easynews client',
                'results_found': 0,
            }

        total_new = 0

        # Crash recovery checkpoint
        from modules.task_checkpoint import TaskCheckpoint
        checkpoint = TaskCheckpoint('easynews_monitor', 'background')
        checkpoint.start(total_items=len(searches_to_perform))
        if checkpoint.is_recovering():
            logger.info("Easynews monitor: recovering — skipping already-completed searches")

        try:
            for i, search in enumerate(searches_to_perform):
                search_term = search['search_term']
                celebrity_id = search['celebrity_id']
                celebrity_name = search['celebrity_name']
                show_name = search['show_name']
                tmdb_id = search['tmdb_id']
                media_type = search['media_type']
                season = search['season']
                episode = search['episode']

                if checkpoint.is_completed(search_term):
                    continue

                checkpoint.set_current(search_term)

                easynews_extra_data["current_search"] = search_term
                activity_manager.update_background_task(
                    "easynews_check", f"Searching: {search_term}",
                    progress_current=i, progress_total=len(searches_to_perform),
                    extra_data=easynews_extra_data
                )

                if progress_callback:
                    progress_callback(search_term, i, len(searches_to_perform))

                logger.info(f"Searching Easynews: {search_term}")

                try:
                    # Get TMDB poster (cached)
                    if tmdb_id and tmdb_id not in poster_cache:
                        poster_cache[tmdb_id] = self.get_tmdb_poster(tmdb_id, media_type)
                    poster_url = poster_cache.get(tmdb_id)

                    # Search Easynews (use max 250 results to find more episodes)
                    results = client.search(search_term, results_per_page=250)

                    for result in results:
                        # Skip sample files
                        if 'sample' in result.filename.lower():
                            continue

                        # Validate result matches the show we're looking for
                        if not self._result_matches_show(result.filename, show_name):
                            continue

                        # Check quality filter
                        quality = EasynewsClient.detect_quality(result.filename)
                        if config['min_quality'] and not self._meets_quality(quality, config['min_quality']):
                            continue

                        # Save result with proper metadata
                        new_id = self._save_appearance_result(
                            celebrity_id=celebrity_id,
                            celebrity_name=celebrity_name,
                            show_name=show_name,
                            tmdb_id=tmdb_id,
                            media_type=media_type,
                            season=season,
                            episode=episode,
                            poster_url=poster_url,
                            easynews_result=result,
                            quality=quality,
                        )
                        if new_id:
                            total_new += 1
                            easynews_extra_data["results_found"] = total_new
                            activity_manager.update_background_task(
                                "easynews_check", f"Found {total_new} new results",
                                progress_current=i, progress_total=len(searches_to_perform),
                                extra_data=easynews_extra_data
                            )

                except Exception as e:
                    logger.error(f"Error searching Easynews for '{search_term}': {e}")
                    continue

                checkpoint.mark_completed(search_term)

            # Checkpoint complete
            checkpoint.finish()

            # Mark as complete
            easynews_extra_data["last_check"] = datetime.now().isoformat()
            activity_manager.stop_background_task("easynews_check")

            self._update_last_checked()

            logger.info(f"Easynews check complete: {total_new} new results found")

            # Send notification if from scheduler and new results found
            if from_scheduler and total_new > 0:
                config = self.get_config()
                if config.get('notifications_enabled', True):
                    self._send_notification(total_new)

            return {
                'success': True,
                'message': f'Check complete: {total_new} new results found',
                'results_found': total_new,
                'searches_performed': len(searches_to_perform),
            }

        except Exception as e:
            logger.error(f"Easynews check failed: {e}")
            activity_manager.stop_background_task("easynews_check")
            return {
                'success': False,
                'message': str(e),
                'results_found': total_new,
            }

    def _send_notification(self, results_found: int):
        """
        Send a Pushover notification about new Easynews results found.

        Args:
            results_found: Number of new results found
        """
        # Don't send notification if no new results
        if results_found <= 0:
            logger.debug("Skipping Easynews notification - no new results")
            return

        try:
            import random
            from modules.pushover_notifier import PushoverNotifier
            from modules.settings_manager import SettingsManager
            from modules.unified_database import UnifiedDatabase

            # Get pushover config from settings
            settings_manager = SettingsManager(self.db_path)
            pushover_config = settings_manager.get('pushover', {})

            if not pushover_config.get('enabled'):
                logger.debug("Pushover notifications disabled globally")
                return

            # Create unified_db for recording notification to database
            unified_db = UnifiedDatabase(self.db_path)

            # Create notifier with unified_db so notification is recorded
            notifier = PushoverNotifier(
                api_token=pushover_config.get('api_token'),
                user_key=pushover_config.get('user_key'),
                unified_db=unified_db
            )

            # Get show summary from recently discovered videos
            image_path = None
            thumbnail_url = None
            shows = {}
            show_list = []
            conn = self._get_connection()
            try:
                cursor = conn.cursor()
                # Get the most recent Easynews results
                cursor.execute('''
                    SELECT thumbnail, title, channel_name FROM celebrity_discovered_videos
                    WHERE platform = 'easynews' AND thumbnail IS NOT NULL AND thumbnail != ''
                    ORDER BY discovered_at DESC
                    LIMIT ?
                ''', (results_found,))
                rows = cursor.fetchall()
                if rows:
                    # Pick a random thumbnail from this batch
                    selected = random.choice(rows)
                    thumbnail_url = selected['thumbnail']

                    # Build show summary from unique shows in THIS batch only
                    shows = {}
                    for row in rows:
                        # Extract show name from channel_name (e.g., "Easynews - tv:Show Name:S1E1")
                        channel = row['channel_name'] or ''
                        if channel.startswith('Easynews - '):
                            parts = channel.replace('Easynews - ', '').split(':')
                            if len(parts) >= 2:
                                show_name = parts[1]
                            else:
                                show_name = row['title'] or 'Unknown'
                        else:
                            show_name = row['title'] or 'Unknown'
                        shows[show_name] = shows.get(show_name, 0) + 1
                        if show_name not in show_list:
                            show_list.append(show_name)

                    # Download thumbnail to temp file
                    if thumbnail_url:
                        import urllib.request
                        import tempfile
                        try:
                            temp_dir = tempfile.gettempdir()
                            temp_path = f"{temp_dir}/easynews_thumb_{random.randint(1000, 9999)}.jpg"
                            urllib.request.urlretrieve(thumbnail_url, temp_path)
                            image_path = temp_path
                            logger.debug(f"Downloaded thumbnail for notification: {temp_path}")
                        except Exception as e:
                            logger.debug(f"Could not download thumbnail: {e}")
            finally:
                conn.close()

            # Build title matching standard notification format
            title = f"📰 {results_found} New Result{'s' if results_found > 1 else ''} on Easynews"

            # Build HTML message matching standard notification format
            from datetime import datetime
            message_parts = []
            message_parts.append("📱 <b>Platform:</b> Easynews")
            message_parts.append(f"🎥 <b>Results:</b> {results_found} new result{'s' if results_found > 1 else ''}")

            # Add show breakdown
            if shows:
                show_parts = [f"{name} ({count})" if count > 1 else name
                              for name, count in sorted(shows.items(), key=lambda x: -x[1])[:5]]
                message_parts.append(f"📺 <b>Shows:</b> {', '.join(show_parts)}")

            now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            message_parts.append(f"⏰ <b>Discovered:</b> {now}")

            message = "\n".join(message_parts)

            # Set notification context for database recording
            # Store thumbnail_url (not temp file path) so notification page can display it
            notifier._current_notification_context = {
                'platform': 'easynews',
                'source': 'easynews_monitor',
                'content_type': 'video',
                'download_count': results_found,
                'metadata': {'shows': show_list, 'thumbnail_url': thumbnail_url}
            }

            # Send notification
            success = notifier.send_notification(
                title=title,
                message=message,
                priority=0,  # Normal priority
                image_path=image_path,
                html=True
            )

            # Clean up temp file
            if image_path:
                try:
                    import os
                    os.unlink(image_path)
                except OSError:
                    pass  # Best effort cleanup of temp file

            if success:
                logger.info(f"Sent notification: {results_found} Easynews results found")
            else:
                logger.debug("Notification not sent (disabled or failed)")

        except Exception as e:
            logger.warning(f"Could not send notification: {e}")

    def _get_quality_rank(self, quality: Optional[str], source: Optional[str] = None, audio: Optional[str] = None) -> int:
        """Get numeric rank for quality comparison (higher is better).

        Combines resolution, source, and audio into a single score:
        - Resolution: 360p=100, 480p=200, 720p=300, 1080p=400, 2160p=500
        - Source: Remux=50, BluRay=40, WEB-DL=30, WEB=20, HDTV=10
        - Audio: Atmos=9, TrueHD=8, DTS-HD=7, 5.1=4, etc.
        """
        score = 0

        # Resolution score (most important)
        if quality:
            resolution_scores = {
                '360p': 100,
                '480p': 200,
                '720p': 300,
                '1080p': 400,
                '2160p': 500,
            }
            score += resolution_scores.get(quality.lower(), 0)

        # Source score
        if source:
            source_scores = {
                'Remux': 50,
                'BluRay': 40,
                'WEB-DL': 30,
                'WEBRip': 25,
                'WEB': 20,
                'HDTV': 10,
                'DVDRip': 5,
                'DVD': 3,
                'CAM': 1,
            }
            score += source_scores.get(source, 0)

        # Audio score
        if audio:
            audio_scores = {
                'Atmos': 9,
                'TrueHD': 8,
                'DTS-HD': 7,
                'DTS:X': 7,
                'DTS': 6,
                '7.1': 5,
                '5.1': 4,
                'FLAC': 3,
                'AAC': 2,
                'MP3': 1,
            }
            score += audio_scores.get(audio, 0)

        return score

    def _save_appearance_result(
        self,
        celebrity_id: int,
        celebrity_name: str,
        show_name: str,
        tmdb_id: int,
        media_type: str,
        season: Optional[int],
        episode: Optional[int],
        poster_url: Optional[str],
        easynews_result: EasynewsResult,
        quality: Optional[str],
    ) -> Optional[int]:
        """Save an Easynews result - only keeps best quality per episode."""
        import hashlib

        # Get or create the Easynews preset for this celebrity
        preset_id = self._get_or_create_easynews_preset(celebrity_id, celebrity_name)

        conn = self._get_connection()
        try:
            cursor = conn.cursor()

            # Build a unique key for this show/episode (not per file)
            if media_type == 'Movie':
                content_key = f"movie:{show_name}"
            elif season and episode:
                content_key = f"tv:{show_name}:S{season}E{episode}"
            elif season:
                content_key = f"tv:{show_name}:S{season}"
            else:
                content_key = f"tv:{show_name}"
                # For TV shows without episode info, skip if we already have episode-specific entries
                cursor.execute('''
                    SELECT COUNT(*) FROM celebrity_discovered_videos
                    WHERE platform = 'easynews'
                    AND celebrity_id = ?
                    AND channel_name LIKE ?
                ''', (celebrity_id, f"Easynews - tv:{show_name}:S%"))
                if cursor.fetchone()[0] > 0:
                    # Already have episode-specific entries, skip this generic one
                    return None

            # Detect audio codec and source FIRST (needed for comparison)
            audio_codec = EasynewsClient.detect_audio(easynews_result.filename)
            source = EasynewsClient.detect_source(easynews_result.filename)

            # Calculate new result's score
            new_score = self._get_quality_rank(quality, source, audio_codec)

            # Check if we already have this episode - look by channel_name which contains show info
            cursor.execute('''
                SELECT id, metadata FROM celebrity_discovered_videos
                WHERE platform = 'easynews'
                AND celebrity_id = ?
                AND channel_name = ?
            ''', (celebrity_id, f"Easynews - {content_key}"))

            existing = cursor.fetchone()
            if existing:
                # Parse existing quality, source, audio from metadata
                existing_meta = {}
                if existing[1]:
                    try:
                        existing_meta = json.loads(existing[1])
                    except (json.JSONDecodeError, TypeError):
                        pass  # Invalid JSON metadata, use empty dict

                existing_quality = existing_meta.get('quality')
                existing_source = existing_meta.get('source')
                existing_audio = existing_meta.get('audio_codec')
                existing_score = self._get_quality_rank(existing_quality, existing_source, existing_audio)

                # Only replace if new score is better
                if new_score <= existing_score:
                    return None  # Existing is same or better quality

                # Delete existing to replace with better quality (upgrade, not truly new)
                logger.info(f"Upgrading {show_name}: score {existing_score} -> {new_score}")
                cursor.execute('DELETE FROM celebrity_discovered_videos WHERE id = ?', (existing[0],))
                is_upgrade = True
            else:
                is_upgrade = False

            # Create a unique video_id from the filename and URL
            video_id = hashlib.md5(f"{easynews_result.filename}:{easynews_result.download_url}".encode()).hexdigest()[:16]

            # Build display title with proper formatting
            if media_type == 'Movie':
                display_title = show_name
            elif season and episode:
                display_title = f"{show_name} - S{season:02d}E{episode:02d}"
            elif season:
                display_title = f"{show_name} - Season {season}"
            else:
                display_title = show_name

            # Build description with file info
            size_mb = easynews_result.size_bytes / (1024 * 1024) if easynews_result.size_bytes else 0
            description = f"Quality: {quality or 'Unknown'} | Source: {source or 'Unknown'} | Audio: {audio_codec or 'Unknown'} | Size: {size_mb:.1f} MB | File: {easynews_result.filename}"

            # Extract numeric resolution (e.g., "720p" -> 720)
            resolution_int = None
            if quality:
                try:
                    resolution_int = int(quality.lower().replace('p', ''))
                except ValueError:
                    pass

            # Build metadata JSON
            metadata = json.dumps({
                'audio_codec': audio_codec,
                'source': source,
                'quality': quality,
                'size_bytes': easynews_result.size_bytes,
                'original_filename': easynews_result.filename,
            })

            # Insert into celebrity_discovered_videos
            cursor.execute('''
                INSERT INTO celebrity_discovered_videos (
                    preset_id, celebrity_id, video_id, platform, url, title,
                    channel_name, thumbnail, upload_date, description,
                    content_type, status, max_resolution, metadata
                ) VALUES (?, ?, ?, 'easynews', ?, ?, ?, ?, ?, ?, 'other', 'new', ?, ?)
            ''', (
                preset_id,
                celebrity_id,
                video_id,
                easynews_result.download_url,
                display_title,
                f"Easynews - {content_key}",  # channel_name stores content key for dedup
                poster_url,  # thumbnail (TMDB poster)
                easynews_result.post_date,
                description,
                resolution_int,
                metadata,
            ))
            conn.commit()

            # Pre-cache thumbnail for faster page loading
            if poster_url:
                self._cache_thumbnail(video_id, poster_url, cursor, conn)

            # Return None for upgrades (don't count as "new" for notifications)
            # Only return ID for truly new content
            if is_upgrade:
                logger.info(f"Upgraded Easynews result: {display_title} ({quality})")
                return None
            else:
                logger.info(f"Saved NEW Easynews result: {display_title} ({quality})")
                return cursor.lastrowid
        except Exception as e:
            logger.error(f"Failed to save appearance result: {e}")
            return None
        finally:
            conn.close()

    def _cache_thumbnail(self, video_id: str, thumbnail_url: str, cursor, conn) -> None:
        """
        Pre-cache thumbnail by fetching from URL and storing in database.
        This speeds up Internet Discovery page loading.
        """
        try:
            import requests
            response = requests.get(thumbnail_url, timeout=10, headers={
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
            })
            if response.status_code == 200 and response.content:
                cursor.execute('''
                    UPDATE celebrity_discovered_videos
                    SET thumbnail_data = ?
                    WHERE video_id = ?
                ''', (response.content, video_id))
                conn.commit()
                logger.debug(f"Cached thumbnail for {video_id}", module="Easynews")
        except Exception as e:
            logger.debug(f"Failed to cache thumbnail for {video_id}: {e}", module="Easynews")

    def _meets_quality(self, quality: Optional[str], min_quality: str) -> bool:
        """Check if quality meets minimum requirement."""
        if not min_quality:
            return True  # No minimum set, allow all
        if not quality:
            return False  # Can't detect quality but minimum is set, reject

        quality_order = ['360p', '480p', '720p', '1080p', '2160p']

        try:
            quality_idx = quality_order.index(quality.lower())
            min_idx = quality_order.index(min_quality.lower())
            return quality_idx >= min_idx
        except ValueError:
            return False  # Unknown quality format, reject when minimum is set

    def _result_matches_show(self, filename: str, show_name: str) -> bool:
        """Check if a result filename matches the show we're looking for.

        Validates that key words from the show name appear in the filename
        to avoid false matches (e.g., 'Very Bad Day' vs 'Very Bad Road Trip').
        """
        import re

        # Normalize both strings
        filename_lower = filename.lower()
        filename_clean = re.sub(r'[^\w\s]', ' ', filename_lower)
        filename_words = filename_clean.split()

        # Get significant words from show name (skip stopwords)
        show_words = re.sub(r'[^\w\s]', ' ', show_name.lower()).split()
        significant_words = [w for w in show_words if w.lower() not in STOPWORDS and len(w) > 2]

        if not significant_words:
            return True  # Can't validate, allow

        # For short titles (2-3 words), require words to appear adjacent in filename
        # This prevents matching scattered words like "christmas...karma" in unrelated content
        if len(significant_words) <= 3:
            # Build a pattern that matches the words in order with optional separators
            pattern = r'[\.\s\-_]*'.join(re.escape(w) for w in significant_words)
            if re.search(pattern, filename_clean):
                return True
            return False

        # For longer titles, count matches and require 80%
        matches = sum(1 for word in significant_words if word in filename_clean)
        match_ratio = matches / len(significant_words)

        if len(significant_words) <= 5:
            return match_ratio >= 1.0
        else:
            return match_ratio >= 0.8

    def _get_tmdb_api_key(self) -> Optional[str]:
        """Get TMDB API key from appearance config."""
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            cursor.execute('SELECT tmdb_api_key FROM appearance_config WHERE id = 1')
            row = cursor.fetchone()
            return row[0] if row else None
        except Exception:
            return None
        finally:
            conn.close()

    # =========================================================================
    # DOWNLOAD METHODS
    # =========================================================================

    def download_result(
        self,
        result_id: int,
        output_dir: str = None,
        progress_callback: Callable[[int, int], None] = None,
    ) -> Dict[str, Any]:
        """
        Download a result to the appropriate organized path.

        Args:
            result_id: ID of the result to download
            output_dir: Override output directory (uses default if not specified)
            progress_callback: Optional callback(downloaded_bytes, total_bytes)

        Returns:
            Dict with download result
        """
        # Get result
        conn = self._get_connection()
        try:
            cursor = conn.cursor()
            cursor.execute('SELECT * FROM easynews_results WHERE id = ?', (result_id,))
            row = cursor.fetchone()
            if not row:
                return {'success': False, 'message': 'Result not found'}
            result = dict(row)
        finally:
            conn.close()

        # Get client
        client = self._get_client()
        if not client:
            return {'success': False, 'message': 'No Easynews credentials configured'}

        # Determine output path
        base_path = output_dir or self.default_output_path

        # Use TMDB info to create organized path
        if result.get('tmdb_id') and result.get('tmdb_title'):
            tmdb_api_key = self._get_tmdb_api_key()
            if tmdb_api_key:
                identifier = MediaIdentifier(tmdb_api_key)
                # Create TMDBMatch-like object
                from modules.media_identifier import TMDBMatch
                match = TMDBMatch(
                    tmdb_id=result['tmdb_id'],
                    title=result['tmdb_title'],
                    original_title=None,
                    media_type='tv' if result.get('parsed_season') else 'movie',
                    year=result.get('parsed_year'),
                    poster_path=result.get('poster_url'),
                    season_number=result.get('parsed_season'),
                    episode_number=result.get('parsed_episode'),
                )
                dest_path = identifier.get_organized_path(match, base_path, result['filename'])
            else:
                dest_path = os.path.join(base_path, result['filename'])
        else:
            dest_path = os.path.join(base_path, result['filename'])

        # Create directory
        os.makedirs(os.path.dirname(dest_path), exist_ok=True)

        # Mark as downloading
        self.update_result_status(result_id, 'downloading')

        try:
            # Download
            download_result = client.download_file(
                result['download_url'],
                dest_path,
                progress_callback=progress_callback,
            )

            if download_result['success']:
                self.update_result_status(result_id, 'downloaded', dest_path)
                logger.info(f"Downloaded: {result['filename']} -> {dest_path}")
                return {
                    'success': True,
                    'path': dest_path,
                    'size': download_result.get('size', 0),
                }
            else:
                self.update_result_status(result_id, 'failed')
                return {
                    'success': False,
                    'message': download_result.get('message', 'Download failed'),
                }

        except Exception as e:
            logger.error(f"Download failed for result {result_id}: {e}")
            self.update_result_status(result_id, 'failed')
            return {
                'success': False,
                'message': str(e),
            }
