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

# Copyright: (c) 2017, Thierry Sallé (@tsalle)
# 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

ANSIBLE_METADATA = {
    'status': ['preview'],
    'supported_by': 'community',
    'metadata_version': '1.1'
}

DOCUMENTATION = '''
---
module: grafana_dashboard
author:
  - Thierry Sallé (@tsalle)
version_added: "2.5"
short_description: Manage Grafana dashboards
description:
  - Create, update, delete, export Grafana dashboards via API.
options:
  grafana_url:
    description:
      - The Grafana URL.
    required: true
  grafana_user:
    description:
      - The Grafana API user.
    default: admin
  grafana_password:
    description:
      - The Grafana API password.
    default: admin
  grafana_api_key:
    description:
      - The Grafana API key.
      - If set, I(grafana_user) and I(grafana_password) will be ignored.
  org_id:
    description:
      - The Grafana Organisation ID where the dashboard will be imported / exported.
      - Not used when I(grafana_api_key) is set, because the grafana_api_key only belong to one organisation..
    default: 1
  state:
    description:
      - State of the dashboard.
    required: true
    choices: [ absent, export, present ]
    default: present
  slug:
    description:
      - slug of the dashboard. It's the friendly url name of the dashboard.
      - When C(state) is C(present), this parameter can override the slug in the meta section of the json file.
      - If you want to import a json dashboard exported directly from the interface (not from the api),
        you have to specify the slug parameter because there is no meta section in the exported json.
  path:
    description:
      - The path to the json file containing the Grafana dashboard to import or export.
  overwrite:
    description:
      - Override existing dashboard when state is present.
    type: bool
    default: 'no'
  message:
    description:
      - Set a commit message for the version history.
      - Only used when C(state) is C(present).
  validate_certs:
    description:
      - If C(no), SSL certificates will not be validated.
      - This should only be used on personally controlled sites using self-signed certificates.
    type: bool
    default: 'yes'
'''

EXAMPLES = '''
- name: Import Grafana dashboard foo
  grafana_dashboard:
    grafana_url: http://grafana.company.com
    grafana_api_key: XXXXXXXXXXXX
    state: present
    message: Updated by ansible
    overwrite: yes
    path: /path/to/dashboards/foo.json

- name: Export dashboard
  grafana_dashboard:
    grafana_url: http://grafana.company.com
    grafana_api_key: XXXXXXXXXXXX
    state: export
    slug: foo
    path: /path/to/dashboards/foo.json
'''

RETURN = '''
---
slug:
  description: slug of the created / deleted / exported dashboard.
  returned: success
  type: string
  sample: foo
'''

import json
import base64

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url
from ansible.module_utils._text import to_bytes

__metaclass__ = type


class GrafanaAPIException(Exception):
    pass


class GrafanaMalformedJson(Exception):
    pass


class GrafanaExportException(Exception):
    pass


def grafana_switch_organisation(module, grafana_url, org_id, headers):
    r, info = fetch_url(module, '%s/api/user/using/%s' % (grafana_url, org_id), headers=headers, method='POST')
    if info['status'] != 200:
        raise GrafanaAPIException('Unable to switch to organization %s : %s' % (org_id, info))


def grafana_dashboard_exists(module, grafana_url, slug, headers):
    dashboard_exists = False
    dashboard = {}
    r, info = fetch_url(module, '%s/api/dashboards/db/%s' % (grafana_url, slug), headers=headers, method='GET')
    if info['status'] == 200:
        dashboard_exists = True
        try:
            dashboard = json.loads(r.read())
        except Exception as e:
            raise GrafanaMalformedJson(e)
    elif info['status'] == 404:
        dashboard_exists = False
    else:
        raise GrafanaAPIException('Unable to get dashboard %s : %s' % (slug, info))

    return dashboard_exists, dashboard


def grafana_create_dashboard(module, data):

    # define data payload for grafana API
    try:
        with open(data['path'], 'r') as json_file:
            payload = json.load(json_file)
    except Exception as e:
        raise GrafanaMalformedJson("Can't load json file %s" % str(e))

    # define http header
    headers = {'content-type': 'application/json; charset=utf8'}
    if 'grafana_api_key' in data and data['grafana_api_key']:
        headers['Authorization'] = "Bearer %s" % data['grafana_api_key']
    else:
        auth = base64.b64encode(to_bytes('%s:%s' % (data['grafana_user'], data['grafana_password'])).replace('\n', ''))
        headers['Authorization'] = 'Basic %s' % auth
        grafana_switch_organisation(module, data['grafana_url'], data['org_id'], headers)

    if data.get('slug'):
        slug = data['slug']
    elif 'meta' in payload and 'slug' in payload['meta']:
        slug = payload['meta']['slug']
    else:
        raise GrafanaMalformedJson('No slug found in json')

    # test if dashboard already exists
    dashboard_exists, dashboard = grafana_dashboard_exists(module, data['grafana_url'], slug, headers=headers)

    result = {}
    if dashboard_exists is True:
        if dashboard == payload:
            # unchanged
            result['slug'] = data['slug']
            result['msg'] = "Dashboard %s unchanged." % data['slug']
            result['changed'] = False
        else:
            # update
            if 'overwrite' in data and data['overwrite']:
                payload['overwrite'] = True
            if 'message' in data and data['message']:
                payload['message'] = data['message']

            r, info = fetch_url(module, '%s/api/dashboards/db' % data['grafana_url'], data=json.dumps(payload), headers=headers, method='POST')
            if info['status'] == 200:
                result['slug'] = slug
                result['msg'] = "Dashboard %s updated" % slug
                result['changed'] = True
            else:
                body = json.loads(info['body'])
                raise GrafanaAPIException('Unable to update the dashboard %s : %s' % (slug, body['message']))
    else:
        # create
        if 'dashboard' not in payload:
            payload = {'dashboard': payload}
        r, info = fetch_url(module, '%s/api/dashboards/db' % data['grafana_url'], data=json.dumps(payload), headers=headers, method='POST')
        if info['status'] == 200:
            result['msg'] = "Dashboard %s created" % slug
            result['changed'] = True
            result['slug'] = slug
        else:
            raise GrafanaAPIException('Unable to create the new dashboard %s : %s - %s.' % (slug, info['status'], info))

    return result


def grafana_delete_dashboard(module, data):

    # define http headers
    headers = {'content-type': 'application/json'}
    if 'grafana_api_key' in data and data['grafana_api_key']:
        headers['Authorization'] = "Bearer %s" % data['grafana_api_key']
    else:
        auth = base64.b64encode(to_bytes('%s:%s' % (data['grafana_user'], data['grafana_password'])).replace('\n', ''))
        headers['Authorization'] = 'Basic %s' % auth
        grafana_switch_organisation(module, data['grafana_url'], data['org_id'], headers)

    # test if dashboard already exists
    dashboard_exists, dashboard = grafana_dashboard_exists(module, data['grafana_url'], data['slug'], headers=headers)

    result = {}
    if dashboard_exists is True:
        # delete
        r, info = fetch_url(module, '%s/api/dashboards/db/%s' % (data['grafana_url'], data['slug']), headers=headers, method='DELETE')
        if info['status'] == 200:
            result['msg'] = "Dashboard %s deleted" % data['slug']
            result['changed'] = True
            result['slug'] = data['slug']
        else:
            raise GrafanaAPIException('Unable to update the dashboard %s : %s' % (data['slug'], info))
    else:
        # dashboard does not exist, do nothing
        result = {'msg': "Dashboard %s does not exist." % data['slug'],
                  'changed': False,
                  'slug': data['slug']}

    return result


def grafana_export_dashboard(module, data):

    # define http headers
    headers = {'content-type': 'application/json'}
    if 'grafana_api_key' in data and data['grafana_api_key']:
        headers['Authorization'] = "Bearer %s" % data['grafana_api_key']
    else:
        auth = base64.b64encode(to_bytes('%s:%s' % (data['grafana_user'], data['grafana_password'])).replace('\n', ''))
        headers['Authorization'] = 'Basic %s' % auth
        grafana_switch_organisation(module, data['grafana_url'], data['org_id'], headers)

    # test if dashboard already exists
    dashboard_exists, dashboard = grafana_dashboard_exists(module, data['grafana_url'], data['slug'], headers=headers)

    if dashboard_exists is True:
        try:
            with open(data['path'], 'w') as f:
                f.write(json.dumps(dashboard))
        except Exception as e:
            raise GrafanaExportException("Can't write json file : %s" % str(e))
        result = {'msg': "Dashboard %s exported to %s" % (data['slug'], data['path']),
                  'slug': data['slug'],
                  'changed': True}
    else:
        result = {'msg': "Dashboard %s does not exist." % data['slug'],
                  'slug': data['slug'],
                  'changed': False}

    return result


def main():
    module = AnsibleModule(
        argument_spec=dict(
            state=dict(choices=['present', 'absent', 'export'],
                       default='present'),
            grafana_url=dict(required=True),
            grafana_user=dict(default='admin'),
            grafana_password=dict(default='admin', no_log=True),
            grafana_api_key=dict(type='str', no_log=True),
            org_id=dict(default=1, type='int'),
            slug=dict(type='str'),
            path=dict(type='str'),
            overwrite=dict(type='bool', default=False),
            message=dict(type='str'),
            validate_certs=dict(type='bool', default=True)
        ),
        supports_check_mode=False,
        required_together=[['grafana_user', 'grafana_password', 'org_id']],
        mutually_exclusive=[['grafana_user', 'grafana_api_key']],
    )

    try:
        if module.params['state'] == 'present':
            result = grafana_create_dashboard(module, module.params)
        elif module.params['state'] == 'absent':
            result = grafana_delete_dashboard(module, module.params)
        else:
            result = grafana_export_dashboard(module, module.params)
    except GrafanaAPIException as e:
        module.fail_json(
            failed=True,
            msg="error : %s" % e
        )
        return
    except GrafanaMalformedJson as e:
        module.fail_json(
            failed=True,
            msg="error : json file does not contain a meta section with a slug parameter, or you did'nt specify the slug parameter"
        )
        return
    except GrafanaExportException as e:
        module.fail_json(
            failed=True,
            msg="error : json file cannot be written : %s" % str(e)
        )
        return

    module.exit_json(
        failed=False,
        **result
    )
    return

if __name__ == '__main__':
    main()
