"""Plex Media Server client for linking appearances to library items"""
import asyncio
import uuid
from typing import Dict, List, Optional, Any
from web.backend.core.http_client import http_client
from modules.universal_logger import get_logger

logger = get_logger('Plex')

# Plex API constants
PLEX_TV_API = "https://plex.tv/api/v2"
PLEX_AUTH_URL = "https://app.plex.tv/auth"
CLIENT_IDENTIFIER = "media-downloader-appearances"
PRODUCT_NAME = "Media Downloader"


class PlexOAuth:
    """Handle Plex OAuth PIN-based authentication flow"""

    def __init__(self):
        self._headers = {
            'Accept': 'application/json',
            'X-Plex-Client-Identifier': CLIENT_IDENTIFIER,
            'X-Plex-Product': PRODUCT_NAME,
            'X-Plex-Version': '1.0.0',
            'X-Plex-Device': 'Web',
            'X-Plex-Platform': 'Web',
        }

    async def create_pin(self) -> Optional[Dict]:
        """
        Create a new PIN for authentication.

        Returns:
            Dict with 'id', 'code', and 'auth_url' or None on failure
        """
        try:
            url = f"{PLEX_TV_API}/pins"
            response = await http_client.post(
                url,
                headers=self._headers,
                data={'strong': 'true'}
            )
            data = response.json()

            pin_id = data.get('id')
            pin_code = data.get('code')

            if pin_id and pin_code:
                # Build the auth URL for the user to visit
                auth_url = (
                    f"{PLEX_AUTH_URL}#?"
                    f"clientID={CLIENT_IDENTIFIER}&"
                    f"code={pin_code}&"
                    f"context%5Bdevice%5D%5Bproduct%5D={PRODUCT_NAME.replace(' ', '%20')}"
                )

                logger.info(f"Created Plex PIN {pin_id}")
                return {
                    'id': pin_id,
                    'code': pin_code,
                    'auth_url': auth_url,
                    'expires_at': data.get('expiresAt'),
                }

            return None

        except Exception as e:
            logger.error(f"Failed to create Plex PIN: {e}")
            return None

    async def check_pin(self, pin_id: int) -> Optional[str]:
        """
        Check if the user has authenticated with the PIN.

        Args:
            pin_id: The PIN ID returned from create_pin

        Returns:
            The auth token if authenticated, None if still pending or expired
        """
        try:
            url = f"{PLEX_TV_API}/pins/{pin_id}"
            response = await http_client.get(url, headers=self._headers)
            data = response.json()

            auth_token = data.get('authToken')
            if auth_token:
                logger.info("Plex authentication successful")
                return auth_token

            return None

        except Exception as e:
            logger.error(f"Failed to check Plex PIN: {e}")
            return None

    async def wait_for_auth(self, pin_id: int, timeout: int = 120, poll_interval: int = 2) -> Optional[str]:
        """
        Poll for authentication completion.

        Args:
            pin_id: The PIN ID to check
            timeout: Maximum seconds to wait
            poll_interval: Seconds between checks

        Returns:
            The auth token if successful, None on timeout/failure
        """
        elapsed = 0
        while elapsed < timeout:
            token = await self.check_pin(pin_id)
            if token:
                return token

            await asyncio.sleep(poll_interval)
            elapsed += poll_interval

        logger.warning(f"Plex authentication timed out after {timeout}s")
        return None

    async def get_user_info(self, token: str) -> Optional[Dict]:
        """
        Get information about the authenticated user.

        Args:
            token: Plex auth token

        Returns:
            User info dict or None
        """
        try:
            url = f"{PLEX_TV_API}/user"
            headers = {**self._headers, 'X-Plex-Token': token}
            response = await http_client.get(url, headers=headers)
            data = response.json()

            return {
                'username': data.get('username'),
                'email': data.get('email'),
                'thumb': data.get('thumb'),
                'title': data.get('title'),
            }

        except Exception as e:
            logger.error(f"Failed to get Plex user info: {e}")
            return None

    async def get_user_servers(self, token: str) -> List[Dict]:
        """
        Get list of Plex servers available to the user.

        Args:
            token: Plex auth token

        Returns:
            List of server dictionaries
        """
        try:
            url = f"{PLEX_TV_API}/resources"
            headers = {**self._headers, 'X-Plex-Token': token}
            params = {'includeHttps': 1, 'includeRelay': 1}
            response = await http_client.get(url, headers=headers, params=params)
            data = response.json()

            servers = []
            for resource in data:
                if resource.get('provides') == 'server':
                    connections = resource.get('connections', [])

                    # Prefer non-local (relay/remote) connections for server-to-server communication
                    # Local connections often use internal IPs that aren't reachable externally
                    remote_conn = next((c for c in connections if not c.get('local') and c.get('relay')), None)
                    https_conn = next((c for c in connections if not c.get('local') and 'https' in c.get('uri', '')), None)
                    any_remote = next((c for c in connections if not c.get('local')), None)
                    local_conn = next((c for c in connections if c.get('local')), None)

                    # Try in order: relay, https remote, any remote, local
                    best_conn = remote_conn or https_conn or any_remote or local_conn or (connections[0] if connections else None)

                    if best_conn:
                        # Also include all connection URLs for debugging/manual selection
                        all_urls = [{'url': c.get('uri'), 'local': c.get('local', False), 'relay': c.get('relay', False)} for c in connections]

                        servers.append({
                            'name': resource.get('name'),
                            'machineIdentifier': resource.get('clientIdentifier'),
                            'owned': resource.get('owned', False),
                            'url': best_conn.get('uri'),
                            'local': best_conn.get('local', False),
                            'relay': best_conn.get('relay', False),
                            'accessToken': resource.get('accessToken'),
                            'all_connections': all_urls,
                        })

            return servers

        except Exception as e:
            logger.error(f"Failed to get Plex servers: {e}")
            return []


class PlexClient:
    """Client for interacting with Plex Media Server API"""

    def __init__(self, base_url: str, token: str):
        """
        Initialize Plex client.

        Args:
            base_url: Plex server URL (e.g., 'http://192.168.1.100:32400')
            token: Plex authentication token
        """
        self.base_url = base_url.rstrip('/')
        self.token = token
        self._headers = {
            'X-Plex-Token': token,
            'Accept': 'application/json'
        }

    async def test_connection(self) -> bool:
        """
        Test connection to Plex server.

        Returns:
            True if connection successful, False otherwise
        """
        try:
            url = f"{self.base_url}/identity"
            response = await http_client.get(url, headers=self._headers)
            data = response.json()
            server_name = data.get('MediaContainer', {}).get('friendlyName', 'Unknown')
            logger.info(f"Connected to Plex server: {server_name}")
            return True
        except Exception as e:
            logger.error(f"Plex connection test failed: {e}")
            return False

    async def get_libraries(self) -> List[Dict]:
        """
        Get list of Plex libraries.

        Returns:
            List of library dictionaries with id, title, type
        """
        try:
            url = f"{self.base_url}/library/sections"
            response = await http_client.get(url, headers=self._headers)
            data = response.json()

            libraries = []
            for section in data.get('MediaContainer', {}).get('Directory', []):
                libraries.append({
                    'id': section.get('key'),
                    'title': section.get('title'),
                    'type': section.get('type'),  # 'movie', 'show', 'artist', etc.
                    'uuid': section.get('uuid'),
                })
            return libraries
        except Exception as e:
            logger.error(f"Failed to get Plex libraries: {e}")
            return []

    async def search_by_tmdb_id(self, tmdb_id: int, media_type: str = 'movie') -> Optional[Dict]:
        """
        Search for an item in Plex library by TMDB ID.

        Args:
            tmdb_id: The Movie Database ID
            media_type: 'movie' or 'show'

        Returns:
            Plex item dict with ratingKey, title, etc. or None if not found
        """
        try:
            # Plex uses guid format like: tmdb://12345
            guid = f"tmdb://{tmdb_id}"

            # Search across all libraries
            url = f"{self.base_url}/library/all"
            params = {
                'guid': guid,
                'type': 1 if media_type == 'movie' else 2  # 1=movie, 2=show
            }
            response = await http_client.get(url, headers=self._headers, params=params)
            data = response.json()

            items = data.get('MediaContainer', {}).get('Metadata', [])
            if items:
                item = items[0]
                return {
                    'ratingKey': item.get('ratingKey'),
                    'title': item.get('title'),
                    'year': item.get('year'),
                    'thumb': item.get('thumb'),
                    'type': item.get('type'),
                    'librarySectionID': item.get('librarySectionID'),
                }
            return None
        except Exception as e:
            logger.debug(f"TMDB search failed for {tmdb_id}: {e}")
            return None

    async def search_by_title(self, title: str, year: Optional[int] = None,
                              media_type: str = 'movie') -> Optional[Dict]:
        """
        Search for an item in Plex library by title.

        Args:
            title: Movie or show title
            year: Optional release year for more accurate matching
            media_type: 'movie' or 'show'

        Returns:
            Plex item dict or None if not found
        """
        try:
            url = f"{self.base_url}/search"
            params = {
                'query': title,
                'type': 1 if media_type == 'movie' else 2
            }
            response = await http_client.get(url, headers=self._headers, params=params)
            data = response.json()

            items = data.get('MediaContainer', {}).get('Metadata', [])

            # If year provided, filter for matching year
            if year and items:
                for item in items:
                    if item.get('year') == year:
                        return {
                            'ratingKey': item.get('ratingKey'),
                            'title': item.get('title'),
                            'year': item.get('year'),
                            'thumb': item.get('thumb'),
                            'type': item.get('type'),
                            'librarySectionID': item.get('librarySectionID'),
                        }

            # Return first result if no exact year match
            if items:
                item = items[0]
                return {
                    'ratingKey': item.get('ratingKey'),
                    'title': item.get('title'),
                    'year': item.get('year'),
                    'thumb': item.get('thumb'),
                    'type': item.get('type'),
                    'librarySectionID': item.get('librarySectionID'),
                }
            return None
        except Exception as e:
            logger.debug(f"Title search failed for '{title}': {e}")
            return None

    async def get_episode(self, show_rating_key: str, season: int, episode: int) -> Optional[Dict]:
        """
        Get a specific episode from a TV show.

        Args:
            show_rating_key: Plex ratingKey for the show
            season: Season number
            episode: Episode number

        Returns:
            Episode dict with ratingKey, title, etc. or None if not found
        """
        try:
            # Get all episodes of the show
            url = f"{self.base_url}/library/metadata/{show_rating_key}/allLeaves"
            response = await http_client.get(url, headers=self._headers)
            data = response.json()

            episodes = data.get('MediaContainer', {}).get('Metadata', [])
            for ep in episodes:
                if ep.get('parentIndex') == season and ep.get('index') == episode:
                    return {
                        'ratingKey': ep.get('ratingKey'),
                        'title': ep.get('title'),
                        'season': season,
                        'episode': episode,
                        'show_rating_key': show_rating_key,
                        'type': 'episode',
                    }
            return None
        except Exception as e:
            logger.debug(f"Episode search failed for S{season}E{episode}: {e}")
            return None

    async def get_all_episodes(self, show_rating_key: str) -> Dict[tuple, Dict]:
        """
        Get all episodes for a TV show, indexed by (season, episode) tuple.

        Args:
            show_rating_key: Plex ratingKey for the show

        Returns:
            Dict mapping (season_num, episode_num) to episode info
        """
        episodes_map = {}
        try:
            url = f"{self.base_url}/library/metadata/{show_rating_key}/allLeaves"
            response = await http_client.get(url, headers=self._headers)
            data = response.json()

            episodes = data.get('MediaContainer', {}).get('Metadata', [])
            for ep in episodes:
                season = ep.get('parentIndex')
                episode = ep.get('index')
                if season is not None and episode is not None:
                    episodes_map[(season, episode)] = {
                        'ratingKey': ep.get('ratingKey'),
                        'title': ep.get('title'),
                        'season': season,
                        'episode': episode,
                        'show_rating_key': show_rating_key,
                        'air_date': ep.get('originallyAvailableAt'),
                    }

            logger.debug(f"Found {len(episodes_map)} episodes for show {show_rating_key}")
            return episodes_map

        except Exception as e:
            logger.debug(f"Failed to get episodes for show {show_rating_key}: {e}")
            return {}

    def get_watch_url(self, rating_key: str) -> str:
        """
        Generate a direct watch URL for a Plex item.

        Args:
            rating_key: Plex ratingKey for the item

        Returns:
            URL to open the item in Plex Web
        """
        # Extract server machine identifier from base URL or use a generic format
        # Plex Web URL format: /web/index.html#!/server/{machineId}/details?key=/library/metadata/{ratingKey}
        return f"{self.base_url}/web/index.html#!/server/1/details?key=%2Flibrary%2Fmetadata%2F{rating_key}"

    async def get_server_identity(self) -> Optional[Dict]:
        """
        Get Plex server identity including machine identifier.

        Returns:
            Server identity dict or None
        """
        try:
            url = f"{self.base_url}/identity"
            response = await http_client.get(url, headers=self._headers)
            data = response.json()
            container = data.get('MediaContainer', {})
            return {
                'machineIdentifier': container.get('machineIdentifier'),
                'friendlyName': container.get('friendlyName'),
                'version': container.get('version'),
            }
        except Exception as e:
            logger.error(f"Failed to get server identity: {e}")
            return None

    def get_full_watch_url(self, rating_key: str, machine_id: str) -> str:
        """
        Generate a complete Plex watch URL with machine identifier.

        Args:
            rating_key: Plex ratingKey for the item
            machine_id: Plex server machine identifier

        Returns:
            Complete Plex Web URL
        """
        encoded_key = f"%2Flibrary%2Fmetadata%2F{rating_key}"
        return f"{self.base_url}/web/index.html#!/server/{machine_id}/details?key={encoded_key}"

    async def search_by_actor(self, actor_name: str) -> List[Dict]:
        """
        Search Plex library for all movies and TV shows featuring an actor.

        Uses Plex's actor filter to find all content with the actor in cast.

        Args:
            actor_name: Name of the actor to search for

        Returns:
            List of appearances with show/movie info and role details
        """
        appearances = []
        seen_keys = set()  # Track to avoid duplicates
        actor_name_lower = actor_name.lower()

        try:
            # Get all libraries
            libraries = await self.get_libraries()

            for library in libraries:
                lib_key = library.get('id')
                lib_type = library.get('type')

                # Only search movie and show libraries
                if lib_type not in ('movie', 'show'):
                    continue

                try:
                    # Use actor filter to find all content featuring this actor
                    # This is the most reliable method in Plex
                    url = f"{self.base_url}/library/sections/{lib_key}/all"
                    params = {
                        'type': 1 if lib_type == 'movie' else 2,  # 1=movie, 2=show
                        'actor': actor_name,  # Plex accepts actor name directly
                    }

                    response = await http_client.get(url, headers=self._headers, params=params)
                    data = response.json()
                    items = data.get('MediaContainer', {}).get('Metadata', [])

                    logger.debug(f"Found {len(items)} {lib_type}s for '{actor_name}' in library {library.get('title')}")

                    for item in items:
                        rating_key = item.get('ratingKey')
                        if not rating_key or rating_key in seen_keys:
                            continue

                        seen_keys.add(rating_key)

                        # Get detailed metadata for character name
                        detail_url = f"{self.base_url}/library/metadata/{rating_key}"
                        detail_response = await http_client.get(detail_url, headers=self._headers)
                        detail_data = detail_response.json()
                        detail_items = detail_data.get('MediaContainer', {}).get('Metadata', [])

                        if not detail_items:
                            continue

                        detail = detail_items[0]

                        # Find the actor's role/character name
                        character_name = None
                        roles = detail.get('Role', [])
                        for role in roles:
                            role_tag = (role.get('tag') or '').lower()
                            if actor_name_lower in role_tag or role_tag in actor_name_lower:
                                character_name = role.get('role')
                                break

                        # Build poster URL with auth token
                        thumb = detail.get('thumb')
                        poster_url = None
                        if thumb:
                            poster_url = f"{self.base_url}{thumb}?X-Plex-Token={self.token}"

                        # Build appearance data
                        appearance = {
                            'appearance_type': 'Movie' if lib_type == 'movie' else 'TV',
                            'show_name': detail.get('title'),
                            'episode_title': None,
                            'network': detail.get('studio'),
                            'appearance_date': detail.get('originallyAvailableAt'),
                            'year': detail.get('year'),
                            'status': 'aired',
                            'description': detail.get('summary'),
                            'poster_url': poster_url,
                            'credit_type': 'acting',
                            'character_name': character_name,
                            'plex_rating_key': rating_key,
                            'plex_library_id': lib_key,
                            'source': 'plex',
                        }

                        # For TV shows, get episode count
                        if lib_type == 'show':
                            appearance['episode_count'] = detail.get('leafCount', 1)

                        appearances.append(appearance)
                        logger.info(f"Found Plex appearance: {actor_name} in '{detail.get('title')}'" +
                                   (f" as {character_name}" if character_name else ""))

                        # Small delay between detail requests
                        await asyncio.sleep(0.02)

                except Exception as e:
                    logger.debug(f"Error searching library {lib_key}: {e}")
                    continue

            logger.info(f"Found {len(appearances)} Plex appearances for {actor_name}")
            return appearances

        except Exception as e:
            logger.error(f"Failed to search Plex by actor: {e}")
            return []

    async def batch_match_appearances(self, appearances: List[Dict], on_match=None) -> Dict[int, Dict]:
        """
        Match multiple appearances to Plex library items.

        Args:
            appearances: List of appearance dicts with tmdb_show_id or tmdb_movie_id
            on_match: Optional async callback(appearance_id, match_info) called for each match

        Returns:
            Dict mapping appearance ID to Plex match info {rating_key, library_id}
        """
        matches = {}
        server_info = await self.get_server_identity()
        machine_id = server_info.get('machineIdentifier') if server_info else None

        # Dedupe by TMDB ID to avoid redundant searches
        tmdb_cache: Dict[tuple, Optional[Dict]] = {}
        # Cache episode lookups per show
        episode_cache: Dict[str, Dict[tuple, Optional[Dict]]] = {}

        for appearance in appearances:
            appearance_id = appearance.get('id')
            if not appearance_id:
                continue

            # Determine media type and TMDB ID
            tmdb_id = appearance.get('tmdb_movie_id') or appearance.get('tmdb_show_id')
            is_movie = appearance.get('appearance_type') == 'Movie'
            media_type = 'movie' if is_movie else 'show'

            if not tmdb_id:
                continue

            cache_key = (tmdb_id, media_type)

            # Check cache first
            if cache_key in tmdb_cache:
                plex_item = tmdb_cache[cache_key]
            else:
                # Rate limiting
                await asyncio.sleep(0.1)

                # Try TMDB ID first
                plex_item = await self.search_by_tmdb_id(tmdb_id, media_type)

                # Fall back to title search if no TMDB match
                if not plex_item:
                    title = appearance.get('movie_name') or appearance.get('show_name')
                    year = None
                    if appearance.get('release_date'):
                        try:
                            year = int(appearance['release_date'][:4])
                        except (ValueError, TypeError):
                            pass
                    if title:
                        plex_item = await self.search_by_title(title, year, media_type)

                tmdb_cache[cache_key] = plex_item

            if plex_item:
                show_rating_key = plex_item.get('ratingKey')  # Always the show/movie key
                rating_key = show_rating_key if is_movie else None  # Movies get the key, TV starts with None
                library_id = plex_item.get('librarySectionID')

                # For TV shows with season/episode data, try to match the specific episode
                season = appearance.get('season_number')
                episode = appearance.get('episode_number')
                if not is_movie and season and episode:
                    # Check episode cache first
                    show_key = str(show_rating_key)
                    ep_key = (season, episode)
                    if show_key not in episode_cache:
                        episode_cache[show_key] = {}

                    if ep_key in episode_cache[show_key]:
                        episode_item = episode_cache[show_key][ep_key]
                    else:
                        episode_item = await self.get_episode(show_rating_key, season, episode)
                        episode_cache[show_key][ep_key] = episode_item

                    if episode_item:
                        rating_key = episode_item.get('ratingKey')  # Episode-specific key
                    # If episode not found, rating_key stays None - episode not in Plex

                match_info = {
                    'plex_rating_key': rating_key,  # Episode key if found, movie key for movies, None for missing TV episodes
                    'plex_show_rating_key': show_rating_key if not is_movie else None,  # Show key for TV (for series-level navigation)
                    'plex_library_id': library_id,
                    'plex_watch_url': self.get_full_watch_url(rating_key, machine_id) if (rating_key and machine_id) else (self.get_watch_url(rating_key) if rating_key else None),
                }
                matches[appearance_id] = match_info

                # Call the on_match callback for real-time updates
                if on_match:
                    await on_match(appearance_id, match_info)

        logger.info(f"Matched {len(matches)} of {len(appearances)} appearances to Plex library")
        return matches
