import sys
import os
sys.path.insert(0, os.path.dirname(__file__))
import time
from datetime import datetime, timedelta
from threading import Lock
from flask import Flask, render_template, request, redirect, url_for, session, jsonify, send_from_directory, abort, Response, stream_with_context
from flask_socketio import SocketIO, emit
from werkzeug.utils import secure_filename
from mutagen.mp3 import MP3
from models import db, Song, QueueItem, Recommendation, BannedIP, SongVote, BlogPost, StationDetectionEvent, ListenerStat, PollSong, PollVote
import smtplib
import logging
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from sqlalchemy import text
import requests
import json as json_module

def load_env_file(env_path):
    if not os.path.exists(env_path):
        return
    try:
        with open(env_path, 'r', encoding='utf-8') as env_file:
            for raw_line in env_file:
                line = raw_line.strip()
                if not line or line.startswith('#') or '=' not in line:
                    continue
                key, value = line.split('=', 1)
                key = key.strip()
                value = value.strip().strip('"').strip("'")
                if key and key not in os.environ:
                    os.environ[key] = value
    except Exception as e:
        print(f"ENV: Failed to load {env_path}. Error: {e}")

load_env_file(os.path.join(os.path.dirname(__file__), '.env'))

# ===== Utility Functions =====
def get_current_time():
    """Retourne l'heure actuelle en format lisible"""
    now = datetime.now()
    return {
        'time': now.strftime('%H:%M:%S'),
        'date': now.strftime('%d/%m/%Y'),
        'day': ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'][now.weekday()],
        'timestamp': now.isoformat()
    }

def get_current_time_text():
    """Retourne l'heure en format texte lisible (ex: 'Il est 14:30 le mardi 26 mai 2026')"""
    now = datetime.now()
    days = ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche']
    months = ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre']
    day_name = days[now.weekday()]
    month_name = months[now.month - 1]
    return f"Il est {now.strftime('%H:%M:%S')} le {day_name} {now.day} {month_name} {now.year}"

# ===== AI/Ollama Functions =====
OLLAMA_API_URL = "http://82.26.80.39:11434/api/generate"
OLLAMA_MODEL   = "qwen2:0.5b"

# Load AI persona from AI_PROFILE.txt once at startup
_AI_PROFILE_PATH = os.path.join(os.path.dirname(__file__), 'AI_PROFILE.txt')
def _load_ai_profile():
    try:
        with open(_AI_PROFILE_PATH, 'r', encoding='utf-8') as f:
            return f.read().strip()
    except Exception:
        return ""

AI_SYSTEM_PROMPT = _load_ai_profile()

def build_prompt(user_message: str) -> str:
    """
    Builds the full prompt sent to Ollama.
    Injects the AI_PROFILE.txt as a system instruction before the user message,
    so the model always responds in character.
    """
    if AI_SYSTEM_PROMPT:
        return (
            f"[SYSTEM INSTRUCTIONS - follow strictly]\n"
            f"{AI_SYSTEM_PROMPT}\n"
            f"[END SYSTEM INSTRUCTIONS]\n\n"
            f"Utilizador: {user_message}\n"
            f"Assistente:"
        )
    return user_message

def query_ollama_streaming(user_message: str, user_ip: str = None):
    """Query Ollama API with streaming response, using AI profile as context and chat history."""
    # Get chat history for this user (limited to 5 messages for small model)
    history = []
    if user_ip:
        from models import ChatMessage
        recent_messages = ChatMessage.query.filter_by(user_ip=user_ip).order_by(ChatMessage.created_at.desc()).limit(5).all()
        recent_messages.reverse()  # Get in chronological order
        for msg in recent_messages:
            role = "Utilizador" if msg.role == 'user' else "Assistente"
            history.append(f"{role}: {msg.content}")
    
    # Build prompt with history - clearer format for model
    if history:
        history_text = "\n".join(history)
        enhanced_prompt = f"{AI_SYSTEM_PROMPT}\n\n=== CONVERSA ANTERIOR ===\n{history_text}\n=== FIM DA CONVERSA ANTERIOR ===\n\nPERGUNTA ATUAL: {user_message}\n\nIMPORTANTE: Usa a conversa anterior acima para responder à pergunta atual."
    else:
        enhanced_prompt = f"{AI_SYSTEM_PROMPT}\n\nPergunta: {user_message}\n\nResposta:"
    
    try:
        payload = {
            "model": OLLAMA_MODEL,
            "prompt": enhanced_prompt,
            "stream": True
        }

        response = requests.post(
            OLLAMA_API_URL,
            json=payload,
            timeout=30,
            stream=True
        )

        if response.status_code != 200:
            yield json_module.dumps({"error": f"Ollama error: {response.status_code}"})
            return

        for line in response.iter_lines():
            if line:
                try:
                    data = json_module.loads(line)
                    if "response" in data:
                        yield data["response"]
                except Exception:
                    pass

    except requests.exceptions.Timeout:
        yield "\n[Tempo limite excedido]"
    except requests.exceptions.ConnectionError:
        yield "\n[Erro: Não foi possível ligar ao servidor de IA]"
    except Exception as e:
        yield f"\n[Erro: {str(e)}]"


app = Flask(__name__)
app.config['SECRET_KEY'] = 'super-secret-radio-key-ig'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///radio.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['UPLOAD_FOLDER'] = os.path.join(app.root_path, 'static', 'music')
app.config['IMAGE_FOLDER'] = os.path.join(app.root_path, 'static', 'images')

# Error Handlers
@app.errorhandler(404)
def page_not_found(e):
    return render_template('error.html', 
        code=404, 
        status="Página Não Encontrada", 
        message="Oops! O link que seguiste pode estar quebrado ou a página foi removida."), 404

@app.errorhandler(500)
def internal_error(e):
    return render_template('error.html', 
        code=500, 
        status="Erro Interno", 
        message="Algo correu mal nos nossos servidores. Estamos a trabalhar para resolver o problema."), 500

if not os.path.exists(app.config['UPLOAD_FOLDER']):
    os.makedirs(app.config['UPLOAD_FOLDER'])
if not os.path.exists(app.config['IMAGE_FOLDER']):
    os.makedirs(app.config['IMAGE_FOLDER'])

LOG_FOLDER = os.path.join(app.root_path, 'logs')
if not os.path.exists(LOG_FOLDER):
    os.makedirs(LOG_FOLDER)

email_logger = logging.getLogger('email_logger')
email_logger.setLevel(logging.INFO)
email_logger.propagate = False
if not email_logger.handlers:
    formatter = logging.Formatter('%(asctime)s | %(levelname)s | %(message)s')
    file_handler = logging.FileHandler(os.path.join(LOG_FOLDER, 'email.log'), encoding='utf-8')
    file_handler.setFormatter(formatter)
    stream_handler = logging.StreamHandler()
    stream_handler.setFormatter(formatter)
    email_logger.addHandler(file_handler)
    email_logger.addHandler(stream_handler)
    email_logger.info("Email logger initialized. Log file: %s", os.path.join(LOG_FOLDER, 'email.log'))

# Email Configuration
SMTP_SERVER = os.getenv('SMTP_SERVER') or os.getenv('MAIL_HOST', 'mail.thebothost.com')
SMTP_PORT = int(os.getenv('SMTP_PORT') or os.getenv('MAIL_PORT', '587'))
SMTP_USER = os.getenv('SMTP_USER') or os.getenv('MAIL_USERNAME', 'noreply-igradio@tbhwf.com')
SMTP_PASS = os.getenv('SMTP_PASS') or os.getenv('MAIL_PASSWORD', '')
SMTP_DEBUG = os.getenv('SMTP_DEBUG', '0') == '1'
SMTP_USE_SSL = os.getenv('SMTP_USE_SSL', '0') == '1'
SMTP_ASYNC = os.getenv('SMTP_ASYNC', '0') == '1'
MAIL_ENCRYPTION = (os.getenv('MAIL_ENCRYPTION') or '').strip().lower()
if MAIL_ENCRYPTION == 'ssl':
    SMTP_USE_SSL = True
elif MAIL_ENCRYPTION == 'tls':
    SMTP_USE_SSL = False

MAIL_FROM_ADDRESS = os.getenv('MAIL_FROM_ADDRESS')
if MAIL_FROM_ADDRESS:
    SMTP_USER = MAIL_FROM_ADDRESS

def send_email_task(to_email, subject, body):
    if not SMTP_PASS:
        email_logger.error("SMTP skip: SMTP_PASS missing")
        return
    email_logger.info(
        "SMTP start: to=%s subject=%s server=%s:%s ssl=%s",
        to_email, subject, SMTP_SERVER, SMTP_PORT, SMTP_USE_SSL
    )
    server = None
    try:
        msg = MIMEMultipart()
        msg['From'] = SMTP_USER
        msg['To'] = to_email
        msg['Subject'] = subject
        msg.attach(MIMEText(body, 'plain'))

        import ssl
        context = ssl.create_default_context()
        email_logger.info("SMTP connect: opening connection")
        if SMTP_USE_SSL:
            server = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=10, context=context)
            email_logger.info("SMTP ssl: using implicit SSL")
        else:
            server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=10)
        if SMTP_DEBUG:
            server.set_debuglevel(1)
            email_logger.info("SMTP debug enabled (protocol trace in console)")
        if not SMTP_USE_SSL:
            email_logger.info("SMTP tls: starting STARTTLS")
            server.starttls(context=context)
        email_logger.info("SMTP auth: logging in as %s", SMTP_USER)
        server.login(SMTP_USER, SMTP_PASS)
        email_logger.info("SMTP send: sending message to %s", to_email)
        server.send_message(msg)
        email_logger.info("SMTP success: email sent to %s", to_email)
    except Exception as e:
        email_logger.exception("SMTP error: failed to send to %s. Error: %s", to_email, e)
    finally:
        if server is not None:
            try:
                server.quit()
            except Exception:
                email_logger.warning("SMTP warning: error while closing connection")

def send_email(to_email, subject, body):
    if not to_email:
        email_logger.warning("SMTP skip: no recipient")
        return
    if SMTP_ASYNC:
        import threading
        threading.Thread(target=send_email_task, args=(to_email, subject, body), daemon=True).start()
        email_logger.info("SMTP queued (async): to=%s", to_email)
        return
    email_logger.info("SMTP sending (sync): to=%s", to_email)
    send_email_task(to_email, subject, body)

def ensure_recommendation_columns():
    existing_columns = {
        row[1] for row in db.session.execute(text("PRAGMA table_info(recommendation)")).fetchall()
    }
    if 'email' not in existing_columns:
        db.session.execute(text("ALTER TABLE recommendation ADD COLUMN email VARCHAR(150)"))
    if 'rejection_reason' not in existing_columns:
        db.session.execute(text("ALTER TABLE recommendation ADD COLUMN rejection_reason VARCHAR(250)"))
    db.session.commit()

db.init_app(app)
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')

# Global Radio State
radio_state = {
    'current_song': None,
    'start_time': 0,
    'is_playing': False
}
state_lock = Lock()
listeners = {} # sid: ip

import json
STATE_FILE = os.path.join(app.root_path, 'instance', 'radio_state.json')

WEEKDAY_LABELS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']


def normalize_frequency(value):
    return f"{float(value):.3f}"


def format_last_heard(last_detected_at):
    if not last_detected_at:
        return None

    now = datetime.utcnow()
    delta = now - last_detected_at
    seconds = int(delta.total_seconds())

    if seconds < 60:
        return "agora mesmo"
    if seconds < 3600:
        minutes = max(1, seconds // 60)
        return f"há {minutes}m"
    if seconds < 86400:
        hours = max(1, seconds // 3600)
        return f"há {hours}h"

    now_date = now.date()
    last_date = last_detected_at.date()
    if last_date == (now_date - timedelta(days=1)):
        return "ontem"

    days = max(1, delta.days)
    if days < 7:
        if days == 1:
            return "ontem"
        return f"há {days} dias"

    return last_detected_at.strftime('%d/%m/%Y')

def save_state():
    try:
        state_to_save = radio_state.copy()
        if radio_state['is_playing']:
            state_to_save['elapsed'] = time.time() - radio_state['start_time']
        else:
            state_to_save['elapsed'] = 0
            
        with open(STATE_FILE, 'w') as f:
            json.dump(state_to_save, f)
    except Exception as e:
        print("Save state error:", e)

def load_state():
    global radio_state
    if os.path.exists(STATE_FILE):
        try:
            with open(STATE_FILE, 'r') as f:
                state = json.load(f)
                if state.get('is_playing') and state.get('current_song'):
                    with app.app_context():
                        song = Song.query.get(state['current_song']['id'])
                        if song:
                            radio_state['current_song'] = {
                                'id': song.id,
                                'title': song.title,
                                'artist': song.artist,
                                'filename': song.filename,
                                'image_filename': song.image_filename,
                                'duration': song.duration
                            }
                            # Retoma a música exatamente do segundo em que o servidor foi desligado
                            elapsed = state.get('elapsed', 0)
                            radio_state['start_time'] = time.time() - elapsed
                            radio_state['is_playing'] = True
        except Exception as e:
            print("Load state error:", e)

def get_next_song():
    with app.app_context():
        next_item = QueueItem.query.order_by(QueueItem.position).first()
        if next_item:
            song_data = {
                'id': next_item.song.id,
                'title': next_item.song.title,
                'artist': next_item.song.artist,
                'filename': next_item.song.filename,
                'image_filename': next_item.song.image_filename,
                'duration': next_item.song.duration
            }
            db.session.delete(next_item)
            db.session.commit()
            return song_data
        return None

def radio_thread():
    sync_counter = 0
    while True:
        try:
            with state_lock:
                current_song = radio_state['current_song']
                if not radio_state['is_playing'] or (time.time() - radio_state['start_time'] >= (current_song['duration'] if current_song else 0)):
                    next_song = get_next_song()
                    if next_song:
                        radio_state['current_song'] = next_song
                        radio_state['start_time'] = time.time()
                        radio_state['is_playing'] = True
                        save_state()
                        socketio.emit('radio_update', get_radio_status())
                    else:
                        if radio_state['is_playing']:
                            radio_state['current_song'] = None
                            radio_state['is_playing'] = False
                            save_state()
                            socketio.emit('radio_update', None)
                else:
                    sync_counter += 1
                    if sync_counter >= 5:
                        socketio.emit('sync', get_radio_status())
                        sync_counter = 0
        except Exception as e:
            print("Error in radio_thread:", e)
        socketio.sleep(1)

def get_radio_status():
    if radio_state['current_song']:
        with app.app_context():
            likes = SongVote.query.filter_by(song_id=radio_state['current_song']['id'], vote_type='like').count()
            dislikes = SongVote.query.filter_by(song_id=radio_state['current_song']['id'], vote_type='dislike').count()

        with app.app_context():
            queue_items = QueueItem.query.order_by(QueueItem.position).limit(5).all()
            queue_list = [{'title': q.song.title, 'artist': q.song.artist, 'duration': q.song.duration} for q in queue_items]
            
        return {
            'song_id': radio_state['current_song']['id'],
            'title': radio_state['current_song']['title'],
            'artist': radio_state['current_song']['artist'],
            'filename': radio_state['current_song']['filename'],
            'image_filename': radio_state['current_song']['image_filename'],
            'duration': radio_state['current_song']['duration'],
            'start_time': radio_state['start_time'],
            'current_time': time.time() - radio_state['start_time'],
            'queue': queue_list,
            'likes': likes,
            'dislikes': dislikes
        }
    return None

@app.before_request
def check_ban():
    ip = request.remote_addr
    banned = BannedIP.query.filter_by(ip_address=ip).first()
    if banned:
        abort(403, "Seu IP foi banido.")

@app.route('/')
def index():
    posts = BlogPost.query.order_by(BlogPost.published_at.desc()).limit(3).all()
    poll_songs = PollSong.query.order_by(PollSong.votes.desc()).limit(5).all()
    return render_template('index.html', posts=posts, poll_songs=poll_songs)

@app.route('/live.mp3')
def live_stream():
    """Provides a continuous, proxied MP3 stream that syncs with the radio state."""
    def generate():
        last_song_id = None
        
        while True:
            current_file = None
            start_offset = 0
            
            with state_lock:
                if radio_state['current_song']:
                    song = radio_state['current_song']
                    # If it's a new song or we are just starting
                    if song['id'] != last_song_id:
                        current_file = os.path.join(app.config['UPLOAD_FOLDER'], song['filename'])
                        # Calculate where we should be in the file
                        start_offset = time.time() - radio_state['start_time']
                        last_song_id = song['id']
            
            if current_file and os.path.exists(current_file):
                try:
                    with open(current_file, 'rb') as f:
                        # Simple bit rate estimation to seek (very rough for MP3 but works for basic sync)
                        # A better way would be using mutagen to find byte offset, but we'll try a basic approach
                        file_size = os.path.getsize(current_file)
                        duration = radio_state['current_song']['duration']
                        if duration > 0:
                            bytes_per_sec = file_size / duration
                            seek_pos = int(start_offset * bytes_per_sec)
                            if seek_pos < file_size:
                                f.seek(seek_pos)
                        
                        # Stream the file in chunks
                        while True:
                            chunk = f.read(4096 * 4) # 16KB chunks
                            if not chunk:
                                break
                            yield chunk
                            
                            # Check if the song has changed on the server while we were streaming
                            with state_lock:
                                if not radio_state['current_song'] or radio_state['current_song']['id'] != last_song_id:
                                    break
                            
                except Exception as e:
                    print(f"Stream error: {e}")
                    break
            else:
                # If nothing is playing, wait a bit and try again
                time.sleep(1)
                with state_lock:
                    if not radio_state['current_song']:
                        # Optional: yield some silence or a placeholder if needed
                        pass

    return Response(stream_with_context(generate()), mimetype='audio/mpeg')


@app.route('/admin', methods=['GET', 'POST'])
def admin():
    if request.method == 'POST':
        if request.form.get('password') == 'admin123':
            session['admin'] = True
            return redirect(url_for('admin_dashboard'))
    if session.get('admin'):
        return redirect(url_for('admin_dashboard'))
    return render_template('admin_login.html')

@app.route('/admin/logout')
def admin_logout():
    session.pop('admin', None)
    return redirect(url_for('admin'))

@app.route('/admin/dashboard')
def admin_dashboard():
    if not session.get('admin'): return redirect(url_for('admin'))
    songs = Song.query.all()
    queue = QueueItem.query.order_by(QueueItem.position).all()
    recommendations = Recommendation.query.filter_by(status='pending').all()
    banned_ips = BannedIP.query.all()
    blog_posts = BlogPost.query.order_by(BlogPost.published_at.desc()).all()
    song_rankings = (
        db.session.query(
            Song.id,
            Song.title,
            Song.artist,
            db.func.sum(db.case((SongVote.vote_type == 'like', 1), else_=0)).label('likes'),
            db.func.sum(db.case((SongVote.vote_type == 'dislike', 1), else_=0)).label('dislikes')
        )
        .outerjoin(SongVote, SongVote.song_id == Song.id)
        .group_by(Song.id, Song.title, Song.artist)
        .order_by(db.text('likes DESC'), db.text('dislikes DESC'))
        .all()
    )
    blog_posts = BlogPost.query.order_by(BlogPost.published_at.desc()).all()
    poll_songs = PollSong.query.order_by(PollSong.votes.desc()).all()

    return render_template(
        'admin.html',
        songs=songs,
        queue=queue,
        recommendations=recommendations,
        banned_ips=banned_ips,
        song_rankings=song_rankings,
        blog_posts=blog_posts,
        poll_songs=poll_songs
    )

@app.route('/admin/upload', methods=['POST'])
def upload_song():
    if not session.get('admin'): return jsonify({'error': 'Unauthorized'}), 401
    if 'file' not in request.files: return jsonify({'error': 'No file part'})
    file = request.files['file']
    if file.filename == '': return jsonify({'error': 'No selected file'})

    allowed_ext = ('.mp3', '.wav')
    file_ext = os.path.splitext(file.filename)[1].lower()

    if file and file_ext in allowed_ext:
        filename = secure_filename(file.filename)
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        file.save(filepath)
        import urllib.request
        import urllib.parse
        import json
        from mutagen import File
        audio_file = File(filepath)
        duration = audio_file.info.length if audio_file and getattr(audio_file, 'info', None) else 0

        title_tag = request.form.get('title')
        artist_tag = "Desconhecido"
        image_filename = "default_cover.png"
        has_apic = False

        if audio_file and audio_file.tags:
            tags = audio_file.tags

            if file_ext == '.mp3':
                # ID3 tags
                if 'TIT2' in tags and str(tags['TIT2']).strip():
                    title_tag = title_tag or str(tags['TIT2'])
                if 'TPE1' in tags and str(tags['TPE1']).strip():
                    artist_tag = str(tags['TPE1'])
                # Embedded cover art (APIC frame)
                for tag in tags.values():
                    if getattr(tag, 'FrameID', '') == 'APIC':
                        img_data = tag.data
                        img_ext = 'png' if tag.mime == 'image/png' else 'jpg'
                        image_filename = f"cover_{int(time.time())}.{img_ext}"
                        with open(os.path.join(app.config['IMAGE_FOLDER'], image_filename), 'wb') as img:
                            img.write(img_data)
                        has_apic = True
                        break
            else:
                # WAV — mutagen exposes RIFF INFO tags as a dict-like object
                # Keys: 'INAM' = title, 'IART' = artist
                inam = tags.get('INAM') or tags.get('inam')
                iart = tags.get('IART') or tags.get('iart')
                if inam:
                    val = str(inam).strip()
                    if val:
                        title_tag = title_tag or val
                if iart:
                    val = str(iart).strip()
                    if val:
                        artist_tag = val
                # WAV files rarely embed cover art; skip APIC search

        # Fallback to filename parsing if tags are missing
        name_no_ext = os.path.splitext(file.filename)[0]
        clean_name = name_no_ext.replace('_', ' ')
        if not title_tag or title_tag == filename or artist_tag == "Desconhecido":
            if ' - ' in clean_name:
                parts = clean_name.split(' - ', 1)
                if artist_tag == "Desconhecido":
                    artist_tag = parts[0].strip()
                if not title_tag or title_tag == filename:
                    title_tag = parts[1].strip()
            else:
                if not title_tag or title_tag == filename:
                    title_tag = clean_name

        title = title_tag or clean_name

        if not has_apic:
            import unicodedata
            search_artist = unicodedata.normalize('NFKD', artist_tag).encode('ASCII', 'ignore').decode('utf-8')
            search_title  = unicodedata.normalize('NFKD', title).encode('ASCII', 'ignore').decode('utf-8')
            query = urllib.parse.quote(f"{search_artist} {search_title}")
            url = f"https://itunes.apple.com/search?term={query}&entity=song&limit=1"
            try:
                req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
                with urllib.request.urlopen(req) as response:
                    data = json.loads(response.read().decode())
                    if data.get('resultCount', 0) > 0:
                        img_url = data['results'][0].get('artworkUrl100', '').replace('100x100bb', '500x500bb')
                        if img_url:
                            image_filename = f"cover_{int(time.time())}.jpg"
                            urllib.request.urlretrieve(img_url, os.path.join(app.config['IMAGE_FOLDER'], image_filename))
            except Exception as e:
                print("API Error:", e)

        song = Song(title=title, artist=artist_tag, filename=filename, image_filename=image_filename, duration=duration)
        db.session.add(song)
        db.session.commit()
        return redirect(url_for('admin_dashboard'))
    return jsonify({'error': 'Tipo de ficheiro inválido. Apenas .mp3 e .wav são aceites.'})


@app.route('/admin/delete_song/<int:id>', methods=['POST'])
def delete_song(id):
    if not session.get('admin'): return redirect(url_for('admin'))
    song = Song.query.get(id)
    if song:
        try:
            os.remove(os.path.join(app.config['UPLOAD_FOLDER'], song.filename))
        except:
            pass
        QueueItem.query.filter_by(song_id=id).delete()
        db.session.delete(song)
        db.session.commit()
    return redirect(url_for('admin_dashboard'))

@app.route('/admin/queue/<int:song_id>', methods=['POST'])
def queue_song(song_id):
    if not session.get('admin'): return redirect(url_for('admin'))
    song = Song.query.get(song_id)
    if song:
        pos = db.session.query(db.func.max(QueueItem.position)).scalar() or 0
        q = QueueItem(song_id=song.id, position=pos + 1)
        db.session.add(q)
        db.session.commit()

        # If radio is currently stopped, start playback immediately from queue.
        with state_lock:
            if not radio_state['is_playing'] or not radio_state['current_song']:
                next_song = get_next_song()
                if next_song:
                    radio_state['current_song'] = next_song
                    radio_state['start_time'] = time.time()
                    radio_state['is_playing'] = True
                    save_state()
                    socketio.emit('radio_update', get_radio_status())
    return redirect(url_for('admin_dashboard'))

@app.route('/admin/ban', methods=['POST'])
def ban_ip():
    if not session.get('admin'): return redirect(url_for('admin'))
    ip = request.form.get('ip')
    if ip and not BannedIP.query.filter_by(ip_address=ip).first():
        ban = BannedIP(ip_address=ip)
        db.session.add(ban)
        db.session.commit()
        socketio.emit('banned', {'ip': ip})
    return redirect(url_for('admin_dashboard'))

@app.route('/admin/unban/<int:id>', methods=['POST'])
def unban_ip(id):
    if not session.get('admin'): return redirect(url_for('admin'))
    ban = BannedIP.query.get(id)
    if ban:
        db.session.delete(ban)
        db.session.commit()
    return redirect(url_for('admin_dashboard'))

@app.route('/api/time', methods=['GET'])
def api_time():
    """Endpoint API pour obtenir l'heure actuelle"""
    return jsonify(get_current_time())

@app.route('/api/time/text', methods=['GET'])
def api_time_text():
    """Endpoint API pour obtenir l'heure en format texte lisible"""
    return jsonify({'time_text': get_current_time_text()})

# ===== AI API Security =====
# In-memory store — resets on server restart (lightweight, no Redis needed)
from collections import defaultdict

_ai_rate_data  = defaultdict(list)   # ip -> [timestamps of requests]
_ai_last_req   = {}                   # ip -> timestamp of last request
_ai_temp_ban   = {}                   # ip -> ban_expiry_timestamp
_ai_rate_lock  = Lock()

AI_RATE_LIMIT      = 8          # max requests per window
AI_RATE_WINDOW     = 60         # seconds for the window
AI_COOLDOWN_SEC    = 2          # min seconds between two requests
AI_BAN_THRESHOLD   = 20         # requests/min that trigger a temp ban
AI_BAN_DURATION    = 300        # temp ban duration in seconds
AI_MAX_PROMPT_LEN  = 500        # max characters in prompt

def get_real_ip():
    """Return the real client IP, respecting X-Forwarded-For if trusted."""
    xff = request.headers.get('X-Forwarded-For', '')
    if xff:
        return xff.split(',')[0].strip()
    return request.remote_addr or '0.0.0.0'

def check_ai_security():
    """
    Returns (allowed: bool, error_message: str, http_code: int).
    Call this at the top of every AI endpoint.
    """
    ip = get_real_ip()
    now = time.time()

    with _ai_rate_lock:
        # 1. Check permanent ban (BannedIP table)
        if BannedIP.query.filter_by(ip_address=ip).first():
            return False, "Acesso negado.", 403

        # 2. Check temporary ban
        if ip in _ai_temp_ban:
            remaining = int(_ai_temp_ban[ip] - now)
            if remaining > 0:
                return False, f"Demasiados pedidos. Tenta novamente em {remaining}s.", 429
            else:
                del _ai_temp_ban[ip]

        # 3. Cooldown — minimum delay between requests
        last = _ai_last_req.get(ip, 0)
        if now - last < AI_COOLDOWN_SEC:
            wait = round(AI_COOLDOWN_SEC - (now - last), 1)
            return False, f"Vai devagar! Aguarda {wait}s.", 429

        # 4. Sliding-window rate limit
        window_start = now - AI_RATE_WINDOW
        _ai_rate_data[ip] = [t for t in _ai_rate_data[ip] if t > window_start]
        count = len(_ai_rate_data[ip])

        # Auto temp-ban on extreme abuse
        if count >= AI_BAN_THRESHOLD:
            _ai_temp_ban[ip] = now + AI_BAN_DURATION
            _ai_rate_data[ip] = []
            return False, f"Abuso detectado. IP bloqueado por {AI_BAN_DURATION}s.", 429

        if count >= AI_RATE_LIMIT:
            return False, f"Limite de {AI_RATE_LIMIT} pedidos/minuto atingido. Aguarda um momento.", 429

        # 5. All good — register this request
        _ai_rate_data[ip].append(now)
        _ai_last_req[ip] = now

    return True, "", 200

def check_ai_origin():
    """
    Reject requests that don't come from the site itself.
    Checks Referer and X-Requested-With headers.
    """
    # Allow if request comes from same host (Referer check)
    referer = request.headers.get('Referer', '')
    origin  = request.headers.get('Origin', '')
    host    = request.host  # e.g. "localhost:5000" or "igradio.com"

    # If Referer or Origin is set, it must match our host
    if referer and host not in referer:
        return False
    if origin and host not in origin:
        return False

    # Must be a JSON POST with proper content-type
    ct = request.headers.get('Content-Type', '')
    if 'application/json' not in ct:
        return False

    return True

@app.route('/api/ai/ask', methods=['POST'])
def ai_ask():
    """Endpoint pour poser une question à l'IA (streaming SSE)"""

    # Origin / content-type guard
    if not check_ai_origin():
        return jsonify({'error': 'Acesso não autorizado.'}), 403

    # Rate limit + anti-spam
    allowed, msg, code = check_ai_security()
    if not allowed:
        return jsonify({'error': msg}), code

    data   = request.json or {}
    prompt = (data.get('prompt') or '').strip()
    user_ip = request.remote_addr

    if not prompt:
        return jsonify({'error': 'Prompt vazio.'}), 400

    # Prompt length limit
    if len(prompt) > AI_MAX_PROMPT_LEN:
        return jsonify({'error': f'Prompt demasiado longo (máx. {AI_MAX_PROMPT_LEN} caracteres).'}), 400

    # Save user message to chat history
    from models import ChatMessage
    user_msg = ChatMessage(user_ip=user_ip, role='user', content=prompt)
    db.session.add(user_msg)
    db.session.commit()

    def generate():
        full_response = ""
        try:
            for chunk in query_ollama_streaming(prompt, user_ip):
                if chunk:
                    full_response += chunk
                    yield f"data: {json_module.dumps({'response': chunk})}\n\n"
        except Exception as e:
            yield f"data: {json_module.dumps({'error': str(e)})}\n\n"
        
        # Save assistant response to chat history
        if full_response:
            try:
                assistant_msg = ChatMessage(user_ip=user_ip, role='assistant', content=full_response)
                db.session.add(assistant_msg)
                db.session.commit()
            except Exception as e:
                pass  # Silently fail to avoid breaking the stream

    headers = {
        'Content-Type': 'text/event-stream; charset=utf-8',
        'Cache-Control': 'no-cache, no-transform',
        'X-Accel-Buffering': 'no',
        'X-Robots-Tag': 'noindex'
    }
    return Response(stream_with_context(generate()), headers=headers)

@app.route('/api/ai/clear-history', methods=['POST'])
def clear_chat_history():
    """Clear chat history for current user"""
    from models import ChatMessage
    user_ip = request.remote_addr
    
    try:
        ChatMessage.query.filter_by(user_ip=user_ip).delete()
        db.session.commit()
        return jsonify({'success': True, 'message': 'Histórico de chat apagado.'})
    except Exception as e:
        db.session.rollback()
        return jsonify({'success': False, 'error': str(e)}), 500

@app.route('/api/ai/chat', methods=['POST'])
def ai_chat():
    """Endpoint pour le chat IA (réponse complète)"""
    data = request.json or {}
    prompt = (data.get('prompt') or '').strip()
    
    if not prompt:
        return jsonify({'error': 'prompt is required'}), 400
    
    try:
        response_text = ""
        for chunk in query_ollama_streaming(prompt):
            response_text += chunk
        
        return jsonify({
            'response': response_text,
            'is_profile_response': False
        })
    except Exception as e:
        return jsonify({'error': str(e)}), 500

@app.route('/api/ai/profile', methods=['GET'])
def ai_profile():
    """Endpoint pour obtenir le profil de l'IA"""
    profile = {
        "id": "ig-radio-ai-assistant",
        "name": "IG Radio AI Assistant",
        "version": "1.0",
        "model": OLLAMA_MODEL,
        "provider": "Ollama",
        "language": "Multilingual (French, English, Portuguese)",
        "capabilities": [
            "Answer questions",
            "Have conversations",
            "Provide information",
            "Creative writing",
            "Problem solving"
        ],
        "personality": "Friendly and helpful assistant for IG Radio",
        "created_at": "2026-05-26",
        "api_version": "1.0",
        "endpoints": {
            "ask": "/api/ai/ask (streaming)",
            "chat": "/api/ai/chat (complete response)",
            "profile": "/api/ai/profile"
        },
        "status": "online",
        "response_time": "varies",
        "description": "I am the IG Radio AI Assistant, powered by Qwen 2 0.5B. I'm here to help you with questions and conversations on the IG Radio platform."
    }
    return jsonify(profile)

@app.route('/api/recommend', methods=['POST'])
def recommend():
    data = request.json or {}
    title = (data.get('title') or '').strip()
    artist = (data.get('artist') or '').strip()
    email = (data.get('email') or '').strip()
    if not title or not artist or not email:
        return jsonify({'success': False, 'error': 'title, artist and email are required'}), 400

    rec = Recommendation(
        title=title,
        artist=artist,
        email=email,
        user_ip=request.remote_addr,
        status='pending'
    )
    db.session.add(rec)
    db.session.commit()
    return jsonify({'success': True})

@app.route('/api/song_vote', methods=['POST'])
def song_vote():
    data = request.json or {}
    song_id = data.get('song_id')
    vote_type = (data.get('vote_type') or '').strip().lower()
    if not song_id or vote_type not in ('like', 'dislike'):
        return jsonify({'success': False, 'error': 'song_id and vote_type are required'}), 400

    song = Song.query.get(song_id)
    if not song:
        return jsonify({'success': False, 'error': 'song not found'}), 404

    user_ip = request.remote_addr or 'unknown'
    existing_vote = SongVote.query.filter_by(song_id=song_id, user_ip=user_ip).first()
    if existing_vote:
        if existing_vote.vote_type == vote_type:
            db.session.delete(existing_vote)
            vote_type = None
        else:
            existing_vote.vote_type = vote_type
    else:
        db.session.add(SongVote(song_id=song_id, user_ip=user_ip, vote_type=vote_type))
    db.session.commit()

    likes = SongVote.query.filter_by(song_id=song_id, vote_type='like').count()
    dislikes = SongVote.query.filter_by(song_id=song_id, vote_type='dislike').count()
    return jsonify({'success': True, 'likes': likes, 'dislikes': dislikes, 'vote_type': vote_type})

@app.route('/admin/recommend/<int:id>/<action>', methods=['POST'])
def handle_recommendation(id, action):
    email_logger.info("Recommendation route called: id=%s action=%s method=%s", id, action, request.method)
    if not session.get('admin'): 
        email_logger.warning("Recommendation route denied (not admin): id=%s action=%s", id, action)
        return redirect(url_for('admin'))
    rec = Recommendation.query.get(id)
    if not rec:
        email_logger.warning("Recommendation not found: id=%s action=%s", id, action)
        return redirect(url_for('admin_dashboard'))

    email_logger.info("Recommendation decision: id=%s action=%s recipient=%s", rec.id, action, rec.email)
    if action == 'accept':
        rec.status = 'accepted'
        db.session.commit()
        email_logger.info("Recommendation accepted: id=%s", rec.id)
        send_email(
            rec.email, 
            "Recomendação Aceite - IG Radio",
            f"Olá!\n\nA sua recomendação '{rec.title}' de '{rec.artist}' foi aceite pelo administrador e em breve será tocada na rádio.\n\nObrigado por participar!"
        )
    elif action == 'reject':
        reason = (request.form.get('reason') or '').strip()
        rec.status = 'rejected'
        rec.rejection_reason = reason
        db.session.commit()
        email_logger.info("Recommendation rejected: id=%s reason=%s", rec.id, reason or "none")
        send_email(
            rec.email, 
            "Recomendação Rejeitada - IG Radio",
            f"Olá.\n\nLamentamos informar que a sua recomendação '{rec.title}' de '{rec.artist}' não foi aceite.\n\nRazão: {reason or 'Não especificada.'}\n\nObrigado por participar!"
        )
    else:
        email_logger.warning("Recommendation invalid action: id=%s action=%s", rec.id, action)
    return redirect(url_for('admin_dashboard'))

# ── Blog Routes ─────────────────────────────────────────────────────────────

@app.route('/blog')
def blog_index():
    posts = BlogPost.query.order_by(BlogPost.published_at.desc()).all()
    return render_template('blog.html', posts=posts)

@app.route('/blog/<int:post_id>')
def blog_post(post_id):
    post = BlogPost.query.get_or_404(post_id)
    links = json.loads(post.links) if post.links else []
    return render_template('blog_post.html', post=post, links=links)

@app.route('/privacy')
def privacy():
    return render_template('privacy.html')

@app.route('/terms')
def terms():
    return render_template('terms.html')

@app.route('/admin/blog/create', methods=['POST'])
def blog_create():
    if not session.get('admin'): return redirect(url_for('admin'))
    title = (request.form.get('title') or '').strip()
    description = (request.form.get('description') or '').strip()
    if not title or not description:
        return redirect(url_for('admin_dashboard'))

    banner_filename = None
    banner_file = request.files.get('banner')
    if banner_file and banner_file.filename:
        ext = os.path.splitext(secure_filename(banner_file.filename))[1].lower()
        if ext in ('.jpg', '.jpeg', '.png', '.gif', '.webp'):
            banner_filename = f"blog_{int(time.time())}{ext}"
            banner_file.save(os.path.join(app.config['IMAGE_FOLDER'], banner_filename))

    # Parse optional links (label|url pairs sent as repeated fields)
    link_labels = request.form.getlist('link_label')
    link_urls   = request.form.getlist('link_url')
    links_data  = [{'label': l.strip(), 'url': u.strip()}
                   for l, u in zip(link_labels, link_urls)
                   if l.strip() and u.strip()]

    post = BlogPost(
        title=title,
        description=description,
        banner_filename=banner_filename,
        links=json.dumps(links_data, ensure_ascii=False) if links_data else None
    )
    db.session.add(post)
    db.session.commit()
    return redirect(url_for('admin_dashboard'))

@app.route('/admin/blog/delete/<int:post_id>', methods=['POST'])
def blog_delete(post_id):
    if not session.get('admin'): return redirect(url_for('admin'))
    post = BlogPost.query.get_or_404(post_id)
    if post.banner_filename:
        try:
            os.remove(os.path.join(app.config['IMAGE_FOLDER'], post.banner_filename))
        except Exception:
            pass
    db.session.delete(post)
    db.session.commit()
    return redirect(url_for('admin_dashboard'))

@app.route('/admin/blog/edit/<int:post_id>', methods=['POST'])
def blog_edit(post_id):
    if not session.get('admin'): return redirect(url_for('admin'))
    post = BlogPost.query.get_or_404(post_id)
    title = (request.form.get('title') or '').strip()
    description = (request.form.get('description') or '').strip()
    if title:
        post.title = title
    if description:
        post.description = description

    banner_file = request.files.get('banner')
    if banner_file and banner_file.filename:
        ext = os.path.splitext(secure_filename(banner_file.filename))[1].lower()
        if ext in ('.jpg', '.jpeg', '.png', '.gif', '.webp'):
            if post.banner_filename:
                try:
                    os.remove(os.path.join(app.config['IMAGE_FOLDER'], post.banner_filename))
                except Exception:
                    pass
            new_name = f"blog_{int(time.time())}{ext}"
            banner_file.save(os.path.join(app.config['IMAGE_FOLDER'], new_name))
            post.banner_filename = new_name

    link_labels = request.form.getlist('link_label')
    link_urls   = request.form.getlist('link_url')
    links_data  = [{'label': l.strip(), 'url': u.strip()}
                   for l, u in zip(link_labels, link_urls)
                   if l.strip() and u.strip()]
    post.links = json.dumps(links_data, ensure_ascii=False) if links_data else None

    db.session.commit()
    return redirect(url_for('admin_dashboard'))

# ─────────────────────────────────────────────────────────────────────────────

@socketio.on('connect')
def handle_connect(auth=None):
    ip = request.remote_addr
    with app.app_context():
        banned = BannedIP.query.filter_by(ip_address=ip).first()
        if banned:
            return False
    listeners[request.sid] = ip
    emit('radio_update', get_radio_status())
    update_admin_stats()

@socketio.on('disconnect')
def handle_disconnect():
    if request.sid in listeners:
        del listeners[request.sid]
    update_admin_stats()

def update_admin_stats():
    unique_ips = list(set(listeners.values()))
    count = len(listeners)
    print(f"DEBUG: Updating admin stats. Listeners: {count}")
    socketio.emit('admin_stats', {'listener_count': count, 'ips': unique_ips})
    
def log_listeners():
    """Background task to log listener count every 5 minutes."""
    # Log immediately on start
    try:
        with app.app_context():
            count = len(listeners)
            db.session.add(ListenerStat(count=count))
            db.session.commit()
    except: pass

    while True:
        socketio.sleep(300) 
        try:
            with app.app_context():
                count = len(listeners)
                stat = ListenerStat(count=count)
                db.session.add(stat)
                db.session.commit()
        except Exception as e:
            print(f"Error logging listeners: {e}")

@app.route('/admin/poll/add', methods=['POST'])
def poll_add():
    if not session.get('admin'): return redirect(url_for('admin'))
    title = (request.form.get('title') or '').strip()
    artist = (request.form.get('artist') or 'Desconhecido').strip()
    if title:
        ps = PollSong(title=title, artist=artist)
        db.session.add(ps)
        db.session.commit()
    return redirect(url_for('admin_dashboard'))

@app.route('/admin/poll/delete/<int:id>', methods=['POST'])
def poll_delete(id):
    if not session.get('admin'): return redirect(url_for('admin'))
    ps = PollSong.query.get_or_404(id)
    PollVote.query.filter_by(poll_song_id=id).delete()
    db.session.delete(ps)
    db.session.commit()
    return redirect(url_for('admin_dashboard'))

@app.route('/api/poll/list')
def poll_list():
    songs = PollSong.query.order_by(PollSong.votes.desc()).all()
    user_ip = request.remote_addr
    voted_ids = [v.poll_song_id for v in PollVote.query.filter_by(user_ip=user_ip).all()]
    has_voted_any = len(voted_ids) > 0
    
    return jsonify({
        'success': True,
        'has_voted_any': has_voted_any,
        'songs': [{
            'id': s.id,
            'title': s.title,
            'artist': s.artist,
            'votes': s.votes,
            'voted': s.id in voted_ids
        } for s in songs]
    })

@app.route('/api/poll/vote/<int:song_id>', methods=['POST'])
def poll_vote(song_id):
    user_ip = request.remote_addr
    song = PollSong.query.get_or_404(song_id)
    
    # Check if user has already voted for THIS song (Unvote logic)
    existing = PollVote.query.filter_by(poll_song_id=song_id, user_ip=user_ip).first()
    if existing:
        db.session.delete(existing)
        song.votes = max(0, song.votes - 1)
        db.session.commit()
        return jsonify({'success': True, 'action': 'unvoted', 'new_votes': song.votes})

    # Check if user has voted for ANY OTHER song
    any_voted = PollVote.query.filter_by(user_ip=user_ip).first()
    if any_voted:
        return jsonify({'success': False, 'error': 'Já votaste noutra música. Retira o teu voto primeiro.'})
    
    # Add vote
    vote = PollVote(poll_song_id=song_id, user_ip=user_ip)
    song.votes += 1
    db.session.add(vote)
    db.session.commit()
    return jsonify({'success': True, 'action': 'voted', 'new_votes': song.votes})

@app.route('/api/stats/listeners')
def get_listener_stats():
    """Returns listener counts aggregated by hour for the last 24h."""
    now = datetime.utcnow()
    last_24h = now - timedelta(hours=24)
    
    stats = (
        db.session.query(
            db.func.strftime('%Y-%m-%d %H:00:00', ListenerStat.timestamp).label('hour'),
            db.func.max(ListenerStat.count).label('max_listeners')
        )
        .filter(ListenerStat.timestamp >= last_24h)
        .group_by('hour')
        .order_by('hour')
        .all()
    )
    
    return jsonify({
        'success': True,
        'stats': [{'hour': row.hour, 'count': row.max_listeners} for row in stats]
    })


@app.route('/api/station_detection', methods=['POST'])
def create_station_detection():
    data = request.json or {}
    frequency = data.get('frequency')
    quality = data.get('quality', 0)
    source = (data.get('source') or '').strip() or None
    detected_at_input = (data.get('detected_at') or '').strip()

    if frequency is None:
        return jsonify({'success': False, 'error': 'frequency is required'}), 400

    try:
        frequency_value = float(frequency)
    except (TypeError, ValueError):
        return jsonify({'success': False, 'error': 'invalid frequency'}), 400

    try:
        quality_value = float(quality)
    except (TypeError, ValueError):
        return jsonify({'success': False, 'error': 'invalid quality'}), 400

    detected_at = datetime.utcnow()
    if detected_at_input:
        try:
            detected_at = datetime.fromisoformat(detected_at_input.replace('Z', '+00:00')).replace(tzinfo=None)
        except ValueError:
            return jsonify({'success': False, 'error': 'invalid detected_at, expected ISO8601'}), 400

    event = StationDetectionEvent(
        station_key=normalize_frequency(frequency_value),
        frequency=frequency_value,
        quality=quality_value,
        source=source,
        detected_at=detected_at
    )
    db.session.add(event)
    db.session.commit()
    return jsonify({'success': True, 'event_id': event.id})


@app.route('/api/stations/history', methods=['GET'])
def stations_history():
    limit = request.args.get('limit', default=30, type=int)
    limit = max(1, min(limit, 200))

    # Aggregate by station_key to expose frequency, last heard and quality stats.
    rows = (
        db.session.query(
            StationDetectionEvent.station_key.label('station_key'),
            db.func.avg(StationDetectionEvent.frequency).label('frequency'),
            db.func.max(StationDetectionEvent.detected_at).label('last_detected_at'),
            db.func.avg(StationDetectionEvent.quality).label('avg_quality'),
            db.func.max(StationDetectionEvent.quality).label('peak_quality'),
            db.func.count(StationDetectionEvent.id).label('detections')
        )
        .group_by(StationDetectionEvent.station_key)
        .order_by(db.func.max(StationDetectionEvent.detected_at).desc())
        .limit(limit)
        .all()
    )

    payload = []
    for row in rows:
        payload.append({
            'station_key': row.station_key,
            'frequency': round(float(row.frequency or 0), 3),
            'last_detected_at': row.last_detected_at.isoformat() if row.last_detected_at else None,
            'last_heard_human': format_last_heard(row.last_detected_at),
            'avg_quality': round(float(row.avg_quality or 0), 2),
            'peak_quality': round(float(row.peak_quality or 0), 2),
            'detections': int(row.detections or 0)
        })

    return jsonify({'success': True, 'stations': payload})


@app.route('/api/stations/<station_key>/timeline', methods=['GET'])
def station_timeline(station_key):
    events = (
        StationDetectionEvent.query
        .filter_by(station_key=station_key)
        .order_by(StationDetectionEvent.detected_at.asc())
        .all()
    )
    if not events:
        return jsonify({'success': False, 'error': 'station not found'}), 404

    by_hour = [0 for _ in range(24)]
    by_weekday = {label: 0 for label in WEEKDAY_LABELS}

    for event in events:
        by_hour[event.detected_at.hour] += 1
        by_weekday[WEEKDAY_LABELS[event.detected_at.weekday()]] += 1

    return jsonify({
        'success': True,
        'station_key': station_key,
        'frequency': round(float(sum(event.frequency for event in events) / len(events)), 3),
        'total_detections': len(events),
        'last_detected_at': events[-1].detected_at.isoformat(),
        'last_heard_human': format_last_heard(events[-1].detected_at),
        'timeline': {
            'by_hour': by_hour,
            'by_weekday': by_weekday
        },
        'events': [{
            'detected_at': e.detected_at.isoformat(),
            'quality': e.quality,
            'source': e.source
        } for e in events[-50:]] # Return last 50 events
    })

if __name__ == '__main__':
    with app.app_context():
        db.create_all()
        ensure_recommendation_columns()
    load_state()
    socketio.start_background_task(radio_thread)
    socketio.start_background_task(log_listeners)
    socketio.run(app, host='0.0.0.0', port=5005, debug=True, use_reloader=False, allow_unsafe_werkzeug=True)
