# (c) 2016, Marcos Diez <marcos@unitron.com.br>
# https://github.com/marcosdiez/
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

from __future__ import (absolute_import, division, print_function)

__metaclass__ = type

DOCUMENTATION = '''
name: mongodb
author:
  - Marcos Diez (@marcosdiez)
version_added: "1.0.0"
short_description: lookup info from MongoDB
description:
    - 'The ``MongoDB`` lookup runs the *find()* command on a given *collection* on a given *MongoDB* server.'
    - 'The result is a list of jsons, so slightly different from what PyMongo returns. In particular, *timestamps* are converted to epoch integers.'
options:
    connect_string:
        description:
            - Can be any valid MongoDB connection string, supporting authentication, replica sets, etc.
            - "More info at U(https://docs.mongodb.org/manual/reference/connection-string/)"
        default: "mongodb://localhost/"
    database:
        description:
            - Name of the database which the query will be made
        required: True
    collection:
        description:
            - Name of the collection which the query will be made
        required: True
    filter:
        description:
            - Criteria of the output
        type: 'dict'
        default: {}
    projection:
        description:
            - Fields you want returned
        type: dict
        default: {}
    skip:
        description:
            - How many results should be skipped
        type: integer
    limit:
        description:
            - How many results should be shown
        type: integer
    sort:
        description:
            - Sorting rules.
            - Please use the strings C(ASCENDING) and C(DESCENDING) to set the order.
            - Check the example for more information.
        type: list
        elements: list
        default: []
    extra_connection_parameters:
        description:
            - Extra connection parameters that to be sent to pymongo.MongoClient
            - Check the example to see how to connect to mongo using an SSL certificate.
            - "All possible parameters are here: U(https://api.mongodb.com/python/current/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient)"
        type: dict
        default: {}
notes:
    - "Please check https://api.mongodb.org/python/current/api/pymongo/collection.html?highlight=find#pymongo.collection.Collection.find for more details."
requirements:
    - pymongo >= 2.4 (python library)
'''

EXAMPLES = '''
- hosts: localhost
  gather_facts: false
  vars:
    mongodb_parameters:
      #mandatory parameters
      database: 'local'
      collection: "startup_log"
      #optional
      connection_string: "mongodb://localhost/"
      # connection_string: "mongodb://username:password@my.server.com:27017/"
      # extra_connection_parameters: { "ssl" : True , "ssl_certfile": /etc/self_signed_certificate.pem" }
      #optional query  parameters, we accept any parameter from the normal mongodb query.
      # filter:  { "hostname": "u18" }
      projection: { "pid": True    , "_id" : False , "hostname" : True }
      skip: 0
      limit: 1
      sort:  [ [ "startTime" , "ASCENDING" ] , [ "age", "DESCENDING" ] ]
  tasks:
    - debug: msg="The PID from MongoDB is {{ lookup('mongodb', mongodb_parameters ).pid }}"

    - debug: msg="The HostName from the MongoDB server is {{ lookup('mongodb', mongodb_parameters ).hostname }}"

    - debug: msg="Mongo DB is stored at {{ lookup('mongodb', mongodb_parameters_inline )}}"
      vars:
        mongodb_parameters_inline:
          database: 'local'
          collection: "startup_log"
          connection_string: "mongodb://localhost/"
          limit: 1
          projection: { "cmdline.storage": True }

      # lookup syntax, does the same as below
    - debug: msg="The hostname is {{ item.hostname }} and the pid is {{ item.pid }}"
      loop: "{{ lookup('mongodb', mongodb_parameters, wantlist=True) }}"

      # query syntax, does the same as above
    - debug: msg="The hostname is {{ item.hostname }} and the pid is {{ item.pid }}"
      loop: "{{ query('mongodb', mongodb_parameters) }}"

    - name: "Raw output from the mongodb lookup (a json with pid and hostname )"
      debug: msg="{{ lookup('mongodb', mongodb_parameters) }}"

    - name: "Yet another mongodb query, now with the parameters on the task itself"
      debug: msg="pid={{item.pid}} hostname={{item.hostname}} version={{ item.buildinfo.version }}"
      with_mongodb:
        - database: 'local'
          collection: "startup_log"
          connection_string: "mongodb://localhost/"
          limit: 1
          projection: { "pid": True    , "hostname": True , "buildinfo.version": True }

    # Please notice this specific query may result more than one result. This is expected
    - name: "Shows the whole output from mongodb"
      debug: msg="{{ item }}"
      with_mongodb:
        - database: 'local'
          collection: "startup_log"
          connection_string: "mongodb://localhost/"


'''

RETURN = """
  _list_of_jsons:
    description:
      - a list of JSONs with the results of the MongoDB query.
    type: list
"""

import datetime

from ansible.module_utils.six import string_types, integer_types
from ansible.module_utils._text import to_native
from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase

try:
    from pymongo import ASCENDING, DESCENDING
    from pymongo.errors import ConnectionFailure
    from pymongo import MongoClient
except ImportError:
    try:  # for older PyMongo 2.2
        from pymongo import Connection as MongoClient
    except ImportError:
        pymongo_found = False
    else:
        pymongo_found = True
else:
    pymongo_found = True


class LookupModule(LookupBase):

    def _fix_sort_parameter(self, sort_parameter):
        if sort_parameter is None:
            return sort_parameter

        if not isinstance(sort_parameter, list):
            raise AnsibleError(u"Error. Sort parameters must be a list, not [ {0} ]".format(sort_parameter))

        for item in sort_parameter:
            self._convert_sort_string_to_constant(item)

        return sort_parameter

    def _convert_sort_string_to_constant(self, item):
        original_sort_order = item[1]
        sort_order = original_sort_order.upper()
        if sort_order == u"ASCENDING":
            item[1] = ASCENDING
        elif sort_order == u"DESCENDING":
            item[1] = DESCENDING
        # else the user knows what s/he is doing and we won't predict. PyMongo will return an error if necessary

    def convert_mongo_result_to_valid_json(self, result):
        if result is None:
            return result
        if isinstance(result, integer_types + (float, bool)):
            return result
        if isinstance(result, string_types):
            return result
        elif isinstance(result, list):
            new_list = []
            for elem in result:
                new_list.append(self.convert_mongo_result_to_valid_json(elem))
            return new_list
        elif isinstance(result, dict):
            new_dict = {}
            for key in result.keys():
                value = result[key]  # python2 and 3 compatible....
                new_dict[key] = self.convert_mongo_result_to_valid_json(value)
            return new_dict
        elif isinstance(result, datetime.datetime):
            # epoch
            return (result - datetime.datetime(1970, 1, 1)). total_seconds()
        else:
            # failsafe
            return u"{0}".format(result)

    def run(self, terms, variables, **kwargs):
        try:
            return self._run_helper(terms)
        except Exception as e:
            print(u"There was an exception on the mongodb_lookup: {0}".format(to_native(e)))
            raise e

    def _run_helper(self, terms):
        if not pymongo_found:
            raise AnsibleError(u"pymongo is required in the control node (this machine) for mongodb lookup.")
        ret = []
        for term in terms:
            for required_parameter in [u"database", u"collection"]:
                if required_parameter not in term:
                    raise AnsibleError(u"missing mandatory parameter [{0}]".format(required_parameter))

            connection_string = term.get(u'connection_string', u"mongodb://localhost")
            database = term[u"database"]
            collection = term[u'collection']
            extra_connection_parameters = term.get(u'extra_connection_parameters', {})

            if u"extra_connection_parameters" in term:
                del term[u"extra_connection_parameters"]
            if u"connection_string" in term:
                del term[u"connection_string"]
            del term[u"database"]
            del term[u"collection"]

            if u"sort" in term:
                term[u"sort"] = self._fix_sort_parameter(term[u"sort"])

            # all other parameters are sent to mongo, so we are future and past proof

            try:
                client = MongoClient(connection_string, **extra_connection_parameters)
                results = client[database][collection].find(**term)

                for result in results:
                    result = self.convert_mongo_result_to_valid_json(result)
                    ret.append(result)

            except ConnectionFailure as e:
                raise AnsibleError(u'unable to connect to database: %s' % str(e))

        return ret
