"""
Copyright 2015 Hewlett-Packard

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

"""

import uuid

import falcon
from oslo_config import cfg

from freezer_api.api.common import resource
from freezer_api.common import exceptions as freezer_api_exc
from freezer_api.keystone_client import KeystoneClient
from freezer_api import policy


CONF = cfg.CONF


class JobsBaseResource(resource.BaseResource):
    """
    Base class able to create actions contained in a job document

    """
    def __init__(self, storage_driver):
        self.db = storage_driver

    def get_action(self, project_id, action_id):
        found_action = None
        try:
            found_action = self.db.get_action(project_id=project_id,
                                              action_id=action_id)
        except freezer_api_exc.DocumentNotFound:
            pass
        return found_action

    def update_actions_in_job(self, project_id, user_id, job_doc):
        """
        Looks into a job document and creates actions in the db.
        Actions are given an action_id if they don't have one yet
        """
        job = Job(job_doc)
        for action in job.actions():
            if action.action_id:
                # action has action_id, let's see if it's in the db
                found_action_doc = self.get_action(project_id=project_id,
                                                   action_id=action.action_id)
                if found_action_doc:
                    if action == Action(found_action_doc):
                        # action already present in the db, do nothing
                        continue
                    else:
                        # action is different, generate new action_id
                        action.action_id = ''
                # action not found in db, leave current action_id
            self.db.add_action(project_id=project_id,
                               user_id=user_id,
                               doc=action.doc)

    def _filter_pid(self, req, project_id, obj_list):
        context = req.env.get('freezer.context')
        if not isinstance(obj_list, list):
            obj_list = [obj_list]

        for obj in obj_list:
            client_id = obj.get('client_id')
            client_info = self.db.get_client(project_id=project_id,
                                             client_id=client_id)
            client_owner = None
            if client_info:
                client_owner = client_info[0].get('project_id')

            if context.project_id != client_owner:
                if 'job_schedule' in obj:
                    obj['job_schedule'].pop('current_pid', None)

    def _should_create_trust(self, project_id, job_doc):
        """
        Implements internal logic on when trusts should be created.
        By default, we create trusts for clients registered as central,
        when [centralized_scheduler]/enabled is True (default).
        We always create trusts if [centralized_scheduler]/enforce_trusts
        is True (default is False).
        Trusts are never created when [centralized_scheduler]/enabled is
        explicitly set to False.
        """
        if not CONF.centralized_scheduler.enabled:
            return False
        if CONF.centralized_scheduler.enforce_trusts:
            return True
        client_id = job_doc.get('client_id')
        if not client_id:
            return False
        client_doc = self.db.get_client(project_id=project_id,
                                        client_id=client_id)
        if client_doc and client_doc[0].get('client', {}).get('is_central'):
            return True
        return False


class JobsCollectionResource(JobsBaseResource):
    """
    Handler for endpoint: /v2/{project_id}/jobs
    """

    @policy.enforce('jobs:get_all')
    def on_get(self, req, resp, project_id):
        # GET /v2/{project_id}/jobs(?limit,offset)     Lists jobs
        offset = req.get_param_as_int('offset') or 0
        limit = req.get_param_as_int('limit') or 10
        all_projects = req.get_param_as_bool('all_projects') or False
        search = self.json_body(req)
        all_projects = policy.can(
            'jobs:get_all_projects', req.env['freezer.context'],
            do_raise=False
        )
        obj_list = self.db.search_job(project_id=project_id,
                                      all_projects=all_projects,
                                      offset=offset, limit=limit,
                                      search=search)
        if not all_projects:
            self._filter_pid(req, project_id, obj_list)
        resp.media = {'jobs': obj_list}

    @policy.enforce('jobs:create')
    def on_post(self, req, resp, project_id):
        # POST /v2/{project_id}/jobs    Creates job entry
        try:
            job = Job(self.json_body(req))
        except KeyError:
            raise freezer_api_exc.BadDataFormat(
                message='Missing request body')
        user_id = req.context.user_id
        self.update_actions_in_job(project_id, user_id, job.doc)
        if self._should_create_trust(project_id, job.doc):
            ks_client = req.env['freezer.context'].keystone_client
            trust = ks_client.create_trust(user_id, project_id)
            job.doc['user_credentials'] = {
                'trust_id': trust.id,
                'trustor_user_id': trust.trustor_user_id,
            }
        job_id = self.db.add_job(project_id=project_id,
                                 user_id=user_id, doc=job.doc)
        resp.status = falcon.HTTP_201
        resp.media = {'job_id': job_id}


class JobsResource(JobsBaseResource):
    """
    Handler for endpoint: /v2/{project_id}/jobs/{job_id}
    """

    @policy.enforce('jobs:get')
    def on_get(self, req, resp, project_id, job_id):
        # GET /v2/{project_id}/jobs/{job_id}     retrieves the specified job
        all_projects = policy.can('jobs:get_all_projects',
                                  req.env['freezer.context'],
                                  do_raise=False)
        obj = self.db.get_job(project_id=project_id, job_id=job_id,
                              all_projects=all_projects)
        if obj:
            all_projects = policy.can(
                'jobs:get_all_projects', req.env['freezer.context'],
                do_raise=False
            )
            if not all_projects:
                self._filter_pid(req, project_id, obj)
            resp.media = obj
        else:
            resp.status = falcon.HTTP_404

    @policy.enforce('jobs:delete')
    def on_delete(self, req, resp, project_id, job_id):
        # DELETE /v2/{project_id}/jobs/{job_id}     Deletes the specified job
        user_id = req.context.user_id
        job_id, trust_id = self.db.delete_job(project_id=project_id,
                                              user_id=user_id, job_id=job_id)
        if trust_id:
            trust_in_use = self.db.trust_in_use(trust_id)
            if not trust_in_use:
                client = KeystoneClient(req.context)
                client.delete_trust(trust_id, project_id)
        resp.media = {'job_id': job_id}
        resp.status = falcon.HTTP_204

    @policy.enforce('jobs:update')
    def on_patch(self, req, resp, project_id, job_id):
        # PATCH /v2/{project_id}/jobs/{job_id}     updates the specified job
        user_id = req.context.user_id
        job = Job(self.json_body(req))
        self.update_actions_in_job(project_id, user_id, job.doc)
        new_version = self.db.update_job(project_id=project_id,
                                         user_id=user_id,
                                         job_id=job_id,
                                         patch_doc=job.doc)
        resp.media = {'job_id': job_id, 'version': new_version}

    @policy.enforce('jobs:create')
    def on_post(self, req, resp, project_id, job_id):
        # PUT /v2/{project_id}/jobs/{job_id}
        # Creates/Replaces the specified job
        user_id = req.context.user_id
        job = Job(self.json_body(req))
        self.update_actions_in_job(project_id, user_id, job.doc)
        new_version = self.db.replace_job(project_id=project_id,
                                          user_id=user_id,
                                          job_id=job_id,
                                          doc=job.doc)
        resp.status = falcon.HTTP_201
        resp.media = {'job_id': job_id, 'version': new_version}


class JobsEvent(resource.BaseResource):
    """
    Handler for endpoint: /v2/{project_id}/jobs/{job_id}/event

    Actions are passed in the body, for example:
    {
        "start": null
    }
    """
    def __init__(self, storage_driver):
        self.db = storage_driver

    @policy.enforce('jobs:event:create')
    def on_post(self, req, resp, project_id, job_id):
        # POST /v2/{project_id}/jobs/{job_id}/event
        # requests an event on the specified job

        doc = self.json_body(req)

        try:
            event, params = next(iter(doc.items()))
        except Exception:
            raise freezer_api_exc.BadDataFormat("Bad event request format")

        job_doc = self.db.get_job(project_id=project_id,
                                  job_id=job_id)
        job = Job(job_doc)
        result = job.execute_event(event, params)

        if job.need_update:
            user_id = req.context.user_id
            self.db.replace_job(project_id=project_id,
                                user_id=user_id,
                                job_id=job_id,
                                doc=job.doc)
        resp.status = falcon.HTTP_202
        resp.media = {'result': result}


class Action(object):
    def __init__(self, doc):
        self.doc = doc

    @property
    def action_id(self):
        return self.doc.get('action_id', '')

    @action_id.setter
    def action_id(self, value):
        self.doc['action_id'] = value

    def create_new_action_id(self):
        self.doc['action_id'] = uuid.uuid4().hex

    def __eq__(self, other):
        # return self.doc == other.doc
        dont_care_keys = ['_version', 'user_id']
        lh = self.doc.get('freezer_action', None)
        rh = other.doc.get('freezer_action', None)
        diffkeys = [k for k in lh if lh[k] != rh.get(k)]
        diffkeys += [k for k in rh if rh[k] != lh.get(k)]
        for k in diffkeys:
            if k not in dont_care_keys:
                return False
        return True

    def __ne__(self, other):
        return not (self.__eq__(other))


class Job(object):
    """
    A class with knowledge of the inner working of a job data structure.

    Responibilities:
     - manage the events that can be sent to a job. The result of handling
       an event is a modification of the information contained in the
       job document
     - extract actions from a job (usage example: to be used to create actions)
    """
    def __init__(self, doc):
        self.doc = doc
        if self.doc.get("action_defaults") is not None:
            self.expand_default_properties()
        self.event_result = ''
        self.need_update = False
        if 'job_schedule' not in doc:
            doc['job_schedule'] = {}
        self.job_schedule = doc['job_schedule']
        self.event_handlers = {'start': self.start,
                               'stop': self.stop,
                               'abort': self.abort}

    def execute_event(self, event, params):
        handler = self.event_handlers.get(event, None)
        if not handler:
            raise freezer_api_exc.BadDataFormat("Bad Action Method")
        try:
            self.event_result = handler(params)
        except freezer_api_exc.BadDataFormat:
            raise
        except Exception as e:
            raise freezer_api_exc.FreezerAPIException(e)
        return self.event_result

    @property
    def job_status(self):
        return self.job_schedule.get('status', '')

    @job_status.setter
    def job_status(self, value):
        self.job_schedule['status'] = value

    def start(self, params=None):
        if self.job_schedule.get('event') != 'start':
            self.job_schedule['event'] = 'start'
            self.job_schedule['status'] = ''
            self.job_schedule['result'] = ''
            self.need_update = True
            return 'success'
        return 'start already requested'

    def stop(self, params=None):
        if self.job_schedule.get('event') != 'stop':
            self.job_schedule['event'] = 'stop'
            self.need_update = True
            return 'success'
        return 'stop already requested'

    def abort(self, params=None):
        if self.job_schedule.get('event') != 'abort':
            self.job_schedule['event'] = 'abort'
            self.need_update = True
            return 'success'
        return 'abort already requested'

    def actions(self):
        """
        Generator to iterate over the actions contained in a job

        :return: yields Action objects
        """
        for action_doc in self.doc.get('job_actions', []):
            yield Action(action_doc)

    def expand_default_properties(self):
        action_defaults = self.doc.pop("action_defaults")
        if isinstance(action_defaults, dict):
            for key, val in action_defaults.items():
                for action in self.doc.get("job_actions"):
                    if action["freezer_action"].get(key) is None:
                        action["freezer_action"][key] = val
        else:
            raise freezer_api_exc.BadDataFormat(
                message="action_defaults shouldbe a dictionary"
            )
