# -*- coding: utf-8 -*-
#
# -----------------------------------------------------------------------------------
# Copyright (c) Microsoft Open Technologies (Shanghai) Co. Ltd.  All rights reserved.
#
# The MIT License (MIT)
#
# 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.
# -----------------------------------------------------------------------------------

import sys

sys.path.append("..")

from hackathon import (
    Component,
    RequiredFeature,
)

from hackathon.constants import (
    EStatus,
    VERemoteProvider,
    VE_PROVIDER,
    PortBindingType,
    VEStatus,
    ReservedUser,
    AVMStatus,
)
from hackathon.database.models import (
    VirtualEnvironment,
    DockerHostServer,
    Experiment,
    Hackathon,
    Template,
    User,
    HackathonTemplateRel)

from hackathon.azureformation.azureFormation import (
    AzureFormation,
)

from hackathon.hackathon_response import (
    internal_server_error,
    precondition_failed,
    not_found,
    forbidden,
    ok,
)

from hackathon.template.docker_template_unit import (
    DockerTemplateUnit,
)
from hackathon.template.base_template import (
    BaseTemplate,
)
import json
import random
import string
from sqlalchemy import (
    and_,
)

from datetime import timedelta
from hackathon.constants import CLOUD_ECLIPSE


class ExprManager(Component):
    register_manager = RequiredFeature("register_manager")
    user_manager = RequiredFeature("user_manager")
    hackathon_manager = RequiredFeature("hackathon_manager")
    admin_Manager = RequiredFeature("admin_manager")
    template_manager = RequiredFeature("template_manager")
    docker = RequiredFeature("docker")
    scheduler = RequiredFeature("scheduler")

    def start_expr(self, hackathon_name, template_name, user_id):
        """
        A user uses a template to start a experiment under a hackathon
        :param hackathon_name:
        :param template_name:
        :param user_id:
        :return:
        """
        hack_temp = self.__check_template_status(hackathon_name, template_name)
        if hack_temp is None:
            return not_found('hackathon or template is not existed')

        hackathon = hack_temp[0]
        # if not self.register_manager.is_user_registered(user_id, hackathon):
        #     return forbidden("user not registered or not approved")

        if hackathon.event_end_time < self.util.get_now():
            self.log.warn("hackathon is ended. The expr starting process will be stopped")
            return precondition_failed('hackathen is ended')

        template = hack_temp[1]

        if user_id > 0:
            expr = self.__check_expr_status(user_id, hackathon, template)
            if expr is not None:
                return self.__report_expr_status(expr)

        # new expr
        return self.__start_new_expr(hackathon, template, user_id)

    def heart_beat(self, expr_id):
        expr = self.db.find_first_object_by(Experiment, id=expr_id, status=EStatus.RUNNING)
        if expr is None:
            return not_found('Experiment does not running')

        expr.last_heart_beat_time = self.util.get_now()
        self.db.commit()
        return ok('OK')

    def stop_expr(self, expr_id, force=0):
        """
        :param expr_id: experiment id
        :param force: 0: only stop container and release ports, 1: force stop and delete container and release ports.
        :return:
        """
        self.log.debug("begin to stop %d" % expr_id)
        expr = self.db.find_first_object_by(Experiment, id=expr_id, status=EStatus.RUNNING)
        if expr is not None:
            # Docker
            docker = self.docker.get_docker(expr.hackathon)
            if expr.template.provider == VE_PROVIDER.DOCKER:
                # stop containers
                for c in expr.virtual_environments.all():
                    try:
                        self.log.debug("begin to stop %s" % c.name)
                        if force:
                            docker.delete(c.name, virtual_environment=c, container=c.container, expr_id=expr_id)
                            c.status = VEStatus.DELETED
                        else:
                            docker.stop(c.name, virtual_environment=c, container=c.container, expr_id=expr_id)
                            c.status = VEStatus.STOPPED
                    except Exception as e:
                        self.log.error(e)
                        self.__roll_back(expr_id)
                        return internal_server_error('Failed stop/delete container')
                if force:
                    expr.status = EStatus.DELETED
                else:
                    expr.status = EStatus.STOPPED
                self.db.commit()
            else:
                try:
                    # todo support delete azure vm
                    hosted_docker = RequiredFeature("hosted_docker")
                    af = AzureFormation(hosted_docker.load_azure_key_id(expr_id))
                    af.stop(expr_id, AVMStatus.STOPPED_DEALLOCATED)
                except Exception as e:
                    self.log.error(e)
                    return internal_server_error('Failed stopping azure')

            self.log.debug("experiment %d ended success" % expr_id)
            return ok('OK')
        else:
            return ok('expr not exist')

    def get_expr_status(self, expr_id):
        expr = self.db.find_first_object_by(Experiment, id=expr_id)
        if expr is not None:
            return self.__report_expr_status(expr)
        else:
            return not_found('Experiment Not found')

    def get_expr_list_by_user_id(self, user_id):
        return map(lambda u: u.dic(),
                   self.db.find_all_objects(Experiment, and_(Experiment.user_id == user_id,
                                                             Experiment.status < 5)))

    def get_expr_list_by_hackathon_id(self, hackathon_id, **kwargs):
        condition = self.__get_filter_condition(hackathon_id, **kwargs)
        experiments = self.db.find_all_objects(Experiment, condition)
        return map(lambda u: self.__get_expr_with_user_info(u), experiments)

    def scheduler_recycle_expr(self):
        """recycle experiment acrroding to hackathon basic info on recycle configuration

        According to the hackathon's basic info on 'recycle_enabled', find out time out experiments
        Then call function to recycle them

        :return:
        """
        self.log.debug("start checking recyclable experiment ... ")
        for hackathon in self.hackathon_manager.get_recyclable_hackathon_list():
            # check recycle enabled
            mins = self.hackathon_manager.get_recycle_minutes(hackathon)
            expr_time_cond = Experiment.create_time < self.util.get_now() - timedelta(minutes=mins)
            status_cond = Experiment.status == EStatus.RUNNING
            # filter out the experiments that need to be recycled
            exprs = self.db.find_all_objects(Experiment,
                                             status_cond,
                                             expr_time_cond,
                                             Experiment.hackathon_id == hackathon.id)
            for expr in exprs:
                self.__recycle_expr(expr)

    def schedule_pre_allocate_expr_job(self):
        next_run_time = self.util.get_now() + timedelta(seconds=1)
        self.scheduler.add_interval(feature="expr_manager",
                                    method="pre_allocate_expr",
                                    id="pre_allocate_expr",
                                    next_run_time=next_run_time,
                                    minutes=self.util.safe_get_config("pre_allocate.check_interval_minutes", 5))

    def pre_allocate_expr(self):
        # only deal with online hackathons
        hackathon_id_list = self.hackathon_manager.get_pre_allocate_enabled_hackathon_list()
        htrs = self.db.find_all_objects(HackathonTemplateRel, HackathonTemplateRel.hackathon_id.in_(hackathon_id_list))
        templates = map(lambda x: x.template, htrs)
        for template in templates:
            try:
                pre_num = self.hackathon_manager.get_pre_allocate_number(template.hackathon)
                curr_num = self.db.count(Experiment,
                                         Experiment.user_id == ReservedUser.DefaultUserID,
                                         Experiment.template_id == template.id,