#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: (c) 2020, Rhys Campbell (@rhysmeister) <rhys.james.campbell@googlemail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
__metaclass__ = type


DOCUMENTATION = r'''
---
module: mongodb_index

short_description: Creates or drops indexes on MongoDB collections.

description:
  - Creates or drops indexes on MongoDB collections.
  - Supports multiple index options, i.e. unique, sparse and partial.
  - Validates existence of indexes by name only.

author: Rhys Campbell (@rhysmeister)
version_added: "1.0.0"

extends_documentation_fragment:
  - community.mongodb.login_options
  - community.mongodb.ssl_options

options:
  indexes:
    description:
      - List of indexes to create or drop
    type: list
    elements: raw
    required: yes
  replica_set:
    description:
      - Replica set to connect to (automatically connects to primary for writes).
    type: str
notes:
    - Requires the pymongo Python package on the remote host, version 2.4.2+.

requirements:
  - pymongo
'''

EXAMPLES = r'''
- name: Create a single index on a collection
  community.mongodb.mongodb_index:
    login_user: admin
    login_password: secret
    indexes:
      - database: mydb
        collection: test
        keys:
          - username: 1
            last_login: -1
        options:
          name: myindex
        state: present

- name: Drop an index on a collection
  community.mongodb.mongodb_index:
    login_user: admin
    login_password: secret
    indexes:
      - database: mydb
        collection: test
        options:
          name: myindex
        state: absent

- name: Create multiple indexes
  community.mongodb.mongodb_index:
    login_user: admin
    login_password: secret
    indexes:
      - database: mydb
        collection: test
        keys:
          - username: 1
            last_login: -1
        options:
          name: myindex
        state: present
      - database: mydb
        collection: test
        keys:
          - email: 1
            last_login: -1
        options:
          name: myindex2
        state: present

- name: Add a unique index
  community.mongodb.mongodb_index:
    login_port: 27017
    login_user: admin
    login_password: secret
    login_database: "admin"
    indexes:
      - database: "test"
        collection: "rhys"
        keys:
          username: 1
        options:
          name: myuniqueindex
          unique: true
        state: present

- name: Add a ttl index
  community.mongodb.mongodb_index:
    login_port: 27017
    login_user: admin
    login_password: secret
    login_database: "admin"
    indexes:
      - database: "test"
        collection: "rhys"
        keys:
          created: 1
        options:
          name: myttlindex
          expireAfterSeconds: 3600
        state: present

- name: Add a sparse index
  community.mongodb.mongodb_index:
    login_port: 27017
    login_user: admin
    login_password: secret
    login_database: "admin"
    indexes:
      - database: "test"
        collection: "rhys"
        keys:
          last_login: -1
        options:
          name: mysparseindex
          sparse: true
        state: present

- name: Add a partial index
  community.mongodb.mongodb_index:
    login_port: 27017
    login_user: admin
    login_password: secret
    login_database: "admin"
    indexes:
      - database: "test"
        collection: "rhys"
        keys:
          last_login: -1
        options:
          name: mypartialindex
          partialFilterExpression:
            rating:
              $gt: 5
        state: present

- name: Add a index in the background (background option is deprecated from 4.2+)
  community.mongodb.mongodb_index:
    login_port: 27017
    login_user: admin
    login_password: secret
    login_database: "admin"
    indexes:
      - database: "test"
        collection: "rhys"
        options:
          name: idxbackground
        keys:
          username: -1
        backgroud: true
        state: present

- name: Check creating 5 index all with multiple options specified
  community.mongodb.mongodb_index:
    login_port: 27017
    login_user: admin
    login_password: secret
    login_database: "admin"
    indexes:
      - database: "test"
        collection: "indextest"
        options:
          name: "idx_unq_username"
          unique: true
        keys:
          username: -1
        state: present
      - database: "test"
        collection: "indextest"
        options:
          name: "idx_last_login"
          sparse: true
        keys:
          last_login: -1
        state: present
      - database: "test"
        collection: "indextest"
        options:
          name: "myindex"
        keys:
          first_name: 1
          last_name: -1
          city: 1
        state: present
      - database: "test"
        collection: partialtest
        options:
          name: "idx_partialtest"
          partialFilterExpression:
            rating:
              $gt: 5
        keys:
          rating: -1
          title: 1
        state: present
      - database: "test"
        collection: "wideindex"
        options:
          name: "mywideindex"
        keys:
          email: -1
          username: 1
          first_name: 1
          last_name: 1
          dob: -1
          city: 1
          last_login: -1
          review_count: 1
          rating_count: 1
          last_post: -1
        state: present
'''

RETURN = r'''
indexes_created:
  description: List of indexes created.
  returned: always
  type: list
  sample: ["myindex", "myindex2"]
indexes_dropped:
  description: List of indexes dropped.
  returned: always
  type: list
  sample: ["myindex", "myindex2"]
changed:
  description: Indicates the module has changed something.
  returned: When the module has changed something.
  type: bool
failed:
  description: Indicates the module has failed.
  returned: When the module has encountered an error.
  type: bool
'''

from uuid import UUID

from distutils.version import LooseVersion

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible.module_utils.six import iteritems
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import (
    check_compatibility,
    missing_required_lib,
    mongodb_common_argument_spec,
    ssl_connection_options
)
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import PyMongoVersion, PYMONGO_IMP_ERR, pymongo_found, MongoClient
from ansible_collections.community.mongodb.plugins.module_utils.mongodb_common import index_exists, create_index, drop_index


def validate_module(module):
    '''
    Runs validation rules specific the mongodb_index module
    '''
    required_index_keys = [
        "database",
        "collection",
        "options",
        "state",
    ]
    indexes = module.params['indexes']

    if len(indexes) == 0:
        module.fail_json(msg="One or more indexes must be specified")
    if not all(isinstance(i, dict) for i in indexes):
        module.fail_json(msg="Indexes must be supplied as dictionaries")

    # Ensure keys are present in index spec
    for k in required_index_keys:
        for i in indexes:
            if k not in i.keys():
                module.fail_json(msg="Missing required index key {0}".format(k))

    # Check index subkeys look correct
    for i in indexes:
        if not isinstance(i["database"], str):
            module.fail_json(msg="database key should be str")
        elif not isinstance(i["collection"], str):
            module.fail_json(msg="collection key should be str")
        elif i["state"] == "present" and "keys" not in i.keys():
            module.fail_json(msg="keys must be supplied when state is present")
        elif i["state"] == "present" and not isinstance(i["keys"], dict):
            module.fail_json(msg="keys key should be dict")
        elif not isinstance(i["options"], dict):
            module.fail_json(msg="options key should be dict")
        elif "name" not in i["options"]:
            module.fail_json(msg="The options dict must contain a name field")
        elif i["state"] not in ["present", "absent"]:
            module.fail_json(msg="state must be one of present or absent")


# ================
# Module execution
#
def main():
    argument_spec = mongodb_common_argument_spec()
    argument_spec.update(
        indexes=dict(type='list', elements='raw', required=True),
        replica_set=dict(type='str'),
    )
    module = AnsibleModule(
        argument_spec=argument_spec,
        supports_check_mode=True,
        required_together=[['login_user', 'login_password']],
    )

    if not pymongo_found:
        module.fail_json(msg=missing_required_lib('pymongo'),
                         exception=PYMONGO_IMP_ERR)

    validate_module(module)

    login_user = module.params['login_user']
    login_password = module.params['login_password']
    login_database = module.params['login_database']
    login_host = module.params['login_host']
    login_port = module.params['login_port']
    ssl = module.params['ssl']
    indexes = module.params['indexes']
    replica_set = module.params['replica_set']

    connection_params = {
        'host': login_host,
        'port': login_port,
    }

    if replica_set:
        connection_params["replicaset"] = replica_set

    if ssl:
        connection_params = ssl_connection_options(connection_params, module)

    client = MongoClient(**connection_params)

    if login_user:
        try:
            client.admin.authenticate(login_user, login_password, source=login_database)
        except Exception as e:
            module.fail_json(msg='Unable to authenticate: %s' % to_native(e))

    # Get server version:
    try:
        srv_version = LooseVersion(client.server_info()['version'])
    except Exception as e:
        module.fail_json(msg='Unable to get MongoDB server version: %s' % to_native(e))

    # Get driver version::
    driver_version = LooseVersion(PyMongoVersion)

    # Check driver and server version compatibility:
    check_compatibility(module, srv_version, driver_version)

    # Pre flight checks done
    indexes_created = []
    indexes_dropped = []
    changed = None
    for i in indexes:
        try:
            idx = index_exists(client, i["database"], i["collection"], i["options"]["name"])
        except Exception as excep:
            module.fail_json(msg="Could not determine index status: {0}".format(str(excep)))
        if module.check_mode:
            if idx:
                if i["state"] == "present":
                    changed = False
                elif i["state"] == "absent":
                    indexes_dropped.append("{0}.{1}.{2}".format(i["database"],
                                                                i["collection"],
                                                                i["options"]["name"]))
                    changed = True
            else:
                if i["state"] == "present":
                    indexes_created.append("{0}.{1}.{2}".format(i["database"],
                                                                i["collection"],
                                                                i["options"]["name"]))
                    changed = True
                elif i["state"] == "absent":
                    changed = False
        else:
            if idx:
                if i["state"] == "present":
                    changed = False
                elif i["state"] == "absent":
                    try:
                        drop_index(client, i["database"], i["collection"],
                                   i["options"]["name"])
                        indexes_dropped.append("{0}.{1}.{2}".format(i["database"],
                                                                    i["collection"],
                                                                    i["options"]["name"]))
                        changed = True
                    except Exception as excep:
                        module.fail_json(msg="Error dropping index: {0}".format(str(excep)))

            else:
                if i["state"] == "present":
                    try:
                        create_index(client=client,
                                     database=i["database"],
                                     collection=i["collection"],
                                     keys=i["keys"],
                                     options=i["options"])
                        indexes_created.append("{0}.{1}.{2}".format(i["database"],
                                                                    i["collection"],
                                                                    i["options"]["name"]))
                        changed = True
                    except Exception as excep:
                        module.fail_json(msg="Error creating index: {0}".format(str(excep)))
                elif i["state"] == "absent":
                    changed = False

    module.exit_json(changed=changed,
                     indexes_created=indexes_created,
                     indexes_dropped=indexes_dropped)


if __name__ == '__main__':
    main()
