#!/usr/bin/env python3
"""Radio DJ — smart track picker for Liquidsoap.
Prints one absolute file path to stdout and exits.
All logging goes to /var/lib/radio/dj.log.

This is the per-track fallback. The preferred approach is generate_daily_playlist.py
which pre-generates a full day's playlist.
"""

import json
import logging
import logging.handlers
import os
import random
import sqlite3
import sys
import time
from pathlib import Path

sys.path.insert(0, os.path.dirname(__file__))
import dj_core

# ── Logging ────────────────────────────────────────────────────────────────────
LOG_PATH = "/var/lib/radio/dj.log"
os.makedirs(os.path.dirname(LOG_PATH), exist_ok=True)
log = logging.getLogger("radio_dj")
log.setLevel(logging.DEBUG)
handler = logging.handlers.RotatingFileHandler(LOG_PATH, maxBytes=1_000_000, backupCount=3)
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
log.addHandler(handler)

HISTORY_PRUNE = 7 * 86400


# ── State DB ───────────────────────────────────────────────────────────────────

def init_state_db():
    try:
        conn = sqlite3.connect(dj_core.STATE_DB, timeout=5)
        conn.execute("PRAGMA journal_mode=WAL")
        conn.row_factory = sqlite3.Row
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS history (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                path TEXT NOT NULL, artist TEXT, album TEXT,
                genre TEXT, energy INTEGER, mode TEXT, timestamp REAL NOT NULL
            );
            CREATE TABLE IF NOT EXISTS album_queue (
                id INTEGER PRIMARY KEY CHECK (id = 1),
                album TEXT, tracks TEXT, current_index INTEGER DEFAULT 0
            );
            CREATE INDEX IF NOT EXISTS idx_history_timestamp ON history(timestamp);
            CREATE INDEX IF NOT EXISTS idx_history_artist ON history(artist);
            CREATE INDEX IF NOT EXISTS idx_history_path ON history(path);
        """)
        conn.commit()
        return conn
    except Exception as e:
        log.error("State DB corrupt, recreating: %s", e)
        try:
            os.remove(dj_core.STATE_DB)
        except OSError:
            pass
        conn = sqlite3.connect(dj_core.STATE_DB, timeout=5)
        conn.execute("PRAGMA journal_mode=WAL")
        conn.row_factory = sqlite3.Row
        conn.executescript("""
            CREATE TABLE IF NOT EXISTS history (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                path TEXT NOT NULL, artist TEXT, album TEXT,
                genre TEXT, energy INTEGER, mode TEXT, timestamp REAL NOT NULL
            );
            CREATE TABLE IF NOT EXISTS album_queue (
                id INTEGER PRIMARY KEY CHECK (id = 1),
                album TEXT, tracks TEXT, current_index INTEGER DEFAULT 0
            );
            CREATE INDEX IF NOT EXISTS idx_history_timestamp ON history(timestamp);
            CREATE INDEX IF NOT EXISTS idx_history_artist ON history(artist);
            CREATE INDEX IF NOT EXISTS idx_history_path ON history(path);
        """)
        conn.commit()
        return conn


def prune_history(state):
    cutoff = time.time() - HISTORY_PRUNE
    state.execute("DELETE FROM history WHERE timestamp < ?", (cutoff,))
    state.commit()


def get_cooldowns(state):
    now = time.time()
    rows = state.execute(
        "SELECT path, artist, album, genre, timestamp FROM history ORDER BY timestamp DESC"
    ).fetchall()

    cooled_paths = set()
    cooled_artists = set()
    cooled_albums = set()
    last_genre = None

    for r in rows:
        age = now - r["timestamp"]
        if age < dj_core.TRACK_COOLDOWN:
            cooled_paths.add(r["path"])
        if age < dj_core.ARTIST_COOLDOWN and r["artist"]:
            cooled_artists.add(r["artist"].lower())
        if age < dj_core.ALBUM_COOLDOWN and r["album"]:
            cooled_albums.add(r["album"].lower())

    if rows:
        last_genre = dj_core.normalize_genre(rows[0]["genre"]) if rows[0]["genre"] else None

    return cooled_paths, cooled_artists, cooled_albums, last_genre


def get_recent_energies(state, n=3):
    rows = state.execute(
        "SELECT energy FROM history ORDER BY timestamp DESC LIMIT ?", (n,)
    ).fetchall()
    return [r["energy"] for r in rows if r["energy"] is not None]


def record_pick(state, path, artist, album, genre, energy, mode):
    state.execute(
        "INSERT INTO history (path, artist, album, genre, energy, mode, timestamp) VALUES (?,?,?,?,?,?,?)",
        (path, artist, album, genre, energy, mode, time.time()),
    )
    state.commit()


# ── Album queue ────────────────────────────────────────────────────────────────

def get_album_queue(state):
    row = state.execute("SELECT album, tracks, current_index FROM album_queue WHERE id = 1").fetchone()
    if row and row["tracks"]:
        try:
            tracks = json.loads(row["tracks"])
            return row["album"], tracks, row["current_index"] or 0
        except (json.JSONDecodeError, TypeError):
            pass
    return None, [], 0


def set_album_queue(state, album, tracks, idx):
    state.execute(
        "INSERT OR REPLACE INTO album_queue (id, album, tracks, current_index) VALUES (1, ?, ?, ?)",
        (album, json.dumps(tracks), idx),
    )
    state.commit()


def clear_album_queue(state):
    state.execute("DELETE FROM album_queue WHERE id = 1")
    state.commit()


# ── Main ───────────────────────────────────────────────────────────────────────

def main():
    try:
        now = time.time()
        hour = time.localtime().tm_hour
        target_energy = dj_core.ENERGY_CURVE.get(hour, 5)

        state = init_state_db()
        prune_history(state)

        # ── Check album queue first ───────────────────────────────────
        album_name, album_tracks, album_idx = get_album_queue(state)
        if album_tracks and album_idx < len(album_tracks):
            next_path = album_tracks[album_idx]
            if os.path.isfile(next_path):
                set_album_queue(state, album_name, album_tracks, album_idx + 1)
                all_tracks = dj_core.load_tracks()
                meta = next((t for t in all_tracks if t["path"] == next_path), None)
                if meta:
                    record_pick(state, next_path, meta["artist"], meta["album"],
                                meta["genre"], meta["energy"], "full_album")
                    log.info("ALBUM [%d/%d] %s — %s — %s (energy=%d)",
                             album_idx + 1, len(album_tracks),
                             meta["artist"], meta["title"], meta["album"], meta["energy"])
                else:
                    record_pick(state, next_path, "", album_name, "", 5, "full_album")
                    log.info("ALBUM [%d/%d] %s", album_idx + 1, len(album_tracks), next_path)

                if album_idx + 1 >= len(album_tracks):
                    clear_album_queue(state)
                    log.info("Album complete, resuming normal mode")

                state.close()
                print(next_path)
                return
            else:
                log.warning("Album track missing: %s, clearing queue", next_path)
                clear_album_queue(state)

        # ── Load data ─────────────────────────────────────────────────
        all_tracks = dj_core.load_tracks()
        if not all_tracks:
            log.error("No tracks found anywhere!")
            fallback = dj_core.random_file_fallback()
            if fallback:
                print(fallback)
            state.close()
            return

        nav_data = dj_core.load_navidrome()
        cooled_paths, cooled_artists, cooled_albums, last_genre = get_cooldowns(state)
        recent_energies = get_recent_energies(state)

        log.info("Loaded %d tracks, %d with play data, target energy=%d (hour=%d)",
                 len(all_tracks), len(nav_data), target_energy, hour)

        # ── Energy filter ─────────────────────────────────────────────
        pool = dj_core.filter_by_energy(all_tracks, target_energy)
        log.info("Energy pool: %d tracks (target=%d ±2)", len(pool), target_energy)

        # ── Cooldown filter ───────────────────────────────────────────
        pool = dj_core.apply_cooldowns(pool, cooled_paths, cooled_artists, cooled_albums, last_genre)
        log.info("After cooldowns: %d tracks", len(pool))

        if not pool:
            log.warning("Pool empty after cooldowns, relaxing artist/album cooldowns")
            pool = dj_core.filter_by_energy(all_tracks, target_energy)
            pool = [t for t in pool if t["path"] not in cooled_paths]
            if not pool:
                log.warning("Still empty, using all tracks minus path cooldown")
                pool = [t for t in all_tracks if t["path"] not in cooled_paths]
            if not pool:
                pool = all_tracks

        # ── Mode selection ────────────────────────────────────────────
        mode = dj_core.pick_mode()
        log.info("Mode roll: %s", mode)

        # Handle full_album
        if mode == "full_album":
            multi = dj_core.find_album_tracks(pool)
            if multi:
                chosen_key = random.choice(list(multi.keys()))
                album_tks = multi[chosen_key]
                paths = [t["path"] for t in album_tks]
                first = album_tks[0]
                set_album_queue(state, first["album"], paths, 1)
                record_pick(state, first["path"], first["artist"], first["album"],
                            first["genre"], first["energy"], "full_album")
                log.info("FULL ALBUM: %s — %s (%d tracks, energy=%d)",
                         first["artist"], first["album"], len(paths), first["energy"])
                state.close()
                print(first["path"])
                return
            else:
                log.info("full_album: no multi-track albums in pool, falling back to shuffle")
                mode = "shuffle"

        # ── Apply mode filter ─────────────────────────────────────────
        mode_pool, actual_mode = dj_core.apply_mode(pool, mode, nav_data, now)
        log.info("Mode %s → %d candidates", actual_mode, len(mode_pool))

        # ── Contrast pick ─────────────────────────────────────────────
        pick = dj_core.contrast_pick(mode_pool, recent_energies)

        # ── Verify file exists ────────────────────────────────────────
        if not os.path.isfile(pick["path"]):
            log.warning("Selected file missing: %s, trying another", pick["path"])
            mode_pool = [t for t in mode_pool if os.path.isfile(t["path"])]
            if mode_pool:
                pick = random.choice(mode_pool)
            else:
                fallback = dj_core.random_file_fallback()
                if fallback:
                    print(fallback)
                state.close()
                return

        # ── Record and output ─────────────────────────────────────────
        record_pick(state, pick["path"], pick["artist"], pick["album"],
                    pick["genre"], pick["energy"], actual_mode)
        log.info("PICK: [%s] %s — %s — %s (genre=%s, energy=%d, pool=%d)",
                 actual_mode, pick["artist"], pick["title"], pick["album"],
                 pick["genre"], pick["energy"], len(mode_pool))

        state.close()
        print(pick["path"])

    except Exception as e:
        log.exception("Fatal error: %s", e)
        fallback = dj_core.random_file_fallback()
        if fallback:
            print(fallback)
        else:
            print("/dev/null")


if __name__ == "__main__":
    main()
