# Copyright (c) 2012-2013 Paul Tagliamonte <paultag@debian.org>
# Copyright (c) 2014-2015 Clement Schreiner <clement@mux.me>
# Copyright (c) 2015      Lucas Kanashiro <kanashiro.duarte@gmail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

from debile.master.orm import (Person, Builder, Suite, Component, Arch, Check,
                               Group, GroupSuite, Source, Binary, Job)
from debile.master.keyrings import import_pgp, import_ssl, clean_ssl_keyring
from debile.master.utils import emit

from debian.debian_support import Version
from datetime import datetime, timedelta

import threading
import logging


NAMESPACE = threading.local()


def generic_method(fn):
    def _(*args, **kwargs):
        try:
            return fn(*args, **kwargs)
        except:
            logger = logging.getLogger('debile')
            logger.debug("Caught exception when processing xmlrpc request.", exc_info=True)
            raise
    return _


def builder_method(fn):
    def _(*args, **kwargs):
        if not NAMESPACE.machine:
            raise Exception("You can't do that")
        try:
            return fn(*args, **kwargs)
        except:
            logger = logging.getLogger('debile')
            logger.debug("Caught exception when processing xmlrpc request.", exc_info=True)
            raise
    return _


def user_method(fn):
    def _(*args, **kwargs):
        if not NAMESPACE.user:
            raise Exception("You can't do that")
        try:
            return fn(*args, **kwargs)
        except:
            logger = logging.getLogger('debile')
            logger.debug("Caught exception when processing xmlrpc request.", exc_info=True)
            raise
    return _


class DebileMasterInterface(object):
    """
    This is the exposed interface for the builders. Code enhancing the server
    should likely go here, unless you know what you're doing.
    """

    shutdown_request = False

    def __init__(self, ssl_keyring=None, pgp_keyring=None):
        self.ssl_keyring = ssl_keyring
        self.pgp_keyring = pgp_keyring

    # Simple stuff.

    @builder_method
    def builder_whoami(self):
        """
        ID check
        """
        return NAMESPACE.machine.name

    @user_method
    def user_whoami(self):
        """
        ID check
        """
        return NAMESPACE.user.name

    # The following trio of methods handle the job control.

    @builder_method
    def get_next_job(self, suites, components, arches, checks):
        NAMESPACE.machine.last_ping = datetime.utcnow()

        if self.__class__.shutdown_request:
            return None

        arches = [x for x in arches if x not in ["source", "all"]]
        job = NAMESPACE.session.query(Job).join(Job.source).join(Source.group_suite).filter(
            ~Job.depedencies.any(),
            Job.dose_report == None,
            Job.assigned_at == None,
            Job.finished_at == None,
            Job.failed.is_(None),
            GroupSuite.suite.has(Suite.name.in_(suites)),
            Source.component.has(Component.name.in_(components)),
            (Job.arch.has(Arch.name.in_(arches)) |
             (Job.arch.has(Arch.name.in_(["source", "all"])) &
              Source.affinity.has(Arch.name.in_(arches)))),
            Job.check.has(Check.name.in_(checks)),
        ).order_by(
            Job.assigned_count.asc(),
            Source.uploaded_at.asc(),
        ).first()

        if job is None:
            return None

        job.assigned_count += 1
        job.assigned_at = datetime.utcnow()
        job.builder = NAMESPACE.machine

        emit('start', 'job', job.debilize())

        return job.debilize()

    @builder_method
    def close_job(self, job_id, failed):
        job = NAMESPACE.session.query(Job).get(job_id)
        job.finished_at = datetime.utcnow()

        emit('complete', 'job', job.debilize())

        return True

    @builder_method
    def forfeit_job(self, job_id):
        job = NAMESPACE.session.query(Job).get(job_id)
        job.assigned_at = None
        job.builder = None

        emit('abort', 'job', job.debilize())

        return True

    # Useful methods below.

    @generic_method
    def get_group(self, group_id):
        return NAMESPACE.session.query(Group).get(group_id).debilize()

    @generic_method
    def get_source(self, source_id):
        return NAMESPACE.session.query(Source).get(source_id).debilize()

    @generic_method
    def get_binary(self, binary_id):
        return NAMESPACE.session.query(Binary).get(binary_id).debilize()

    @generic_method
    def get_job(self, job_id):
        return NAMESPACE.session.query(Job).get(job_id).debilize()

    # Creating builders/users

    @user_method
    def create_builder(self, name, pgp, ssl=None, ip=None):
        """
        :param str name: name of the builder
        :param str pgp: path to the pgp public keyfile
        :param str ssl: path to the ssl certificate
        :param str ip: ip address of the builder
        """

        if NAMESPACE.session.query(Builder).filter_by(name=name).first():
            raise ValueError("Slave already exists.")

        pgp = import_pgp(self.pgp_keyring, pgp)
        if ssl is not None:
            ssl = import_ssl(self.ssl_keyring, ssl, name)
            b = Builder(name=name, maintainer=NAMESPACE.user, pgp=pgp, ssl=ssl,
                        last_ping=datetime.utcnow())

        elif ip is not None:
            b = Builder(name=name, maintainer=NAMESPACE.user, pgp=pgp, ip=ip,
                        last_ping=datetime.utcnow())

        else:
            raise ValueError("Need either ssl certificate or ip address")

        NAMESPACE.session.add(b)

        emit('create', 'slave', b.debilize())