# TODO: Add /jobs/<id>/data.zip to fetch all files at once in a zip file format
# TODO: Store completed work in /path/to/store/<id>.zip

import os
import logging
import json
import pickle
import flask
from flask import redirect, url_for, flash
from flask import send_from_directory
from werkzeug.utils import  secure_filename

from . import store

app = flask.Flask(__name__)

# ==== File upload specialization ===
# By uploading files into a temporary file provided by store, we
# can then move the files directly into place on the store rather
# than copy them.  This gives us reduced storage, reduced memory
# and reduced CPU.
class Request(flask.Request):
    # Upload directly into temporary files.
    def _get_file_stream(self, total_content_length, content_type,
                         filename=None, content_length=None):
        #print "returning named temporary file for",filename
        return store.tempfile()
app.request_class = Request


# ==== Format download specialization ===
def _format_response(response, format='json', template=None):
    """
    Return response as a particular format.
    """
    #print "response",response
    if format == 'html':
        if template is None: flask.abort(400)
        return flask.render_template(template, **response)
    elif format == 'json':
        return flask.jsonify(**dict((str(k),v) for k,v in response.items()))
    elif format == 'pickle':
        return pickle.dumps(response)
    else:
        flask.abort(400) # Bad request


@app.route('/jobs.<format>', methods=['GET'])
def list_jobs(format='json'):
    """
    GET /jobs.<format>

    Return a list of all job ids.
    """
    response = dict(jobs=SCHEDULER.jobs())
    return _format_response(response, format, template='list_jobs.html')

@app.route('/jobs/<any(u"pending",u"active",u"error",u"complete"):status>.<format>',
           methods=['GET'])
def filter_jobs(status, format='json'):
    """
    GET /jobs/<pending|active|error|complete>.<format>

    Return all jobs with a particular status.
    """
    response = dict(jobs=SCHEDULER.jobs(status=str(status).upper()))
    return _format_response(response, format, template='list_jobs.html')

@app.route('/jobs.<format>', methods=['POST'])
def create_job(format='json'):
    """
    POST /jobs.<format>

    Schedule a new job, return the job record.

    The POST data should contain::

        {
        notify: "<user@email or @twitterid>",
        service: "<name of service>",
        version: "<service version>",
        name: "<request name>",
        data: "<service data>",
        ...
        }

    The response contains::

        {
        id: <job id>,
        job: <job details>
        }

    Job details is simply a copy of the original request.

    """
    request = flask.request.json #@UndefinedVariable in request proxy
    if request is None: flask.abort(415) # Unsupported media
    id = SCHEDULER.submit(request, origin=flask.request.remote_addr) #@UndefinedVariable in request proxy
    flash('Job %s scheduled' % id)
    response = {'id': id, 'job': SCHEDULER.info(id)}
    #return redirect(url_for('show_job', id=id, format=format))
    return _format_response(response, format=format, template='show_job.html')

@app.route('/jobs/<int:id>.<format>', methods=['GET'])
def show_job(id, format='json'):
    """
    GET /jobs/<id>.<format>

    Get job record by id.

    The response contains::

        {
        id: <job id>,
        job: <job details>
        }

    Job details is simply a copy of the original request.
    """
    response = {'id': id, 'job': SCHEDULER.info(id)}
    return _format_response(response, format=format, template='show_job.html')

@app.route('/jobs/<int:id>/results.<format>', methods=['GET'])
def get_results(id, format='json'):
    """
    GET /jobs/<id>/results.<format>

    Get job results by id.

    Returns::

        {
        id: <job id>
        status: 'PENDING|ACTIVE|COMPLETE|ERROR|UNKNOWN',
        result: <job value>     (absent if status != COMPLETE)
        trace: <error trace>    (absent if status != ERROR)
        }
    """
    response = SCHEDULER.results(id)
    response['id'] = id
    #print "returning response",response
    return _format_response(response, format=format)

@app.route('/jobs/<int:id>/status.<format>', methods=['GET'])
def get_status(id, format='json'):
    """
    GET /jobs/<id>/status.<format>

    Get job status by id.

    Returns::

        {
        id: <job id>,
        status: 'PENDING|ACTIVE|COMPLETE|ERROR|UNKNOWN'
        }
    """
    response = { 'status': SCHEDULER.status(id) }
    response['id'] = id
    return _format_response(response, format=format)


@app.route('/jobs/<int:id>.<format>', methods=['DELETE'])
def delete_job(id, format='json'):
    """
    DELETE /jobs/<id>.<format>

    Deletes a job, returning the list of remaining jobs as <format>
    """
    SCHEDULER.delete(id)
    flash('Job %s deleted' % id)
    response = dict(jobs=SCHEDULER.jobs())
    return _format_response(response, format=format, template="list_jobs.html")
    #return redirect(url_for('list_jobs', id=id, format=format))

@app.route('/jobs/nextjob.<format>', methods=['POST'])
def fetch_work(format='json'):
    # TODO: verify signature
    request = flask.request.json #@UndefinedVariable in request proxy
    if request is None: flask.abort(415) # Unsupported media
    job = SCHEDULER.nextjob(queue=request['queue'])
    return _format_response(job, format=format)

@app.route('/jobs/<int:id>/postjob', methods=['POST'])
def return_work(id):
    # TODO: verify signature corresponds to flask.request.form['queue']
    # TODO: verify that work not already returned by another client
    try:
        #print "decoding <%s>"%flask.request.form['results']
        results = json.loads(flask.request.form['results']) #@UndefinedVariable in request proxy
    except:
        import traceback;
        logging.error(traceback.format_exc())
        results = {
            'status': 'ERROR',
            'error': 'No results returned from the server',
            'trace': flask.request.form['results'], #@UndefinedVariable in request proxy
        }
    _transfer_files(id)
    SCHEDULER.postjob(id, results)
    # Should be signaling code 204: No content
    return _format_response({},format="json")

@app.route('/jobs/<int:id>/data/index.<format>')
def listfiles(id, format):
    try:
        path = store.path(id)
        files = sorted(os.listdir(path))
        finfo = [(f,os.path.getsize(os.path.join(path,f)))
                 for f in files if os.path.isfile(os.path.join(path,f))]
    except:
        finfo = []
    response = { 'files': finfo }
    response['id'] = id
    return _format_response(response, format=format, template="index.html")

# TODO: don't allow putfiles without authentication
#@app.route('/jobs/<int:id>/data/', methods=['GET','PUT'])
def putfiles(id):
    if flask.request.method=='PUT': #@UndefinedVariable in request proxy
        # TODO: verify signature
        _transfer_files(id)
    return redirect(url_for('getfile',id=id,filename='index.html'))

@app.route('/jobs/<int:id>/data/<filename>')
def getfile(id, filename):
    as_attachment = filename.endswith('.htm') or filename.endswith('.html')
    if filename.endswith('.json'):
        mimetype = "application/json"
    else:
        mimetype = None

    return send_from_directory(store.path(id), filename,
                               mimetype=mimetype, as_attachment=as_attachment)

#@app.route('/jobs/<int:id>.<format>', methods=['PUT'])
#def update_job(id, format='.json'):
#    """
#    PUT /job/<id>.<format>
#
#    Updates a job using data from the job submission form.
#    """
#    book = Book(id=id, name=u"I don't know") # Your query
#    book.name = request.form['name'] # Save it
#    flash('Book %s updated!' % book.name)
#    return redirect(url_for('show_job', id=id, format=format))

#@app.route('/jobs/new.html')
#def new_job_form():
#    """
#    GET /jobs/new
#
#    Returns a job submission form.
#    """
#    return render_template('new_job.html')

#@app.route('/jobss/<int:id>/edit.html')
#def edit_job_form(id):
#    """
#    GET /books/<id>/edit
#
#    Form for editing job details
#    """
#    book = Book(id=id, name=u'Something crazy') # Your query
#    return render_template('edit_book.html', book=book)

def _transfer_files(jobid):
    logging.warn("XSS attacks possible if stored file is mimetype html")
    for file in flask.request.files.getlist('file'): #@UndefinedVariable in request proxy
        if not file: continue
        filename = secure_filename(os.path.split(file.filename)[1])
        # Because we used named temporary files that aren't deleted on
        # closing as our streaming file type, we can simply move the
        # resulting files to the store rather than copying them.
        file.stream.close()
        logging.warn("moving %s -> %s"%(file.stream.name, os.path.join(store.path(jobid),filename)))
        os.rename(file.stream.name, os.path.join(store.path(jobid),filename))


def init_scheduler(conf):
    if conf == 'slurm':
        from . import slurm
        Scheduler = slurm.Scheduler
    elif conf == 'direct':
        logging.warn("direct scheduler is not a good choice!")
        try: os.nice(19)
        except: pass
        from . import simplequeue
        Scheduler = simplequeue.Scheduler
    elif conf == 'dispatch':
        from . import dispatcher
        Scheduler = dispatcher.Scheduler
    else:
        raise ValueError("unknown scheduler %s"%conf)
    return Scheduler()

def serve():
    app.run(host='0.0.0.0')

def fullpath(p): return os.path.abspath(os.path.expanduser(p))
def configure(jobstore=None, jobkey=None, jobdb=None, scheduler=None):
    global SCHEDULER, app

    if jobstore:
        store.ROOT = fullpath(jobstore)
    if jobkey:
        app.config['SECRET_KEY'] = open(fullpath(jobkey)).read()
    if jobdb:
        from . import db
        db.DB_URI = jobdb

    SCHEDULER = init_scheduler(scheduler)

if __name__ == '__main__':
    configure(jobstore='/tmp/server/%s',
              jobdb='sqlite:///tmp/jobqueue.db',
              jobkey='~/.bumps/key',
              scheduler='dispatch',
              )
    app.config['DEBUG'] = True
    serve()
