"""
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 freezer_api.api.common import resource
from freezer_api.common import exceptions as freezer_api_exc
from freezer_api import policy


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, user_id, action_id):
        found_action = None
        try:
            found_action = self.db.get_action(project_id=project_id,
                                              user_id=user_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,
                                                   user_id=user_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)


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
        user_id = req.get_header('X-User-ID')
        offset = req.get_param_as_int('offset') or 0
        limit = req.get_param_as_int('limit') or 10
        search = self.json_body(req)
        obj_list = self.db.search_job(project_id=project_id,
                                      user_id=user_id, offset=offset,
                                      limit=limit, search=search)
        resp.body = {'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.get_header('X-User-ID')
        self.update_actions_in_job(project_id, user_id, job.doc)
        job_id = self.db.add_job(project_id=project_id,
                                 user_id=user_id, doc=job.doc)
        resp.status = falcon.HTTP_201
        resp.body = {'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
        # search in body
        user_id = req.get_header('X-User-ID') or ''
        obj = self.db.get_job(project_id=project_id,
                              user_id=user_id, job_id=job_id)
        if obj:
            resp.body = 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.get_header('X-User-ID')
        self.db.delete_job(project_id=project_id,
                           user_id=user_id, job_id=job_id)
        resp.body = {'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.get_header('X-User-ID') or ''
        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.body = {'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.get_header('X-User-ID') or ''
        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.body = {'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

        user_id = req.get_header('X-User-ID') or ''
        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,
                                  user_id=user_id,
                                  job_id=job_id)
        job = Job(job_doc)
        result = job.execute_event(event, params)

        if job.need_update:
            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.body = {'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"
            )
