# encoding=utf-8

"""Web API for ardj.

Lets HTTP clients access the database.
"""

import json
import mimetypes
import os
import re
import sys
import time
import traceback

import jinja2
import web

from ardj import get_data_path
from ardj import config
from ardj.database import connect, commit
from ardj.listeners import get_count
from ardj.log import *
from ardj.models import *
from ardj.util import utf, skip_current_track, format_duration, format_age, \
    get_user_path, get_music_path
from ardj.tracks import add_vote

import auth
import console
import tracks
import log
from users import get_admins
from util import skip_current_track


def get_web_path(path):
    return get_data_path("webroot/" + path)


def send_json(f):
    """The @send_json decorator, encodes the return value in JSON."""
    def wrapper(*args, **kwargs):
        web.header("Access-Control-Allow-Origin", "*")
        try:
            data = f(*args, **kwargs)
        except Exception, e:
            data = {
                "success": False,
                "error": unicode(e),
                "error_type": e.__class__.__name__,
            }

        if web.ctx.env["PATH_INFO"].endswith(".js"):
            var_name = "response"
            callback_name = None

            for part in web.ctx.env["QUERY_STRING"].split("&"):
                if part.startswith("var="):
                    var_name = part[4:]
                elif part.startswith("callback="):
                    callback_name = part[9:]

            web.header("Content-Type", "application/javascript; charset=UTF-8")
            if callback_name is not None:
                return "var %s = %s; %s(%s);" % (var_name, json.dumps(data), callback_name, var_name)
            return "var %s = %s;" % (var_name, json.dumps(data))
        else:
            web.header("Content-Type", "application/json; charset=UTF-8")
            return json.dumps(data, ensure_ascii=False, indent=True)
    return wrapper


class UsageError(RuntimeError):
    pass


class Templates(object):
    env = None

    @classmethod
    def get_instance(cls):
        if cls.env is None:
            folder = get_web_path("templates")
            loader = jinja2.FileSystemLoader(folder)
            cls.env = jinja2.Environment(loader=loader)
        return cls.env

    @classmethod
    def render(cls, template_name, data=None):
        if data is None:
            data = {}

        elif not isinstance(data, dict):
            raise ValueError("template data must be a dict")

        # TODO: move to config
        data["site_name"] = "ardj"

        log_debug("Rendering template {}", template_name)

        tpl = cls.get_instance().get_template(template_name)
        return tpl.render(data)


class Controller:
    """
    Basic request handler.
    Adds exception handling and some handy methods.
    """
    def GET(self, *args):
        db = connect()
        try:
            return self.on_get(*args)
        except Exception, e:
            return self.on_exception(e)

    def POST(self, *args):
        db = connect()
        try:
            return self.on_post(*args)
        except Exception, e:
            return self.on_exception(e)

    def on_exception(self, e):
        log_exception("%s: %s -- %s" % (e.__class__.__name__, e, web.ctx.path))
        return self.unavailable(str(e) + "\n\n" + traceback.format_exc())

    def send_file(self, path):
        if not os.path.exists(path):
            log_debug("web: file {} not found", path)
            return self.notfound("file not found")

        elif not os.path.isfile(path):
            log_debug("web: {} is not a file", path)
            return self.notfound("not a file")

        ct = mimetypes.guess_type(path)[0]
        if ct == "text/html":
            ct += "; charset=utf-8"

        web.header("Content-Type", ct)
        web.ctx.status = "200 OK"

        with open(path, "rb") as f:
            return f.read()

    def send_json(self, data):
        encoded_data = json.dumps(data)

        web.header("Content-Type", "application/javascript; charset=utf-8")
        web.header("Access-Control-Allow-Origin", "*")

        return encoded_data

    def render_page(self, template, data=None, status="200 OK"):
        html = Templates.render(template, data)

        web.header("Content-Type", "text/html; charset=utf-8")
        web.ctx.status = status

        return html

    def notfound(self, message):
        web.ctx.status = "404 Not Found"
        web.header("Content-Type", "text/plain; charset=utf-8")
        return utf(message)

    def unavailable(self, message):
        web.ctx.status = "503 Service Unavailable"
        web.header("Content-Type", "text/plain; charset=utf-8")
        return utf(message)

    def get_login(self):
        return "anonymous"  # FIXME

    def tracks(self, tracks):
        return [t.for_template() for t in tracks]


class DataController(Controller):
    """
    Controller which works with the database.
    Methods are executed inside a transaction.
    """
    def GET(self, *args):
        db = connect()
        try:
            res = self.on_get(*args)
            db.commit()
            return res
        except Exception, e:
            db.rollback()
            return self.on_exception(e)


class JsonController(DataController):
    def on_exception(self, e):
        log_exception("{}: {}", e.__class__.__name__, str(e))
        return self.send_json({"message": str(e)})


class AuthController(Controller):
    def GET(self):
        args = web.input(token=None)
        if args.token is None:
            web.header("Content-Type", "text/plain; charset=utf-8")
            return "Please specify a token or POST."
        token = auth.confirm_token(args.token)
        if token:
            web.header("Content-Type", "text/plain; charset=utf-8")
            return "OK, tell this to your program: %s" % args.token
        else:
            web.header("Content-Type", "text/plain; charset=utf-8")
            return "Wrong token."

    @send_json
    def POST(self):
        args = web.input(id=None, type=None)
        auth.create_token(args.id, args.type)
        return {"status": "ok", "message": "You'll soon receive a message with a confirmation link."}


class StaticController(Controller):
    def on_get(self, url):
        path = get_web_path(url)
        return self.send_file(path)


class IndexController(DataController):
    def on_get(self):
        return self.render_page("index.tpl", {
            "tab": "recent",
            "tracks": self.get_recent(),
            "queue": self.get_queue(),
        })

    def get_recent(self):
        return self.tracks(Track.find_recently_played(100))

    def get_queue(self):
        return self.tracks(Track.get_queue())


class InfoController(Controller):
    @send_json
    def GET(self):
        args = web.input(id=None, token=None)
        sender = auth.get_id_by_token(args.token)

        track_id = args.id
        if track_id is None:
            raise RuntimeError("track id not specified")

        track = tracks.get_track_by_id(track_id, sender=sender)
        if track is None:
            raise RuntimeError("track %s not found." % track_id)

        return track


class PlaylistController(Controller):
    @send_json
    def GET(self):
        args = web.input(name="all", artist=None, tag=None)

        playlist_name = args["name"]
        if playlist_name == "bookmarks":
            playlist_name = "bm:hex@umonkey.net"

        artist_name = args["artist"]
        if artist_name == "All artists":
            artist_name = None

        tag_name = args["tag"]
        if tag_name == "All tags":
            tag_name = None

        print "Playlist query: %s" % dict(args)

        result = {"artsits": [], "tags": [], "tracks": []}
        result["artists"] = [a["name"] for a in database.Artist.query(
            playlist=playlist_name, tag=tag_name)]
        result["tags"] = database.Label.query_names(
            playlist=playlist_name, artist=artist_name)

        tracks = database.Track.query(
            playlist=playlist_name, artist=artist_name, tag=tag_name, count=None)
        result["tracks"] = [{"id": t["id"], "artist": t["artist"],
            "title": t["title"]} for t in tracks]

        return result


class QueueController(JsonController):
    def on_get(self, track_id):
        login = self.get_login()

        track = Track.get_by_id(track_id)
        if not track:
            return self.send_json({"message": "track not found"})

        track.queue("anonymous")
        commit()

        return self.send_json({"success": True,
                               "message": "Track added to the queue.",
                               "trigger": "ardj.home.refresh"})


class SkipController(JsonController):
    def on_post(self):
        login = self.get_login()
        log_info("web: user {} skipped current track", login)

        skip_current_track()
        return self.send_json({"message": "OK, skipping.",
                               "trigger": "ardj.home.refresh"})


class RecentController(Controller):
    @send_json
    def GET(self):
        return {
            "success": True,
            "scope": "recent",
            "tracks": list(self.get_tracks()),
        }

    def get_tracks(self):
        for track in database.Track.find_recently_played():
            track["artist_url"] = track.get_artist_url()
            track["track_url"] = track.get_track_url()
            yield track


class RocksController(JsonController):
    vote_value = 1

    def on_get(self, track_id):
        login = self.get_login()

        track = Track.get_by_id(track_id)
        if not track:
            return self.send_json({"message": "track not found"})

        add_vote(track["id"], login, self.vote_value)
        commit()

        return self.send_json({"message": "Thanks!"})


class SucksController(RocksController):
    vote_value = -1


class DownloadController(DataController):
    def on_get(self, track_id):
        login = self.get_login()

        track = Track.get_by_id(track_id)
        if not track:
            return self.notfound("track not found")

        path = get_user_path("music/" + utf(track["filename"]))
        if not os.path.exists(path):
            return self.notfound("file %s not found" % path)

        f = open(path, "rb")

        web.header("Content-Type", "application/octet-stream")

        fn = os.path.basename(path)
        web.header("Content-Disposition", "attachment; filename=\"%s\"" % fn)

        return f


class SearchController(Controller):
    @send_json
    def GET(self):
        args = web.input(query=None)

        track_ids = tracks.find_ids(args.query)
        track_info = [database.Track.get_by_id(id) for id in track_ids]

        return {
            "success": True,
            "scope": "search",
            "tracks": track_info,
        }


class StatusController(JsonController):
    def on_get(self):
        last = web.input(last=None).last

        db = connect()

        track = self.get_current()
        for x in range(12):
            if not last or last != str(track["id"]):
                break

            db.rollback()
            time.sleep(5)
            track = self.get_current()

        return self.send_json(track)

    def get_current(self):
        track_id = tracks.get_last_track_id()
        if track_id is None:
            return None

        track = tracks.get_track_by_id(track_id)
        if track is None:
            return None

        track["current_ts"] = int(time.time())
        track["listeners"] = get_count() or 0
        return track


class TagCloudController(Controller):
    @send_json
    def GET(self):
        tags = database.Track.find_tags(
            cents=4, min_count=1)
        return {"status": "ok", "tags": dict(tags)}


class TrackController(DataController):
    def on_get(self, track_id):
        track = Track.get_by_id(track_id)
        if not track:
            return self.notfound("track not found")

        t = track.for_template()
        t["labels"] = ", ".join(sorted(track.get_labels()))

        return self.render_page("track.tpl", {
            "tab": "music",
            "track": t,
        })

    def on_post(self, track_id):
        track = Track.get_by_id(track_id)
        if not track:
            return self.notfound("track not found")

        try:
            form = web.input(title="", artist="", labels="", delete=None, tagme=None)

            track["title"] = form.title
            track["artist"] = form.artist
            if form.delete == "yes":
                track["weight"] = 0
                log_info("Track %u deleted.", track["id"])
            track.put()

            labels = [l.strip() for l in form.labels.split(",")]
            track.set_labels(labels)
            commit()

            next_url = "/artist/" + utf(track["artist"])

            if form.tagme == "yes":
                tracks = Track.find_by_tag("tagme", limit=1)
                if tracks:
                    next_url = "/track/%u" % tracks[0]["id"]

            return self.send_json({"message": "Changes saved.",
                                   "redirect": next_url})

        except Exception, e:
            log_exception("Error editing track: {}", e)
            return self.send_json({"message": "Error saving changes: %s" % e})


class UpdateTrackController(Controller):
    @send_json
    def POST(self):
        args = web.input(
            token=None,
            id=None,
            title=None,
            artist=None,
            tag=[])

        sender = auth.get_id_by_token(args.token)
        if sender is None:
            raise UsageError("bad token")

        track = database.Track.get_by_id(int(args.id))
        if track is None:
            raise UsageError("track not found")

        if args.artist:
            track["artist"] = args.artist
            log_debug("{0} set artist for track {1} to {2}",
                sender, args.id, args.artist)
        if args.title:
            track["title"] = args.title
            log_debug("{0} set title for track {1} to {2}",
                sender, args.id, args.title)
        track.put()

        if args.tag:
            track.set_labels(args.tag)
            log_debug("{0} set labels for track {1} to {2}",
                sender, args.id, ", ".join(args.tag))

        connect().commit()

        return {"success": True}


class ExceptionHandlingMiddleWare(object):
    """Завершение предыдущей транзакции после обработки каждого запроса, для
    исключения блокировки базы данных."""
    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        try:
            return self.app(environ, start_response)
        finally:
            connect().rollback()


class UploadController(JsonController):
    def on_get(self):
        return self.render_page("upload.tpl", {
            "tab": "upload",
        })

    def on_post(self):
        labels = web.input().get("labels", "music, tagme")
        labels = re.split('[,\s+]', labels)

        accepted, ignored = 0, 0
        for file in self.get_files():
            if not re.match(r'.*\.(mp3|ogg|ogv|oga|flac)$', file.filename):
                log_debug("File {} ignored -- unsupported type.", file.filename)
                ignored += 1

            else:
                dst = get_music_path(utf(file.filename))
                if os.path.exists(dst):
                    os.unlink(dst)

                with open(dst, "wb") as f:
                    f.write(file.value)
                    log_info("File {} written.", dst)

                t = tracks.add_file(dst, labels)
                accepted += 1

        commit()

        message = u"%u files accepted, %u ignored.  Thanks!" % (accepted, ignored)
        return self.send_json({"message": message})

    def get_files(self):
        files = web.webapi.rawinput().get("file")
        if not isinstance(files, list):
            files = [files]
        for file in files:
            yield file


def get_application():
    app = web.application((
        r"/", IndexController,

        r"/track/(\d+)", TrackController,
        r"/track/(\d+)/queue", QueueController,
        r"/track/(\d+)/rocks", RocksController,
        r"/track/(\d+)/sucks", SucksController,
        r"/track/(\d+)/download", DownloadController,
        r"/skip", SkipController,
        r"/status\.json", StatusController,
        r"/upload", UploadController,

        r"/((?:docs|fonts|scripts|styles|images)/.+)", StaticController,

        r"/auth(?:\.json)?", AuthController,
        r"/playlist\.json", PlaylistController,
        r"/tag/cloud\.json", TagCloudController,
        r"/track/search\.json", SearchController,
        r"/track/update\.json", UpdateTrackController,
    ))

    return app


def serve_http(hostname, port):
    """Starts the HTTP web server at the specified socket."""
    sys.argv.insert(1, "%s:%s" % (hostname, port))

    log_info("Starting the ardj web service at http://{}:{}/", hostname, port)

    app = get_application()
    app.run(ExceptionHandlingMiddleWare)


def cmd_create_token(email=None, *args):
    """Create a new token for the specified email address."""
    if email is None:
        raise RuntimeError("Please specify email address.")

    from auth import create_token
    token = create_token(email, "email")
    print "New token: %s (needs activation)." % token["token"]


def cmd_serve(arg=None, *args):
    if arg == "help":
        print "Runs the local web server which is used to administer the radio,"
        print "also to accept user votes."
        exit(1)

    elif arg is not None:
        print "This command takes no argument."
        exit(1)

    os.chdir("/")

    socket = config.get("web_socket", "127.0.0.1:8080")
    serve_http(*socket.split(":", 1))


def cmd_tokens():
    """List valid tokens."""
    from ardj.auth import get_active_tokens
    for t in get_active_tokens():
        print "%s: %s" % (t["login"], t["token"])
