"""
TV Series Processor for NFOGuard
Handles TV series processing and episode management with async I/O support
"""
import os
import re
import asyncio
from pathlib import Path
from typing import Optional, Dict, List, Set, Tuple, Any
from datetime import datetime

from core.database import NFOGuardDatabase
from core.nfo_manager import NFOManager
from core.async_nfo_manager import AsyncNFOManager
from core.path_mapper import PathMapper
from clients.sonarr_client import SonarrClient
from clients.external_clients import ExternalClientManager
from config.settings import config
from utils.logging import _log
from utils.file_utils import (
    find_media_path_by_imdb_and_title,
    find_episodes_on_disk,
    extract_title_from_directory_name
)
from utils.async_file_utils import (
    async_find_episodes_on_disk,
    async_concurrent_episode_processing
)


class TVProcessor:
    """Handles TV series processing"""
    
    def __init__(self, db: NFOGuardDatabase, nfo_manager: NFOManager, path_mapper: PathMapper):
        self.db = db
        self.nfo_manager = nfo_manager
        self.async_nfo_manager = AsyncNFOManager(config.manager_brand, config.debug)
        self.path_mapper = path_mapper
        self.sonarr = SonarrClient(
            os.environ.get("SONARR_URL", ""),
            os.environ.get("SONARR_API_KEY", "")
        )
        self.external_clients = ExternalClientManager()
    
    def find_series_path(self, series_title: str, imdb_id: str, sonarr_path: str = None) -> Optional[Path]:
        """Find series directory path using unified file utilities"""
        return find_media_path_by_imdb_and_title(
            title=series_title,
            imdb_id=imdb_id,
            search_paths=config.tv_paths,
            webhook_path=sonarr_path,
            path_mapper=self.path_mapper
        )
    
    def process_series(self, series_path: Path) -> None:
        """Process a TV series directory"""
        imdb_id = self.nfo_manager.parse_imdb_from_path(series_path)
        if not imdb_id:
            _log("ERROR", f"No IMDb ID found in series path: {series_path}")
            return
        
        _log("INFO", f"Processing TV series: {series_path.name}")
        
        # Update database
        self.db.upsert_series(imdb_id, str(series_path))
        
        # Find video files
        disk_episodes = find_episodes_on_disk(series_path)
        _log("INFO", f"Found {len(disk_episodes)} episodes on disk")
        
        # Get episode dates
        episode_dates = self._gather_episode_dates(series_path, imdb_id, disk_episodes)
        
        # Process episodes
        for (season, episode), (aired, dateadded, source) in episode_dates.items():
            if (season, episode) in disk_episodes:
                # Create NFO
                if config.manage_nfo:
                    season_dir = series_path / config.tv_season_dir_format.format(season=season)
                    self.nfo_manager.create_episode_nfo(
                        season_dir,
                        season, episode, aired, dateadded, source, config.lock_metadata
                    )
                
                # Update file mtimes
                if config.fix_dir_mtimes and dateadded:
                    video_files = disk_episodes[(season, episode)]
                    for video_file in video_files:
                        self.nfo_manager.set_file_mtime(video_file, dateadded)
                
                # Save to database
                self.db.upsert_episode_date(imdb_id, season, episode, aired, dateadded, source, True)
        
        # Skip season.nfo and tvshow.nfo creation - focus only on episode NFOs
        pass
        
        _log("INFO", f"Completed processing TV series: {series_path.name}")
    
    def _extract_series_title_from_path(self, series_path: Path) -> Optional[str]:
        """Extract series title from directory path using unified file utilities"""
        return extract_title_from_directory_name(series_path.name)
    
    
    def _gather_episode_dates(self, series_path: Path, imdb_id: str, disk_episodes: Dict[Tuple[int, int], List[Path]]) -> Dict[Tuple[int, int], Tuple[Optional[str], Optional[str], str]]:
        """Gather episode air dates and date added information"""
        episode_dates = {}
        
        # Get data from Sonarr first if available
        sonarr_episodes = self._get_sonarr_episodes(imdb_id)
        
        # Get data from external sources for missing information
        for (season, episode) in disk_episodes:
            aired = None
            dateadded = None
            source = "unknown"
            
            # Try Sonarr first
            if (season, episode) in sonarr_episodes:
                sonarr_data = sonarr_episodes[(season, episode)]
                aired = sonarr_data.get('airDate')
                dateadded = sonarr_data.get('dateAdded')
                if dateadded:
                    source = "sonarr"
            
            # Fallback to external sources if needed
            if not aired:
                external_aired = self.external_clients.get_episode_air_date(imdb_id, season, episode)
                if external_aired:
                    aired = external_aired
                    if not dateadded:
                        source = "external"
            
            # Use air date as fallback for dateadded if available
            if not dateadded and aired:
                dateadded = aired
                source = f"{source}_fallback" if source != "unknown" else "aired_fallback"
            
            episode_dates[(season, episode)] = (aired, dateadded, source)
        
        return episode_dates
    
    def _get_sonarr_episodes(self, imdb_id: str) -> Dict[Tuple[int, int], Dict[str, Any]]:
        """Get episode information from Sonarr"""
        try:
            series_data = self.sonarr.get_series_by_imdb(imdb_id)
            if not series_data:
                return {}
            
            series_id = series_data.get('id')
            if not series_id:
                return {}
            
            episodes = self.sonarr.get_episodes(series_id)
            
            episode_map = {}
            for episode in episodes:
                season = episode.get('seasonNumber', 0)
                episode_num = episode.get('episodeNumber', 0)
                
                if season > 0 and episode_num > 0:
                    episode_map[(season, episode_num)] = {
                        'airDate': episode.get('airDate'),
                        'dateAdded': episode.get('episodeFile', {}).get('dateAdded') if episode.get('hasFile') else None
                    }
            
            return episode_map
            
        except Exception as e:
            _log("ERROR", f"Failed to get Sonarr episodes for {imdb_id}: {e}")
            return {}
    
    def process_season(self, series_path: str, season_name: str) -> Dict[str, Any]:
        """Process a specific season"""
        series_path_obj = Path(series_path)
        if not series_path_obj.exists():
            raise FileNotFoundError(f"Series path not found: {series_path}")
        
        season_path = series_path_obj / season_name
        if not season_path.exists():
            raise FileNotFoundError(f"Season directory not found: {season_path}")
        
        # Extract season number from directory name
        season_match = re.search(r'(\d+)', season_name)
        if not season_match:
            raise ValueError(f"Could not extract season number from: {season_name}")
        
        season_num = int(season_match.group(1))
        
        # Get series IMDb ID
        imdb_id = self.nfo_manager.parse_imdb_from_path(series_path_obj)
        if not imdb_id:
            raise ValueError(f"No IMDb ID found in series path: {series_path}")
        
        _log("INFO", f"Processing season {season_num} of series: {series_path_obj.name}")
        
        # Find episodes in this season
        disk_episodes = find_episodes_on_disk(series_path_obj)
        season_episodes = {k: v for k, v in disk_episodes.items() if k[0] == season_num}
        
        if not season_episodes:
            return {"status": "no_episodes", "season": season_num, "episodes_found": 0}
        
        # Get episode dates
        episode_dates = self._gather_episode_dates(series_path_obj, imdb_id, season_episodes)
        
        # Process episodes
        processed_count = 0
        for (season, episode), (aired, dateadded, source) in episode_dates.items():
            if (season, episode) in season_episodes:
                # Create NFO
                if config.manage_nfo:
                    self.nfo_manager.create_episode_nfo(
                        season_path,
                        season, episode, aired, dateadded, source, config.lock_metadata
                    )
                
                # Update file mtimes
                if config.fix_dir_mtimes and dateadded:
                    video_files = season_episodes[(season, episode)]
                    for video_file in video_files:
                        self.nfo_manager.set_file_mtime(video_file, dateadded)
                
                # Save to database
                self.db.upsert_episode_date(imdb_id, season, episode, aired, dateadded, source, True)
                processed_count += 1
        
        _log("INFO", f"Processed {processed_count} episodes in season {season_num}")
        
        return {
            "status": "success",
            "season": season_num,
            "episodes_found": len(season_episodes),
            "episodes_processed": processed_count
        }
    
    def process_episode(self, series_path: str, season_name: str, episode_name: str) -> Dict[str, Any]:
        """Process a specific episode"""
        series_path_obj = Path(series_path)
        if not series_path_obj.exists():
            raise FileNotFoundError(f"Series path not found: {series_path}")
        
        season_path = series_path_obj / season_name
        if not season_path.exists():
            raise FileNotFoundError(f"Season directory not found: {season_path}")
        
        episode_path = season_path / episode_name
        if not episode_path.exists():
            raise FileNotFoundError(f"Episode file not found: {episode_path}")
        
        # Extract season and episode numbers
        season_match = re.search(r'(\d+)', season_name)
        episode_match = re.search(r'[sS](\d+)[eE](\d+)|(\d+)x(\d+)', episode_name)
        
        if not season_match:
            raise ValueError(f"Could not extract season number from: {season_name}")
        
        if not episode_match:
            raise ValueError(f"Could not extract episode number from: {episode_name}")
        
        season_num = int(season_match.group(1))
        
        if episode_match.group(1) and episode_match.group(2):  # SxxExx format
            episode_num = int(episode_match.group(2))
        elif episode_match.group(3) and episode_match.group(4):  # NxNN format
            episode_num = int(episode_match.group(4))
        else:
            raise ValueError(f"Could not parse episode number from: {episode_name}")
        
        # Get series IMDb ID
        imdb_id = self.nfo_manager.parse_imdb_from_path(series_path_obj)
        if not imdb_id:
            raise ValueError(f"No IMDb ID found in series path: {series_path}")
        
        _log("INFO", f"Processing episode S{season_num:02d}E{episode_num:02d} of series: {series_path_obj.name}")
        
        # Get episode data
        disk_episodes = {(season_num, episode_num): [episode_path]}
        episode_dates = self._gather_episode_dates(series_path_obj, imdb_id, disk_episodes)
        
        if (season_num, episode_num) not in episode_dates:
            return {"status": "no_data", "season": season_num, "episode": episode_num}
        
        aired, dateadded, source = episode_dates[(season_num, episode_num)]
        
        # Create NFO
        if config.manage_nfo:
            self.nfo_manager.create_episode_nfo(
                season_path,
                season_num, episode_num, aired, dateadded, source, config.lock_metadata
            )
        
        # Update file mtime
        if config.fix_dir_mtimes and dateadded:
            self.nfo_manager.set_file_mtime(episode_path, dateadded)
        
        # Save to database
        self.db.upsert_episode_date(imdb_id, season_num, episode_num, aired, dateadded, source, True)
        
        _log("INFO", f"Processed episode S{season_num:02d}E{episode_num:02d}")
        
        return {
            "status": "success",
            "season": season_num,
            "episode": episode_num,
            "aired": aired,
            "dateadded": dateadded,
            "source": source
        }
    
    # ===== ASYNC METHODS =====
    
    async def async_process_series(self, series_path: Path) -> Dict[str, Any]:
        """
        Async process a TV series directory with concurrent episode processing
        
        Args:
            series_path: Path to series directory
            
        Returns:
            Dictionary with processing results
        """
        imdb_id = self.nfo_manager.parse_imdb_from_path(series_path)
        if not imdb_id:
            return {"status": "error", "reason": f"No IMDb ID found in series path: {series_path}"}
        
        _log("INFO", f"Async processing TV series: {series_path.name}")
        
        # Update database
        self.db.upsert_series(imdb_id, str(series_path))
        
        # Find video files asynchronously
        disk_episodes = await async_find_episodes_on_disk(series_path)
        _log("INFO", f"Found {len(disk_episodes)} episodes on disk")
        
        # Get episode dates (sync for now, could be made async later)
        episode_dates = self._gather_episode_dates(series_path, imdb_id, disk_episodes)
        
        # Prepare episode data for concurrent processing
        episode_data_list = []
        mtime_operations = []
        
        for (season, episode), (aired, dateadded, source) in episode_dates.items():
            if (season, episode) in disk_episodes:
                season_dir = series_path / config.tv_season_dir_format.format(season=season)
                
                # Prepare NFO creation data
                if config.manage_nfo:
                    episode_data_list.append({
                        'season_dir': season_dir,
                        'season': season,
                        'episode': episode,
                        'aired': aired,
                        'dateadded': dateadded,
                        'source': source,
                        'lock_metadata': config.lock_metadata
                    })
                
                # Prepare mtime operations
                if config.fix_dir_mtimes and dateadded:
                    video_files = disk_episodes[(season, episode)]
                    for video_file in video_files:
                        mtime_operations.append((video_file, dateadded))
                
                # Save to database
                self.db.upsert_episode_date(imdb_id, season, episode, aired, dateadded, source, True)
        
        # Process NFOs and mtimes concurrently
        results = {}
        
        if episode_data_list:
            _log("INFO", f"Creating {len(episode_data_list)} episode NFOs concurrently")
            nfo_results = await self.async_nfo_manager.async_batch_create_episode_nfos(
                episode_data_list,
                max_concurrent=config.max_concurrent
            )
            results['nfo_created'] = sum(nfo_results)
            results['nfo_failed'] = len(nfo_results) - sum(nfo_results)
        
        if mtime_operations:
            _log("INFO", f"Setting mtimes for {len(mtime_operations)} files concurrently")
            mtime_results = await self.async_nfo_manager.async_batch_set_file_mtimes(
                mtime_operations,
                max_concurrent=10
            )
            results['mtime_updated'] = sum(mtime_results)
            results['mtime_failed'] = len(mtime_results) - sum(mtime_results)
        
        _log("INFO", f"Completed async processing TV series: {series_path.name}")
        
        return {
            "status": "success",
            "imdb_id": imdb_id,
            "episodes_found": len(disk_episodes),
            "episodes_processed": len(episode_data_list),
            "results": results
        }
    
    async def async_process_multiple_series(
        self,
        series_paths: List[Path],
        max_concurrent: int = 2
    ) -> List[Dict[str, Any]]:
        """
        Process multiple TV series concurrently
        
        Args:
            series_paths: List of series directory paths
            max_concurrent: Maximum concurrent series processing
            
        Returns:
            List of processing results for each series
        """
        semaphore = asyncio.Semaphore(max_concurrent)
        
        async def _process_series_with_semaphore(series_path: Path) -> Dict[str, Any]:
            async with semaphore:
                try:
                    return await self.async_process_series(series_path)
                except Exception as e:
                    _log("ERROR", f"Failed to process series {series_path}: {e}")
                    return {"status": "error", "path": str(series_path), "reason": str(e)}
        
        _log("INFO", f"Processing {len(series_paths)} series with max {max_concurrent} concurrent")
        
        tasks = [_process_series_with_semaphore(path) for path in series_paths]
        results = await asyncio.gather(*tasks, return_exceptions=True)
        
        # Filter out exceptions and convert to proper results
        processed_results = []
        for i, result in enumerate(results):
            if isinstance(result, Exception):
                processed_results.append({
                    "status": "error",
                    "path": str(series_paths[i]),
                    "reason": str(result)
                })
            else:
                processed_results.append(result)
        
        return processed_results
    
    async def async_batch_episode_processing(
        self,
        episodes_data: List[Dict[str, Any]],
        max_concurrent: int = 5
    ) -> Dict[str, Any]:
        """
        Process episodes from webhook data concurrently
        
        Args:
            episodes_data: List of episode data from webhooks
            max_concurrent: Maximum concurrent episode processing
            
        Returns:
            Processing results summary
        """
        async def _process_single_episode(episode_data: Dict[str, Any]) -> Dict[str, Any]:
            try:
                # Extract episode information
                series_path = Path(episode_data.get('series_path'))
                season = episode_data.get('season')
                episode = episode_data.get('episode')
                aired = episode_data.get('aired')
                dateadded = episode_data.get('dateadded')
                
                # Get IMDb ID
                imdb_id = self.nfo_manager.parse_imdb_from_path(series_path)
                if not imdb_id:
                    return {"status": "error", "reason": "No IMDb ID found"}
                
                # Create NFO if needed
                nfo_success = True
                if config.manage_nfo:
                    season_dir = series_path / config.tv_season_dir_format.format(season=season)
                    nfo_success = await self.async_nfo_manager.async_create_episode_nfo(
                        season_dir, season, episode, aired, dateadded, "webhook", config.lock_metadata
                    )
                
                # Update database
                self.db.upsert_episode_date(imdb_id, season, episode, aired, dateadded, "webhook", True)
                
                return {
                    "status": "success",
                    "season": season,
                    "episode": episode,
                    "nfo_created": nfo_success
                }
                
            except Exception as e:
                return {
                    "status": "error",
                    "episode_data": episode_data,
                    "reason": str(e)
                }
        
        _log("INFO", f"Processing {len(episodes_data)} episodes concurrently")
        
        results = await async_concurrent_episode_processing(
            episodes_data,
            _process_single_episode,
            max_concurrent
        )
        
        # Summarize results
        successful = sum(1 for r in results if r and r.get("status") == "success")
        failed = len(results) - successful
        
        return {
            "total_episodes": len(episodes_data),
            "successful": successful,
            "failed": failed,
            "results": results
        }