import random
from datetime import datetime
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app
from flask_login import current_user, login_required
from musicround.models import Song, Round, Tag, db
from musicround.helpers.auth_helpers import oauth
from musicround.helpers.import_helper import ImportHelper

generate_bp = Blueprint('generate', __name__)

# Constants
songs_per_round = 8

# Helper functions
def get_all_decades():
    """
    Return a list of 'decade' strings (e.g. '1970', '1980')
    based on the first 3 digits of the year + '0'.
    """
    all_decades = []
    for song in Song.query.all():
        if song.year:
            decade = str(song.year)[:3] + '0'
            if decade not in all_decades:
                all_decades.append(decade)
    return all_decades

def get_all_genres():
    """
    Return a list of all genres in the Song table.
    """
    all_genres = []
    for song in Song.query.all():
        if song.genre and song.genre not in all_genres:
            all_genres.append(song.genre)
    return all_genres

def get_all_tags():
    """
    Return a list of all tag names in the Tag table.
    """
    return [tag.name for tag in Tag.query.all()]

def get_songs_by_tag(tag_name, limit=8):
    """
    Return songs that have the specified tag.
    """
    tag = Tag.query.filter_by(name=tag_name).first()
    if tag:
        return tag.songs[:limit]
    return []

def get_least_used_genres():
    """
    Returns a list of genre(s) whose usage count is minimal among all genres.
    Usage is measured by how many Rounds of type 'genre' reference that genre.
    """
    all_genres_list = get_all_genres()
    # Start every genre with usage=0
    genre_usage = {g: 0 for g in all_genres_list}

    # Count how many times each genre appears in Rounds of type 'genre'
    used_genre_rounds = Round.query.filter_by(round_type='genre').all()
    for rnd in used_genre_rounds:
        # Ensure we only increment if it exists in genre_usage
        if rnd.round_criteria_used in genre_usage:
            genre_usage[rnd.round_criteria_used] += 1

    # If we have no genres at all, return an empty list
    if not genre_usage:
        return []

    # Find the minimal usage count
    min_usage = min(genre_usage.values())

    # Return all genres that match min_usage
    return [g for g, usage in genre_usage.items() if usage == min_usage]


def get_least_used_decades():
    """
    Returns a list of decade(s) whose usage count is minimal among all decades.
    Usage is measured by how many Rounds of type 'decade' reference that decade.
    """
    all_decades_list = get_all_decades()
    # Start each decade with usage=0
    decade_usage = {d: 0 for d in all_decades_list}

    # Count how many times each decade appears in Rounds of type 'decade'
    used_decade_rounds = Round.query.filter_by(round_type='decade').all()
    for rnd in used_decade_rounds:
        if rnd.round_criteria_used in decade_usage:
            decade_usage[rnd.round_criteria_used] += 1

    # If we have no decades at all, return empty
    if not decade_usage:
        return []

    # Minimal usage
    min_usage = min(decade_usage.values())

    # Return all decades that match min_usage
    return [d for d, usage in decade_usage.items() if usage == min_usage]

def get_least_used_songs(genre=None, decade=None):
    """
    Returns songs that have never been used in a round.
    Can filter by genre or decade.
    """
    least_used_songs = []
    all_songs = Song.query.all()

    # gather round_criteria_used for rounds of type 'song'
    used_song_ids = []
    for rnd in Round.query.all():
        if rnd.round_type == 'song':
            used_song_ids.append(rnd.round_criteria_used)

    # If a song's spotify_id never appears in used_song_ids => "least used"
    for song in all_songs:
        if song.spotify_id not in used_song_ids:
            least_used_songs.append(song)

    # Filter by genre or decade if passed
    if genre:
        least_used_songs = [s for s in least_used_songs if s.genre == genre]
    if decade:
        least_used_songs = [s for s in least_used_songs if s.year and str(s.year)[:3] + '0' == decade]
    return least_used_songs

def get_non_overused_songs(genre=None, decade=None):
    """
    Returns a list of songs whose used_count is <= the average usage among all songs.
    Optional filtering by genre or decade.
    """
    all_songs = Song.query.all()
    total_times_used = sum(song.used_count for song in all_songs) or 1
    average_times_used = total_times_used / len(all_songs) if len(all_songs) else 1

    # pick songs that are used <= average usage
    non_overused_songs = [s for s in all_songs if s.used_count <= average_times_used]

    if genre:
        non_overused_songs = [s for s in non_overused_songs if s.genre == genre]

    if decade:
        non_overused_songs = [s for s in non_overused_songs if s.year and str(s.year)[:3] + '0' == decade]

    return non_overused_songs

def get_random_songs_from_genre(genre, x=5):
    """
    Returns x random songs from the given genre,
    filling from non-overused songs in that genre.
    If not enough, fallback to any non-overused songs.
    """
    non_overused = get_non_overused_songs(genre=genre)
    while len(non_overused) < x:
        more = get_non_overused_songs()
        if not more:  # in case the DB is empty or something else
            break
        non_overused.extend(more)

    return random.sample(non_overused, x) if len(non_overused) >= x else non_overused

def get_random_songs_from_decade(decade, x=5):
    """
    Returns x random songs from the given decade,
    filling from non-overused songs in that decade.
    If not enough, fallback to any non-overused songs.
    """
    non_overused = get_non_overused_songs(decade=decade)
    while len(non_overused) < x:
        more = get_non_overused_songs()
        if not more:
            break
        non_overused.extend(more)

    return random.sample(non_overused, x) if len(non_overused) >= x else non_overused

def get_random_songs(x):
    """
    Returns x random songs from the pool of non-overused songs,
    ensuring some naive diversity constraints:
     - no artist used more than once
     - no decade used more than x/3 times
     - number of unique artists must match number of chosen songs
    """
    non_overused_songs = get_non_overused_songs()
    if len(non_overused_songs) < x:
        return non_overused_songs
        
    random_songs = random.sample(non_overused_songs, x)

    artist_count = {}
    decade_count = {}

    for song in random_songs:
        artist_count[song.artist] = artist_count.get(song.artist, 0) + 1
        if song.year:
            dec = str(song.year)[:3] + '0'
            decade_count[dec] = decade_count.get(dec, 0) + 1

    while (
        max(artist_count.values()) > 1
        or (decade_count and max(decade_count.values()) > len(random_songs) / 3)
        or len(artist_count) != len(random_songs)
    ):
        # 1. If any artist is used more than once, replace that song
        if max(artist_count.values()) > 1:
            repeated_artist = None
            for artist, count in artist_count.items():
                if count > 1:
                    repeated_artist = artist
                    break
            if repeated_artist:
                # remove one of that artist from random_songs
                to_remove = next(s for s in random_songs if s.artist == repeated_artist)
                random_songs.remove(to_remove)
                artist_count[repeated_artist] -= 1

                # pick a new random non-overused
                refill = [s for s in non_overused_songs if s not in random_songs]
                if refill:
                    new_song = random.choice(refill)
                    random_songs.append(new_song)
                    artist_count[new_song.artist] = artist_count.get(new_song.artist, 0) + 1

                    # update decade_count
                    if to_remove.year:
                        dec_to_remove = str(to_remove.year)[:3] + '0'
                        decade_count[dec_to_remove] = decade_count.get(dec_to_remove, 0) - 1
                    if new_song.year:
                        dec_new = str(new_song.year)[:3] + '0'
                        decade_count[dec_new] = decade_count.get(dec_new, 0) + 1
                else:
                    # If no new songs available, just return what we have
                    return random_songs

        elif decade_count and max(decade_count.values()) > len(random_songs) / 3:
            # 2. If any decade is used more than x/3, remove a song from that decade
            repeated_decade = None
            for dec, count in decade_count.items():
                if count > len(random_songs) / 3:
                    repeated_decade = dec
                    break
            if repeated_decade:
                to_remove = next((s for s in random_songs if s.year and str(s.year)[:3] + '0' == repeated_decade), None)
                if to_remove:
                    random_songs.remove(to_remove)
                    decade_count[repeated_decade] -= 1

                    # pick a new random
                    refill = [s for s in non_overused_songs if s not in random_songs]
                    if refill:
                        new_song = random.choice(refill)
                        random_songs.append(new_song)
                        if new_song.year:
                            dec_new = str(new_song.year)[:3] + '0'
                            decade_count[dec_new] = decade_count.get(dec_new, 0) + 1

                        # update artist_count
                        artist_count[to_remove.artist] -= 1
                        artist_count[new_song.artist] = artist_count.get(new_song.artist, 0) + 1
                    else:
                        # If no new songs available, just return what we have
                        return random_songs

        elif len(artist_count) != len(random_songs):
            # 3. if there's mismatch in how many unique artists vs. songs, fix that
            #    i.e. if we have a repeated artist but haven't caught it above
            refill = [s for s in non_overused_songs if s not in random_songs]
            repeated_song = None
            # find a repeated artist
            for s in random_songs:
                if artist_count[s.artist] > 1:
                    repeated_song = s
                    break
            if repeated_song is None or not refill:
                break  # fallback

            random_songs.remove(repeated_song)
            artist_count[repeated_song.artist] -= 1

            new_song = random.choice(refill)
            random_songs.append(new_song)
            artist_count[new_song.artist] = artist_count.get(new_song.artist, 0) + 1

            if repeated_song.year:
                dec_removed = str(repeated_song.year)[:3] + '0'
                decade_count[dec_removed] = decade_count.get(dec_removed, 0) - 1
            if new_song.year:
                dec_new = str(new_song.year)[:3] + '0'
                decade_count[dec_new] = decade_count.get(dec_new, 0) + 1

    return random_songs

def get_random_songs_from_least_used_decade(x):
    """
    Returns up to x songs from *one* of the least used decade(s), chosen at random.
    Returns (songs, chosen_decade).
    """
    candidates = get_least_used_decades()
    if not candidates:
        return [], None

    chosen_decade = random.choice(candidates)
    random_songs = get_random_songs_from_decade(chosen_decade, x=x)

    return random_songs, chosen_decade


def get_random_songs_from_least_used_genre(x):
    """
    Returns up to x songs from *one* of the least used genre(s), chosen at random.
    Returns (songs, chosen_genre).
    """
    candidates = get_least_used_genres()
    if not candidates:
        return [], None

    chosen_genre = random.choice(candidates)
    random_songs = get_random_songs_from_genre(chosen_genre, x=x)

    return random_songs, chosen_genre

def get_songs_from_deezer_playlist(playlist_id):
    """
    Fetch songs from a Deezer playlist, properly import them with metadata, and return them
    """
    try:
        deezer_client = current_app.config['deezer']
        songs_per_round = current_app.config.get('SONGS_PER_ROUND', 10)
        
        imported_songs = ImportHelper.import_item(
            item_id=playlist_id,
            item_type='playlist',
            source='deezer',
            deezer_client=deezer_client
        )

        if not imported_songs:
            current_app.logger.warning(f"No songs returned from ImportHelper.import_item for Deezer playlist {playlist_id}")
            return []

        return imported_songs[:songs_per_round]
    except Exception as e:
        current_app.logger.error(f"Error fetching or importing Deezer playlist {playlist_id}: {e}")
        import traceback
        current_app.logger.error(traceback.format_exc())
        return []

def get_songs_from_spotify_playlist(playlist_id):
    """
    Fetch songs from a Spotify playlist, properly import them with metadata, and return them
    Always returns the songs in the playlist, even if all already exist in the DB.
    """
    try:
        songs_per_round = current_app.config.get('SONGS_PER_ROUND', 10)
        import_result = ImportHelper.import_item(
            item_id=playlist_id,
            item_type='playlist',
            service_name='spotify',
            oauth_spotify=oauth.spotify
        )

        from musicround.models import Song

        # If we have imported_song_ids, use them (these are DB IDs)
        if import_result.get('imported_song_ids'):
            song_db_ids = import_result['imported_song_ids']
            imported_songs = Song.query.filter(Song.id.in_(song_db_ids)).all()
            return imported_songs[:songs_per_round]

        # If no imported_song_ids, fetch all Spotify IDs from the playlist and get those songs from DB
        # Use the Spotify API directly to get the playlist track IDs
        sp = oauth.spotify
        # Get playlist tracks (paginated)
        all_spotify_ids = []
        next_url = f'playlists/{playlist_id}/tracks'
        authlib_token = {
            'access_token': current_user.spotify_token,
            'refresh_token': current_user.spotify_refresh_token,
            'token_type': 'Bearer',
            'expires_at': int(current_user.spotify_token_expiry.timestamp()) if current_user.spotify_token_expiry else None
        }
        while next_url:
            resp = sp.get(next_url, token=authlib_token)
            resp.raise_for_status()
            data = resp.json()
            for item in data.get('items', []):
                track = item.get('track')
                if track and track.get('id'):
                    all_spotify_ids.append(track['id'])
            next_url = data.get('next')
            # If next_url is a full URL, convert to relative for sp.get
            if next_url and next_url.startswith('https://api.spotify.com/v1/'):
                next_url = next_url.replace('https://api.spotify.com/v1/', '')
        if not all_spotify_ids:
            current_app.logger.warning(f"No valid tracks found in Spotify playlist {playlist_id}")
            return []
        # Query all songs in DB with those Spotify IDs, preserving playlist order
        songs_by_spotify_id = {s.spotify_id: s for s in Song.query.filter(Song.spotify_id.in_(all_spotify_ids)).all()}
        ordered_songs = [songs_by_spotify_id[sid] for sid in all_spotify_ids if sid in songs_by_spotify_id]
        return ordered_songs[:songs_per_round]
    except Exception as e:
        current_app.logger.error(f"Error fetching or importing Spotify playlist {playlist_id}: {e}")
        import traceback
        current_app.logger.error(traceback.format_exc())
        return []

@generate_bp.route('/build-music-round', methods=['GET', 'POST'])
@login_required
def build_music_round():
    """Build a music round based on the selected criteria"""
    if request.method == 'POST':
        round_type = request.form['round_type']
        if round_type == 'Random':
            round_criteria = 'Random'
            songs = get_random_songs(songs_per_round)
            return render_template('round.html', songs=songs, round_criteria=round_criteria)

        elif round_type == 'Decade':
            round_criteria = 'Least Used Decade'
            songs, decade_used = get_random_songs_from_least_used_decade(songs_per_round)
            return render_template(
                'round.html',
                songs=songs,
                round_criteria=round_criteria,
                decade=decade_used
            )

        elif round_type == 'Genre':
            round_criteria = 'Least Used Genre'
            songs, genre_used = get_random_songs_from_least_used_genre(songs_per_round)
            return render_template(
                'round.html',
                songs=songs,
                round_criteria=round_criteria,
                genre=genre_used
            )
            
        elif round_type == 'Tag':
            tag_name = request.form.get('tag_name')
            if tag_name:
                round_criteria = f'Tag: {tag_name}'
                songs = get_songs_by_tag(tag_name, songs_per_round)
                return render_template(
                    'round.html',
                    songs=songs,
                    round_criteria=round_criteria,
                    tag=tag_name
                )

    # Pass tag choices to the template for selection
    tags = get_all_tags()
    return render_template('build_music_round.html', tags=tags)

@generate_bp.route('/import-playlist', methods=['GET', 'POST'])
@login_required
def import_playlist():
    """Import a playlist from Deezer or Spotify"""
    if request.method == 'POST':
        playlist_url = request.form.get('playlist_url', '')
        platform = request.form.get('platform', '').lower()
        round_name = request.form.get('round_name', '')
        
        if not playlist_url:
            flash('Please enter a playlist URL or ID', 'error')
            return redirect(url_for('generate.import_playlist'))
        
        # Extract playlist ID from URL or use as is
        playlist_id = playlist_url
        
        if platform == 'deezer':
            # Extract Deezer playlist ID from URL if needed
            if 'deezer.com' in playlist_url:
                try:
                    playlist_id = playlist_url.split('playlist/')[1].split('?')[0]
                except (IndexError, ValueError):
                    flash('Invalid Deezer playlist URL', 'error')
                    return redirect(url_for('generate.import_playlist'))
            
            songs = get_songs_from_deezer_playlist(playlist_id)
            if not songs:
                flash('No songs found or error fetching playlist from Deezer', 'error')
                return redirect(url_for('generate.import_playlist'))
                
            round_criteria = f'Deezer Playlist: {playlist_id}'
            
        elif platform == 'spotify':
            # Extract Spotify playlist ID from URL if needed
            if 'spotify.com' in playlist_url:
                try:
                    playlist_id = playlist_url.split('playlist/')[1].split('?')[0]
                except (IndexError, ValueError):
                    flash('Invalid Spotify playlist URL', 'error')
                    return redirect(url_for('generate.import_playlist'))
            
            songs = get_songs_from_spotify_playlist(playlist_id)
            if not songs:
                flash('No songs found or error fetching playlist from Spotify', 'error')
                return redirect(url_for('generate.import_playlist'))
                
            round_criteria = f'Spotify Playlist: {playlist_id}'
            
        else:
            flash('Please select a valid platform', 'error')
            return redirect(url_for('generate.import_playlist'))
            
        return render_template('round.html', 
                               songs=songs, 
                               round_criteria=round_criteria, 
                               round_name=round_name,
                               playlist_import=True)
    
    return render_template('import_playlist.html')

@generate_bp.route('/save_round', methods=['POST'])
@login_required
def save_round():
    """
    Persists a new Round to the DB (with chosen songs).
    Increments used_count on all chosen songs.
    """
    # get round criteria and name from form
    round_criteria = request.form.get('round_criteria')
    round_name = request.form.get('round_name')
    
    # get optional genre and decade from form
    genre = request.form.get('genre')
    decade = request.form.get('decade')
    tag = request.form.get('tag')
    
    # get list of song IDs from form
    song_ids = request.form.getlist('song_id')

    # get list of song objects from database
    songs = Song.query.filter(Song.id.in_(song_ids)).all()

    # create string representation of song IDs
    song_ids_str = ','.join(song_id for song_id in song_ids)

    # determine round type
    if genre:
        round_type = 'Genre'
        round_criteria_used = genre
    elif decade:
        round_type = 'Decade'
        round_criteria_used = decade
    elif tag:
        round_type = 'Tag'
        round_criteria_used = tag
    else:
        round_type = 'Random'
        round_criteria_used = 'Random Selection'

    # create new Round object and add to database
    new_round = Round(
        name=round_name,
        round_type=round_type,
        round_criteria_used=round_criteria_used,
        songs=song_ids_str,
        created_at=datetime.utcnow()
    )
    db.session.add(new_round)

    # update usage count for each song
    for song in songs:
        song.used_count += 1
        db.session.add(song)

    db.session.commit()

    # redirect back to the rounds page
    return redirect(url_for('rounds.rounds_list'))