#!/usr/bin/python
# -*- coding: utf-8 -*-

import json
import os
import logging
import logging.config
import bottle
import tarfile
from string import Template
import yaml

import igor.log
import igor.main
import igor.backends.libvirt
import igor.backends.cobbler
import igor.job
import igor.utils
import igor.reports
import igor.backends.files
from igor.hacks import IgordJSONEncoder

import igor.config as config

logger = logging.getLogger(__name__)

logger.info("Starting igor daemon")

BOTTLE_MAX_READ_SIZE = 1024 * 1024 * 512

#
# Parse the config at first
#
CONFIG = config.parse_config()

# Enabled backends:
enabled_backends = CONFIG["backends.enabled"].split()
primary_profile_backend = CONFIG["backends.primary_profile"]
#
# Now define our origins, where we get the items (hosts, profiles, …) from
#
plan_origins = {}
testsuite_origins = {}
profile_origins = {}
host_origins = {}

if "files" in enabled_backends:
    plan_origins["files"] = \
        igor.backends.files.TestplansOrigin( \
                    CONFIG["testplans.path"].split(":"))

    testsuite_origins["files"] = \
        igor.backends.files.TestsuitesOrigin( \
                    CONFIG["testcases.path"].split(":"))

    host_origins["files"] = \
            igor.backends.files.HostsOrigin(CONFIG["hosts.path"].split(":"))

if "cobbler" in enabled_backends:
    __cobbler_origin_args = (CONFIG["cobbler.url"], \
                             CONFIG["cobbler.username"], \
                             CONFIG["cobbler.password"], \
                             CONFIG["cobbler.ssh_uri"])
    c_profiles = igor.backends.cobbler.ProfileOrigin(*__cobbler_origin_args,
        remote_path_prefix=CONFIG.get("cobbler.remote_path_prefix", "/tmp"))
    profile_origins["cobbler"] = c_profiles

    # Just systems with igor- prefix
    c_hosts = igor.backends.cobbler.HostsOrigin(*__cobbler_origin_args, \
                expression=CONFIG["cobbler.hosts.identification_expression"], \
                whitelist=CONFIG["cobbler.hosts.whitelist"])
    host_origins["cobbler"] = c_hosts

if "libvirt" in enabled_backends:
    __libvirt_con_args = (CONFIG["libvirtd.connection_uri"], \
    CONFIG["libvirtd.virt-install.storage_pool"], \
    CONFIG["libvirtd.virt-install.network_configuration"])

    l_hosts = {"libvirt-create": \
        igor.backends.libvirt.CreateDomainHostOrigin(*__libvirt_con_args),
        "libvirt-existing": \
        igor.backends.libvirt.ExistingDomainHostOrigin(*__libvirt_con_args)
        }
    host_origins.update(l_hosts)

    l_profiles = {"libvirt": igor.backends.libvirt.ProfileOrigin(
    *__libvirt_con_args)}
    profile_origins.update(l_profiles)

#
# Now prepare the essential objects
#
jc = igor.job.JobCenter(session_path=CONFIG["session.path"],
                        hooks_path=CONFIG["hooks.path"])

inventory = igor.main.Inventory( \
    plans=plan_origins, \
    testsuites=testsuite_origins, \
    profiles=profile_origins, \
    hosts=host_origins)
inventory.check()


def to_json(obj):
    format = "json"
    root_tag = "result"

    r = json.dumps(obj, cls=IgordJSONEncoder, sort_keys=True, indent=2)

    if "format" in bottle.request.query:
        format = bottle.request.query["format"]
    if "root" in bottle.request.query:
        root_tag = bottle.request.query["root"]

    if "x-igor-format-xml" in bottle.request.headers:
        format = "xml"

    if format == "xml":
        j = json.loads(r)
        r = "<?xml-stylesheet type='text/xsl' href='/ui/index.xsl' ?>\n"
        r += igor.utils.obj2xml(root_tag, j, as_string=True)

    if format == "yaml":
        j = json.loads(r)
        r = yaml.dump_all(j)

    bottle.response.content_type = "application/%s" % format
    return r


def check_authentication(user, password):
    return user == password


#
# bottles
#

app = bottle.Bottle()


@app.route('/')
def index():
    bottle.response.content_type = "text/xml"
    return "<?xml-stylesheet type='text/xsl' href='/ui/index.xsl' ?>\n<index/>"


@app.route('/ui/<filename>')
def ui_data(filename):
    return bottle.static_file(filename, root=config.DATA_DIR + "/ui/")


@app.route('/static/data/<filename>')
def static_data(filename):
    return bottle.static_file(filename, root=config.DATA_DIR)


@app.route('/jobs/submit/<tname>/with/<pname>/on/<hname>')
@app.route('/jobs/submit/<tname>/with/<pname>/on/<hname>/<cookiereq>')
def submit_testsuite(tname, pname, hname, cookiereq=None):
    for key, name in [("testsuites", tname), \
                      ("profiles", pname), \
                      ("hosts", hname)]:
        item = inventory._lookup(key, name)
        if item is None:
            bottle.abort(412, "Unknown %s '%s'" % (key, name))
    spec = igor.main.JobSpec(testsuite=inventory.testsuites()[tname],
               profile=inventory.profiles()[pname],
               host=inventory.hosts()[hname],
               additional_kargs=bottle.request.query.additional_kargs or "")
    logger.debug("Submitting with args: %s" % str(spec))
    resp = jc.submit(spec, cookiereq)

    return to_json(resp)


@app.route('/jobs')
def get_jobs():
    return to_json(jc.get_jobs())


@app.route('/jobs/<cookie>/start')
def start_job(cookie):
    if cookie not in jc.jobs:
        bottle.abort(404, "Unknown job '%s'" % cookie)
    m = jc.start_job(cookie)
    return to_json(m)


@app.route('/jobs/<cookie>/status')
def job_status(cookie):
    if cookie not in jc.jobs:
        bottle.abort(404, "Unknown job '%s'" % cookie)
    m = jc.jobs[cookie]
    return to_json(m)


@app.route('/jobs/<cookie>/report')
def job_report(cookie):
    if cookie not in jc.jobs:
        bottle.abort(404, "Unknown job '%s'" % cookie)
    j = jc.jobs[cookie]
    bottle.response.content_type = "text/plain; charset=utf8"
    return str(igor.reports.job_status_to_report(j.__to_dict__()))


@app.route('/jobs/<cookie>/step/<n:int>/skip')
def skip_step(cookie, n):
    if cookie not in jc.jobs:
        bottle.abort(404, "Unknown job '%s'" % cookie)
    m = jc.skip_step(cookie, n)
    return to_json(m)


@app.route('/jobs/<cookie>/step/<n:int>/<result:re:success|failed>')
def finish_step(cookie, n, result):
    if cookie not in jc.jobs:
        bottle.abort(404, "Unknown job '%s'" % cookie)
    m = jc.finish_test_step(cookie, n, result == "success", None)
    return to_json(m)


@app.route('/jobs/<cookie>/step/<n:int>/result')
def get_step(cookie, step):
    if cookie not in jc.jobs:
        bottle.abort(404, "Unknown job '%s'" % cookie)
    m = jc.test_step_result(cookie, step)
    return to_json(m)


@app.route('/jobs/<cookie>/step/current/annotate', method='PUT')
def annotate_step(cookie):
    if cookie not in jc.jobs:
        bottle.abort(404, "Unknown job '%s'" % cookie)
    j = jc.jobs[cookie]
    data = bottle.request.body.read(BOTTLE_MAX_READ_SIZE)
    j.annotate(data)


@app.route('/jobs/<cookie>', method='DELETE')
@app.route('/jobs/<cookie>/abort')
def abort_job(cookie):
    if cookie not in jc.jobs:
        bottle.abort(404, "Unknown job '%s'" % cookie)
    try:
        m = jc.abort_job(cookie)
    except Exception as e:
        m = e.message
    return to_json(m)


@app.route('/jobs/<cookie>/testsuite')
def get_job_testsuite_archive(cookie):
    if cookie not in jc.jobs:
        bottle.abort(404, "Unknown job '%s'" % cookie)
    t = jc.jobs[cookie].testsuite
    r = t.get_archive()
    if not r:
        bottle.abort(404, 'No testsuite for %s' % (cookie))

    return r.getvalue()


@app.route('/jobs/<cookie>/artifacts')
def list_artifact(cookie):
    if cookie not in jc.jobs:
        bottle.abort(404, "Unknown job '%s'" % cookie)
    j = jc.jobs[cookie]
    data = to_json(j.list_artifacts())
    return data


@app.route('/jobs/<cookie>/archive')
def get_artifacts_archive(cookie):
    if cookie not in jc.jobs:
        bottle.abort(404, "Unknown job '%s'" % cookie)
    j = jc.jobs[cookie]
    bottle.response.content_type = "application/x-bzip2"
    return j.get_artifacts_archive().getvalue()


@app.route('/jobs/<cookie>/artifacts/<name>', method='PUT')
def add_artifact(cookie, name):
    if cookie not in jc.jobs:
        bottle.abort(404, "Unknown job '%s'" % cookie)
    if "/" in name:
        bottle.abort(412, "Name may not contain slashes")
    j = jc.jobs[cookie]
    data = bottle.request.body.read(BOTTLE_MAX_READ_SIZE)
    j.add_artifact_to_current_step(name, data)


@app.route('/jobs/<cookie>/artifacts/<name>')
def get_artifact(cookie, name):
    if cookie not in jc.jobs:
        bottle.abort(404, "Unknown job '%s'" % cookie)
    j = jc.jobs[cookie]
    bottle.response.content_type = "text/plain; charset=utf8"
    return str(j.get_artifact(name))


@app.route('/firstboot/<cookie>')
@app.route('/jobs/<cookie>/set/enable_pxe/<enable_pxe>')
def disable_pxe_cb(cookie, enable_pxe=False):
    if cookie not in jc.jobs:
        bottle.abort(404, "Unknown job '%s'" % cookie)
    j = jc.jobs[cookie]
    m = j.profile.enable_pxe(j.host, enable_pxe)
    return to_json(m)


@app.route('/jobs/<cookie>/set/kernelargs/<kernelargs>')
def set_kernelargs_cb(cookie, kernelargs):
    if cookie not in jc.jobs:
        bottle.abort(404, "Unknown job '%s'" % cookie)
    raise Exception("Not implemented yet, but needed for updates")
    j = jc.jobs[cookie]
    m = j.profile.set_kargs(j.host, kernelargs)
    return to_json(m)


@app.route('/testjob/<cookie>')
@app.route('//testjob/<cookie>')
def get_bootstrap_script(cookie):
    if cookie not in jc.jobs:
        bottle.abort(404, "Unknown job '%s'" % cookie)

    disable_pxe_cb(cookie)

    script = None

    with open(os.path.join(config.DATA_DIR, "client-bootstrap.sh"), "r") as f:
        script = f.read()

    r = Template(script).safe_substitute(
        igor_cookie=cookie,
        igor_current_step=jc.jobs[cookie].current_step,
        igor_testsuite=jc.jobs[cookie].testsuite.name
    )

    if not r:
        bottle.abort(404, 'No testsuite for %s' % (cookie))

    return r


@app.route('/testsuites')
def list_testsuites():
    testsuites = inventory.testsuites()
    return to_json(testsuites)


@app.route('/testsuites/validate')
def validate_testsuites():
    testsuites = inventory.testsuites().items()
    r = {}
    for n, suite in testsuites:
        r[n] = suite.validate()
    return r


@app.route('/testsuites/<name>/summary')
def get_testsuite_summary(name):
    testsuites = inventory.testsuites()
    if name not in testsuites:
        bottle.abort(404, "Unknown testsuite '%s'" % name)
    return to_json(testsuites[name])


@app.route('/testsuites/<name>/download')
@app.route('/testsuites/<name>/download/<tarball>')
def get_testsuite_archive(name, tarball="testsuite.tar"):
    testsuites = inventory.testsuites()
    if name not in testsuites:
        bottle.abort(404, "Unknown testsuite '%s'" % name)
    t = testsuites[name]
    r = t.get_archive()
    if not r:
        bottle.abort(404, 'No testsuite for %s' % (name))
    bottle.response.content_type = "application/x-tar; charset=binary"
    return r.getvalue()


@app.route('/testplans')
def list_plans():
    return to_json(inventory.plans())


@app.route('/testplans/<name>')
def plan_info(name):
    if name not in inventory.plans():
        bottle.abort(404, "Unknown plan: %s" % name)
    plan = inventory.plans()[name]
    return to_json(plan)


@app.route('/testplans/<name>/submit')
def run_plans(name):
    if name not in inventory.plans():
        bottle.abort(404, "Unknown plan: %s" % name)
    plan = inventory.plans()[name]
    plan.inventory = inventory      # FIXME not very nice
    plan.variables.update({k: bottle.request.query[k] \
                           for k in bottle.request.query.keys()})
    worker = jc.submit_plan(plan)
    return to_json(worker.__to_dict__())


@app.route('/testplans/<name>')
def testplan_summary(name):
    if name not in inventory.plans():
        bottle.abort(404, "Unknown plan: %s" % name)
    return to_json(inventory.plans()[name])


@app.route('/testplans/<name>/status')
def status_plans(name):
    if name not in inventory.plans():
        bottle.abort(404, "Unknown plan: %s" % name)
    r = jc.status_plan(name)
    return to_json(r)


@app.route('/testplans/<name>/report')
def testplan_report(name):
    if name not in inventory.plans():
        bottle.abort(404, "Unknown plan: %s" % name)
    r = jc.status_plan(name)
    bottle.response.content_type = "text/plain; charset=utf8"
    return str(igor.reports.testplan_status_to_report(r))


@app.route('/testplans/<name>/report/junit')
def testplan_junit_report(name):
    if name not in inventory.plans():
        bottle.abort(404, "Unknown plan: %s" % name)
    r = jc.status_plan(name)
    bottle.response.content_type = "text/plain; charset=utf8"
    xml = igor.reports.testplan_status_to_junit_report(r)
    return igor.reports.to_xml_str(xml)


@app.route('/testplans/<name>/abort')
def abort_plans(name):
    if name not in inventory.plans():
        bottle.abort(404, "Unknown plan: %s" % name)
    r = jc.abort_plan(name)
    return to_json(r.__to_dict__())


@app.route('/profiles')
def list_profiles():
    return to_json(inventory.profiles())


@app.route('/hosts')
def list_hosts():
    return to_json(inventory.hosts())


@app.route('/testcases/<suitename>/<setname>/<casename>/source')
def testcase_source(suitename, setname, casename):
    testsuites = inventory.testsuites()
    if suitename not in testsuites:
        bottle.abort(404, "Unknown testsuite '%s'" % suitename)
    suite = testsuites[suitename]
    tset = None
    for _tset in suite.testsets:
        if _tset.name == setname:
            tset = _tset
    if tset is None:
        bottle.abort(404, "Unknown testset '%s'" % setname)
    case = None
    for _case in tset.testcases():
        if _case.name == casename:
            case = _case
    if case is None:
        bottle.abort(404, "Unknown testcase '%s'" % casename)
    source = case.source()
    if source == None:
        bottle.abort(404, "No source '%s'" % casename)
    else:
        bottle.response.content_type = "text/plain"
    return source


@app.route('/profiles/<pname>', method='PUT')
def profile_from_vmlinuz_put(pname):
    reqfiles = set(["kernel", "initrd", "kargs"])
    _tmpdir = igor.utils.TemporaryDirectory()
    with _tmpdir as tmpdir:
        logger.debug("Using PUT tmpdir %s" % tmpdir)
        with tarfile.open(fileobj=bottle.request.body) as tarball:
            arcfiles = tarball.getnames()
            logger.debug("PUT %s" % arcfiles)
            for arcfile in arcfiles:
                assert os.path.basename(arcfile) == arcfile, \
                       "not paths allowed"
            tarball.extractall(path=tmpdir)
        written_files = {}
        for rf in reqfiles:
            fn = rf
            header = "x-%s-filename" % rf
            if header in bottle.request.headers:
                fn = bottle.request.headers[header]
            _tmpdir.cleanfile(fn)
            written_files[rf] = os.path.join(tmpdir, os.path.basename(fn))
        print written_files
        if not all([r in written_files.keys() for r in reqfiles]):
            bottle.abort(412, "Expecting %s files" % str(reqfiles))
        inventory.create_profile(oname=primary_profile_backend, pname=pname, \
                                 **written_files)
    _tmpdir.clean()


@app.route('/profiles/<pname>', method='POST')
def profile_from_vmlinuz_post(pname):
    reqfiles = set(["kernel", "initrd", "kargs"])
    _tmpdir = igor.utils.TemporaryDirectory(reqfiles)
    with _tmpdir as tmpdir:
        logger.debug("Using tmpdir %s" % tmpdir)
        if not all([r in bottle.request.files.keys() for r in reqfiles]):
            bottle.abort(412, "Not all required files given: %s" % reqfiles)
        file_dict = {field: store for field, store \
                                  in bottle.request.files.items()}
        written_files = {}
        for filename in reqfiles:
            store = file_dict[filename]
            dstn = os.path.join(tmpdir, os.path.basename(store.filename))
            with open(dstn, "wb") as d:
                logger.debug("Writing '%s'" % d)
                d.write(store.file.read())
                written_files[filename] = dstn
        inventory.create_profile(oname="cobbler", pname=pname, \
                                 **written_files)
    _tmpdir.clean()


@app.route('/profiles/<pname>/kargs', method='GET')
@app.route('/profiles/<pname>/kargs', method='POST')
def profile_kargs(pname):
    if pname not in inventory.profiles():
        bottle.abort(404, "Unknown profile")
    kargs = bottle.request.forms.kargs
    n_kargs = "NO_KARGS_FOUND"
    if kargs:
        if "{igor_cookie}" not in kargs:
            bottle.abort(412, "{igor_cookie} not found in kargs, this is " + \
                              "needed to initiate the callback to Igor, " + \
                              "e.g. boot_trigger=igor/testjob/{igor_cookie}")
        n_kargs = inventory.profiles()[pname].kargs(kargs)
    else:
#        bottle.abort(412, "No kargs specified")
        n_kargs = inventory.profiles()[pname].kargs()
    return n_kargs


@app.route('/profiles/<pname>', method='DELETE')
@app.route('/profiles/<pname>/remove')
def delete_profile(pname):
    if pname not in inventory.profiles():
        bottle.abort(404, "Unknown profile")
    try:
        inventory.profiles()[pname].delete()
    except Exception as e:
        # FIXME could be solved in the cobblre backend
        logger.warning("An error occurred while removing a " +
                       "profile: %s (%s)" % (e.message, e))


@app.route('/server/log')
def get_log():
    bottle.response.content_type = "text/plain; charset=utf8"
    return igor.log.backlog()

if __name__ == "__main__":
    try:
    #    logger.info("Starting igord")
        bottle.run(app, host='0.0.0.0', port=8080, reloader=False)
    except KeyboardInterrupt:
        logger.debug("Ending igor")
