import uuid
from dataclasses import asdict

import dotenv
from connect import get_connection
from flask import Flask, Response, jsonify, request
from flask_cors import CORS
from flask_socketio import SocketIO, join_room, leave_room
from gps import Coordinates, distance_between_coords
from qrcode_gen import generate_qr
from room import Room
from song import Song, add_song_in_db, get_song_by_title_artist, get_song_by_uuid, init_db
from song_fetch import query_search, yt_get_audio_url, yt_search_song
from state import State

dotenv.load_dotenv()


app = Flask(__name__)
app.config["SECRET_KEY"] = "your_secret_key"
socketio = SocketIO(app, cors_allowed_origins="*", path="/ws")
CORS(app)


db_conn = get_connection()
state = State(app, socketio, db_conn.cursor())
init_db(state.db)

state.rooms[1000] = Room(
    id=1000,
    coord=Coordinates(46.6769043, 11.1851585),
    name="Test Room",
    pin=None,
    tags=set(),
    range_size=150,
    songs={},
    history=[],
    playing=[],
    playing_idx=0,
)


def error(msg: str, status: int = 400) -> Response:
    res = jsonify({"success": False, "error": msg})
    res.status_code = status
    return res


@socketio.on("connect")
def handle_connection():
    print("somebody connected to socket.io", flush=True)


@socketio.on("disconnect")
def handle_disconnection():
    print("somebody disconnected from socket.io", flush=True)


@socketio.on("join_room")
def on_join(data):
    room = data["id"]
    join_room(room)
    print(f"somebody joined {room=}", flush=True)


@socketio.on("leave_room")
def on_leave(data):
    room = data["id"]
    leave_room(room)
    print(f"somebody left {room=}", flush=True)


@app.get("/api/join")
def join():
    room_id = request.args.get("room")
    code = request.args.get("code")

    if room_id is None:
        return error("Missing room id")

    if (room := state.rooms.get(int(room_id))) is None:
        return error("Invalid room")

    if room.pin is not None and room.pin != code:
        return error("Invalid code")

    distance = distance_between_coords(
        lhs=room.coord,
        rhs=Coordinates(
            latitude=float(request.args["lat"]),
            longitude=float(request.args["lon"]),
        ),
    )

    if distance > room.range_size:
        return error("You are not within the room range")

    return {"success": True, "ws": f"/ws/{room_id}"}


@app.get("/api/queue")
def queue():
    if (room_id := request.args.get("room")) is None:
        return error("Missing room id")

    if (room := state.rooms.get(int(room_id))) is None:
        return error("Invalid room")

    return {"success": True, "queue": room.playing, "index": room.playing_idx}


@app.post("/api/queue/next")
def queue_next():
    if (room_id := request.args.get("room")) is None:
        return error("Missing room id")

    if (room := state.rooms.get(int(room_id))) is None:
        return error("Invalid room")

    room.playing_idx += 1

    if room.playing_idx >= len(room.playing):
        ## queue ended
        room.renew_queue()
        ended = True
    else:
        ended = False

    data = {"success": True, "ended": ended, "index": room.playing_idx, "queue": [asdict(s) for s in room.playing]}
    state.socketio.emit("queue_update", data, to=str(room.id))

    return data


@app.post("/api/room/new")
def room_new():
    if (room_name := request.args.get("name")) is None:
        return error("Missing room name")

    if (room_cords := request.args.get("coords")) is None:
        return error("Missing room coords")

    if (room_range := request.args.get("range")) is None:
        return error("Missing room range")

    if room_pin := request.args.get("pin"):
        room_pin = int(room_pin)
    else:
        room_pin = None

    lat, lon = room_cords.split(",")

    room = Room(
        id=max(state.rooms or [0]) + 1,
        coord=Coordinates(float(lat), float(lon)),
        range_size=int(room_range),
        name=room_name,
        pin=room_pin,
        tags=set([tag for tag in request.args.get("tags", "").split(",") if tag]),
        songs={},
        history=[],
        playing=[],
        playing_idx=-1,
    )

    state.rooms[room.id] = room

    return {"success": True, "room_id": room.id}


@app.get("/api/room")
def room():
    lat = request.args.get("lat")
    lon = request.args.get("lon")

    if lat and lon:
        user_coords = Coordinates(latitude=float(lat), longitude=float(lon))
    else:
        return error("Missing user coordinates")

    return [
        {
            "id": room.id,
            "name": room.name,
            "private": room.pin is not None,
            "coords": room.coord,
            "range": room.range_size,
            "distance": d,
        }
        for room in state.rooms.values()
        if (d := distance_between_coords(user_coords, room.coord)) <= room.range_size
    ]


@app.post("/api/addsong")
def add_song():
    if (room_id := request.args.get("room")) is None:
        return error("Missing room id")

    if (room := state.rooms.get(int(room_id))) is None:
        return error("Invalid room")

    if (query := request.args.get("query")) is None:
        return error("Missing query")

    if (info := query_search(query)) is None:
        return error("Search failed")

    if (song := get_song_by_title_artist(info.title, info.artist)) is None:
        ## song not found, downolad from YT
        yt_video_id = yt_search_song(info.title, info.artist)

        if yt_video_id is None:
            return error("No video found on youtube")

        ## add in DB
        song = Song(
            uuid=str(uuid.uuid4()),
            title=info.title,
            artist=info.artist,
            tags=info.tags,
            image_id=info.img_id,
            youtube_id=yt_video_id,
        )

        add_song_in_db(song)

    if len(room.tags) > 0:
        tag_ok = False
        for tag in song.tags:
            if tag in room.tags:
                tag_ok = True

        if not tag_ok:
            return error("Song genre does not belong to this room")

    ## add the song in the room if does not exists
    if song.uuid not in room.songs:
        room.songs[song.uuid] = (song, 1)  # start with one vote
        socketio.emit("new_song", {"song": asdict(song) | {"upvote": 1}}, to=str(room.id))

    return {"success": True, "song": song}


@app.get("/api/room/suggestions")
def get_room_suggestion():
    if (room_id := request.args.get("room")) is None:
        return error("Missing room id")

    if (room := state.rooms.get(int(room_id))) is None:
        return error("Invalid room id")

    return {"success": True, "songs": [asdict(song) | {"upvote": score} for song, score in room.songs.values()]}


@app.post("/api/song/voting")
def post_song_vote():
    if (room_id := request.args.get("room")) is None:
        return error("Missing room id")

    if (room := state.rooms.get(int(room_id))) is None:
        return error("Invalid room id")

    if (song_id := request.args.get("song")) is None:
        return error("Missing song id")

    if (song_info := room.songs.get(song_id)) is None:
        return error("Invalid song id")

    ## update the song
    room.songs[song_id] = (song_info[0], song_info[1] + int(request.args["increment"]))
    socketio.emit("new_vote", {"song": asdict(song_info[0]) | {"upvote": song_info[1]}})

    return {"success": True}


@app.get("/api/room/qrcode")
def room_qrcode():
    if (room_id := request.args.get("room")) is None:
        return error("Missing room id")

    if (pin := request.args.get("pin")) is not None:
        pin = int(pin)

    stream = generate_qr(
        base_uri="https://chillbox.leoinvents.com",
        room_id=int(room_id),
        pin=pin,
    )

    return Response(stream, content_type="image/jpeg")


@app.get("/api/song/audio")
def get_audio_url():
    if (song_uuid := request.args.get("song")) is None:
        return error("Missing song id")

    if (song := get_song_by_uuid(song_uuid)) is None:
        return error("Song not found")

    if (url := yt_get_audio_url(song.youtube_id)) is None:
        return error("Cannot get audio url")

    return {"success": True, "url": url}


if __name__ == "__main__":
    socketio.run(app, host="0.0.0.0", port=5000, debug=True)
