from typing import Optional, Dict, Any
import json
import re
from datetime import datetime, timedelta
from difflib import SequenceMatcher
from utils.logging_config import get_logger
from core.musicbrainz_client import MusicBrainzClient
from database.music_database import MusicDatabase

logger = get_logger("musicbrainz_service")

class MusicBrainzService:
    """Service layer for MusicBrainz integration with caching and matching logic"""
    
    def __init__(self, database: MusicDatabase, app_name: str = "SoulSync", app_version: str = "1.0", contact_email: str = ""):
        self.db = database
        self.mb_client = MusicBrainzClient(app_name, app_version, contact_email)
        self.retry_days = 30  # Retry 'not_found' items after 30 days
    
    def _calculate_similarity(self, str1: str, str2: str) -> float:
        """Calculate string similarity score (0.0 to 1.0)"""
        if not str1 or not str2:
            return 0.0
        
        # Normalize for comparison
        s1 = str1.lower().strip()
        s2 = str2.lower().strip()
        
        if s1 == s2:
            return 1.0
        
        return SequenceMatcher(None, s1, s2).ratio()
    
    def _check_cache(self, entity_type: str, entity_name: str, artist_name: Optional[str] = None) -> Optional[Dict[str, Any]]:
        """Check if we have a cached MusicBrainz result"""
        conn = None
        try:
            conn = self.db._get_connection()
            cursor = conn.cursor()
            
            # Fix: Match exact artist_name (not OR artist_name IS NULL)
            # This prevents getting wrong cached results
            if artist_name is not None:
                cursor.execute("""
                    SELECT musicbrainz_id, metadata_json, match_confidence, last_updated
                    FROM musicbrainz_cache
                    WHERE entity_type = ? AND entity_name = ? AND artist_name = ?
                    ORDER BY last_updated DESC
                    LIMIT 1
                """, (entity_type, entity_name, artist_name))
            else:
                cursor.execute("""
                    SELECT musicbrainz_id, metadata_json, match_confidence, last_updated
                    FROM musicbrainz_cache
                    WHERE entity_type = ? AND entity_name = ? AND artist_name IS NULL
                    ORDER BY last_updated DESC
                    LIMIT 1
                """, (entity_type, entity_name))
            
            row = cursor.fetchone()
            
            if row:
                # Shorter TTL for null results (failed lookups) so they get retried sooner
                last_updated = datetime.fromisoformat(row[3]) if row[3] else None
                ttl_days = 30 if row[0] is None else 90  # row[0] is musicbrainz_id
                if last_updated and (datetime.now() - last_updated).days > ttl_days:
                    logger.debug(f"Cache entry for {entity_type} '{entity_name}' is stale (> {ttl_days} days)")
                    return None
                
                # Parse JSON with error handling
                try:
                    metadata = json.loads(row[1]) if row[1] else None
                except json.JSONDecodeError:
                    logger.warning(f"Invalid JSON in cache for {entity_type} '{entity_name}', ignoring")
                    metadata = None
                
                return {
                    'musicbrainz_id': row[0],
                    'metadata': metadata,
                    'confidence': row[2]
                }
            
            return None
            
        except Exception as e:
            logger.error(f"Error checking cache: {e}")
            return None
        finally:
            if conn:
                conn.close()
    
    def _save_to_cache(self, entity_type: str, entity_name: str, artist_name: Optional[str],
                       musicbrainz_id: Optional[str], metadata: Optional[Dict], confidence: int):
        """Save MusicBrainz result to cache"""
        conn = None
        try:
            conn = self.db._get_connection()
            cursor = conn.cursor()

            metadata_json = json.dumps(metadata) if metadata else None

            cursor.execute("""
                INSERT OR REPLACE INTO musicbrainz_cache
                (entity_type, entity_name, artist_name, musicbrainz_id, metadata_json, match_confidence, last_updated)
                VALUES (?, ?, ?, ?, ?, ?, ?)
            """, (entity_type, entity_name, artist_name, musicbrainz_id, metadata_json, confidence, datetime.now()))

            conn.commit()

            logger.debug(f"Cached {entity_type} '{entity_name}' (MBID: {musicbrainz_id}, confidence: {confidence})")

        except Exception as e:
            logger.error(f"Error saving to cache: {e}")
            if conn:
                conn.rollback()
        finally:
            if conn:
                conn.close()
    
    def match_artist(self, artist_name: str) -> Optional[Dict[str, Any]]:
        """
        Match an artist by name to MusicBrainz
        
        Returns:
            Dict with 'mbid', 'name', 'confidence' or None if no good match
        """
        # Check cache first
        cached = self._check_cache('artist', artist_name)
        if cached:
            logger.debug(f"Cache hit for artist '{artist_name}'")
            return {
                'mbid': cached['musicbrainz_id'],
                'name': artist_name,
                'confidence': cached['confidence'],
                'cached': True
            }
        
        # Search MusicBrainz
        try:
            results = self.mb_client.search_artist(artist_name, limit=5)
            
            if not results:
                logger.info(f"No MusicBrainz results for artist '{artist_name}'")
                self._save_to_cache('artist', artist_name, None, None, None, 0)
                return None
            
            # Find best match
            best_match = None
            best_confidence = 0
            
            for result in results:
                mb_name = result.get('name', '')
                mb_score = result.get('score', 0)  # MusicBrainz search score
                
                # Calculate our own similarity
                similarity = self._calculate_similarity(artist_name, mb_name)
                
                # Combine MusicBrainz score with our similarity (weighted)
                # Cap at 100 to prevent edge cases where MB score > 100
                confidence = min(100, int((similarity * 60) + (mb_score / 100 * 40)))
                
                if confidence > best_confidence:
                    best_confidence = confidence
                    best_match = result
            
            # Only return matches with confidence >= 70%
            if best_match and best_confidence >= 70:
                mbid = best_match.get('id')
                mb_name = best_match.get('name')
                
                # Save to cache
                self._save_to_cache('artist', artist_name, None, mbid, best_match, best_confidence)
                
                logger.info(f"Matched artist '{artist_name}' → '{mb_name}' (MBID: {mbid}, confidence: {best_confidence})")
                
                return {
                    'mbid': mbid,
                    'name': mb_name,
                    'confidence': best_confidence,
                    'cached': False
                }
            else:
                logger.info(f"Low confidence match for artist '{artist_name}' (best: {best_confidence})")
                self._save_to_cache('artist', artist_name, None, None, None, best_confidence)
                return None
                
        except Exception as e:
            logger.error(f"Error matching artist '{artist_name}': {e}")
            return None
    
    # Version qualifiers that distinguish releases (Deluxe, Remastered, etc.)
    _VERSION_QUALIFIERS = re.compile(
        r'\b(deluxe|expanded|remaster(?:ed)?|anniversary|special|collector|'
        r'limited|bonus|platinum|gold|super\s*deluxe|standard)\b',
        re.IGNORECASE
    )

    def _extract_version_qualifier(self, title: str) -> str:
        """Extract version qualifiers from an album title, normalized and sorted."""
        qualifiers = sorted(set(q.lower() for q in self._VERSION_QUALIFIERS.findall(title)))
        return ' '.join(qualifiers)

    def match_release(self, album_name: str, artist_name: Optional[str] = None) -> Optional[Dict[str, Any]]:
        """
        Match a release (album) by name to MusicBrainz

        Returns:
            Dict with 'mbid', 'title', 'confidence' or None if no good match
        """
        # Check cache first
        cached = self._check_cache('release', album_name, artist_name)
        if cached:
            logger.debug(f"Cache hit for release '{album_name}'")
            return {
                'mbid': cached['musicbrainz_id'],
                'title': album_name,
                'confidence': cached['confidence'],
                'cached': True
            }

        # Search MusicBrainz
        try:
            results = self.mb_client.search_release(album_name, artist_name, limit=5)

            if not results:
                logger.info(f"No MusicBrainz results for release '{album_name}'")
                self._save_to_cache('release', album_name, artist_name, None, None, 0)
                return None

            # Extract version qualifier from search query for preference matching
            query_qualifier = self._extract_version_qualifier(album_name)

            # Find best match
            best_match = None
            best_confidence = 0

            for result in results:
                mb_title = result.get('title', '')
                mb_score = result.get('score', 0)

                # Calculate title similarity
                title_similarity = self._calculate_similarity(album_name, mb_title)

                # If we have artist info, check artist match too
                artist_bonus = 0
                if artist_name and 'artist-credit' in result:
                    artist_credits = result['artist-credit']
                    for credit in artist_credits:
                        if isinstance(credit, dict) and 'artist' in credit:
                            mb_artist = credit['artist'].get('name', '')
                            artist_similarity = self._calculate_similarity(artist_name, mb_artist)
                            if artist_similarity > 0.7:
                                artist_bonus = 20
                                break

                # Version qualifier matching: prefer releases with the same
                # edition qualifier (Deluxe, Remastered, etc.) as the query.
                # This prevents "Playing the Angel (Deluxe)" from matching the
                # standard "Playing the Angel" release.
                version_bonus = 0
                if query_qualifier:
                    mb_qualifier = self._extract_version_qualifier(mb_title)
                    if query_qualifier == mb_qualifier:
                        version_bonus = 10  # Same edition — strong preference
                    elif mb_qualifier and mb_qualifier in query_qualifier:
                        version_bonus = 5   # Partial match (e.g. "deluxe" in "super deluxe")
                    elif not mb_qualifier:
                        version_bonus = -5  # Query has qualifier but result doesn't — penalize

                # Combine scores - cap at 100
                confidence = min(100, int((title_similarity * 50) + (mb_score / 100 * 30) + artist_bonus + version_bonus))

                if confidence > best_confidence:
                    best_confidence = confidence
                    best_match = result
            
            # Only return matches with confidence >= 70%
            if best_match and best_confidence >= 70:
                mbid = best_match.get('id')
                mb_title = best_match.get('title')
                
                # Save to cache
                self._save_to_cache('release', album_name, artist_name, mbid, best_match, best_confidence)
                
                logger.info(f"Matched release '{album_name}' → '{mb_title}' (MBID: {mbid}, confidence: {best_confidence})")
                
                return {
                    'mbid': mbid,
                    'title': mb_title,
                    'confidence': best_confidence,
                    'cached': False
                }
            else:
                logger.info(f"Low confidence match for release '{album_name}' (best: {best_confidence})")
                self._save_to_cache('release', album_name, artist_name, None, None, best_confidence)
                return None
                
        except Exception as e:
            logger.error(f"Error matching release '{album_name}': {e}")
            return None
    
    def match_recording(self, track_name: str, artist_name: Optional[str] = None) -> Optional[Dict[str, Any]]:
        """
        Match a recording (track) by name to MusicBrainz
        
        Returns:
            Dict with 'mbid', 'title', 'confidence' or None if no good match
        """
        # Check cache first
        cached = self._check_cache('recording', track_name, artist_name)
        if cached:
            logger.debug(f"Cache hit for recording '{track_name}'")
            return {
                'mbid': cached['musicbrainz_id'],
                'title': track_name,
                'confidence': cached['confidence'],
                'cached': True
            }
        
        # Search MusicBrainz
        try:
            results = self.mb_client.search_recording(track_name, artist_name, limit=5)
            
            if not results:
                logger.info(f"No MusicBrainz results for recording '{track_name}'")
                self._save_to_cache('recording', track_name, artist_name, None, None, 0)
                return None
            
            # Find best match
            best_match = None
            best_confidence = 0
            
            for result in results:
                mb_title = result.get('title', '')
                mb_score = result.get('score', 0)

                # Calculate title similarity
                title_similarity = self._calculate_similarity(track_name, mb_title)

                # Hard gate: title must be at least 60% similar.
                # Without this, artist bonus + MB score can push totally
                # different titles (e.g. "Sweet Surrender" → "Answers")
                # past the confidence threshold.
                if title_similarity < 0.6:
                    continue

                # If we have artist info, check artist match too
                artist_bonus = 0
                if artist_name and 'artist-credit' in result:
                    artist_credits = result['artist-credit']
                    for credit in artist_credits:
                        if isinstance(credit, dict) and 'artist' in credit:
                            mb_artist = credit['artist'].get('name', '')
                            artist_similarity = self._calculate_similarity(artist_name, mb_artist)
                            if artist_similarity > 0.7:
                                artist_bonus = 20
                                break

                # Combine scores - cap at 100
                confidence = min(100, int((title_similarity * 50) + (mb_score / 100 * 30) + artist_bonus))

                if confidence > best_confidence:
                    best_confidence = confidence
                    best_match = result

            # Only return matches with confidence >= 70%
            if best_match and best_confidence >= 70:
                mbid = best_match.get('id')
                mb_title = best_match.get('title')
                
                # Save to cache
                self._save_to_cache('recording', track_name, artist_name, mbid, best_match, best_confidence)
                
                logger.info(f"Matched recording '{track_name}' → '{mb_title}' (MBID: {mbid}, confidence: {best_confidence})")
                
                return {
                    'mbid': mbid,
                    'title': mb_title,
                    'confidence': best_confidence,
                    'cached': False
                }
            else:
                logger.info(f"Low confidence match for recording '{track_name}' (best: {best_confidence})")
                self._save_to_cache('recording', track_name, artist_name, None, None, best_confidence)
                return None
                
        except Exception as e:
            logger.error(f"Error matching recording '{track_name}': {e}")
            return None
    
    def update_artist_mbid(self, artist_id: int, mbid: Optional[str], status: str):
        """Update artist with MusicBrainz ID"""
        conn = None
        try:
            conn = self.db._get_connection()
            cursor = conn.cursor()
            
            cursor.execute("""
                UPDATE artists
                SET musicbrainz_id = ?,
                    musicbrainz_last_attempted = ?,
                    musicbrainz_match_status = ?
                WHERE id = ?
            """, (mbid, datetime.now(), status, artist_id))
            
            conn.commit()
            
            logger.debug(f"Updated artist {artist_id} with MBID: {mbid}, status: {status}")
            
        except Exception as e:
            logger.error(f"Error updating artist {artist_id}: {e}")
            if conn:
                conn.rollback()
        finally:
            if conn:
                conn.close()
    
    def update_album_mbid(self, album_id: int, mbid: Optional[str], status: str):
        """Update album with MusicBrainz release ID"""
        conn = None
        try:
            conn = self.db._get_connection()
            cursor = conn.cursor()
            
            cursor.execute("""
                UPDATE albums
                SET musicbrainz_release_id = ?,
                    musicbrainz_last_attempted = ?,
                    musicbrainz_match_status = ?
                WHERE id = ?
            """, (mbid, datetime.now(), status, album_id))
            
            conn.commit()
            
            logger.debug(f"Updated album {album_id} with MBID: {mbid}, status: {status}")
            
        except Exception as e:
            logger.error(f"Error updating album {album_id}: {e}")
            if conn:
                conn.rollback()
        finally:
            if conn:
                conn.close()
    
    def update_track_mbid(self, track_id: int, mbid: Optional[str], status: str):
        """Update track with MusicBrainz recording ID"""
        conn = None
        try:
            conn = self.db._get_connection()
            cursor = conn.cursor()

            cursor.execute("""
                UPDATE tracks
                SET musicbrainz_recording_id = ?,
                    musicbrainz_last_attempted = ?,
                    musicbrainz_match_status = ?
                WHERE id = ?
            """, (mbid, datetime.now(), status, track_id))

            conn.commit()

            logger.debug(f"Updated track {track_id} with MBID: {mbid}, status: {status}")

        except Exception as e:
            logger.error(f"Error updating track {track_id}: {e}")
            if conn:
                conn.rollback()
        finally:
            if conn:
                conn.close()

