text: # -*- coding: utf-8 -*-
  
  """
  StatusPage
  ==========
  
  Manage the StatusPage_ configuration.
  
  .. _StatusPage: https://www.statuspage.io/
  
  In the minion configuration file, the following block is required:
  
  .. code-block:: yaml
  
    statuspage:
      api_key: <API_KEY>
      page_id: <PAGE_ID>
  
  .. versionadded:: 2017.7.0
  """
  from __future__ import absolute_import, print_function, unicode_literals
  
  import logging
  
  # import python std lib
  import time
  
  # import salt
  from salt.ext import six
  
  # ----------------------------------------------------------------------------------------------------------------------
  # module properties
  # ----------------------------------------------------------------------------------------------------------------------
  
  __virtualname__ = "statuspage"
  
  log = logging.getLogger(__file__)
  
  _DO_NOT_COMPARE_FIELDS = ["created_at", "updated_at"]
  
  _MATCH_KEYS = ["id", "name"]
  
  _PACE = 1  # 1 request per second
  
  # ----------------------------------------------------------------------------------------------------------------------
  # property functions
  # ----------------------------------------------------------------------------------------------------------------------
  
  
  def __virtual__():
      """
      Return the execution module virtualname.
      """
      if "statuspage.create" in __salt__:
          return True
      return (False, "statuspage module could not be loaded")
  
  
  def _default_ret(name):
      """
      Default dictionary returned.
      """
      return {"name": name, "result": False, "comment": "", "changes": {}}
  
  
  def _compute_diff_ret():
      """
      Default dictionary retuned by the _compute_diff helper.
      """
      return {"add": [], "update": [], "remove": []}
  
  
  def _clear_dict(endpoint_props):
      """
      Eliminates None entries from the features of the endpoint dict.
      """
      return dict(
          (prop_name, prop_val)
          for prop_name, prop_val in six.iteritems(endpoint_props)
          if prop_val is not None
      )
  
  
  def _ignore_keys(endpoint_props):
      """
      Ignores some keys that might be different without any important info.
      These keys are defined under _DO_NOT_COMPARE_FIELDS.
      """
      return dict(
          (prop_name, prop_val)
          for prop_name, prop_val in six.iteritems(endpoint_props)
          if prop_name not in _DO_NOT_COMPARE_FIELDS
      )
  
  
  def _unique(list_of_dicts):
      """
      Returns an unique list of dictionaries given a list that may contain duplicates.
      """
      unique_list = []
      for ele in list_of_dicts:
          if ele not in unique_list:
              unique_list.append(ele)
      return unique_list
  
  
  def _clear_ignore(endpoint_props):
      """
      Both _clear_dict and _ignore_keys in a single iteration.
      """
      return dict(
          (prop_name, prop_val)
          for prop_name, prop_val in six.iteritems(endpoint_props)
          if prop_name not in _DO_NOT_COMPARE_FIELDS and prop_val is not None
      )
  
  
  def _clear_ignore_list(lst):
      """
      Apply _clear_ignore to a list.
      """
      return _unique([_clear_ignore(ele) for ele in lst])
  
  
  def _find_match(ele, lst):
      """
      Find a matching element in a list.
      """
      for _ele in lst:
          for match_key in _MATCH_KEYS:
              if _ele.get(match_key) == ele.get(match_key):
                  return ele
  
  
  def _update_on_fields(prev_ele, new_ele):
      """
      Return a dict with fields that differ between two dicts.
      """
      fields_update = dict(
          (prop_name, prop_val)
          for prop_name, prop_val in six.iteritems(new_ele)
          if new_ele.get(prop_name) != prev_ele.get(prop_name) or prop_name in _MATCH_KEYS
      )
      if len(set(fields_update.keys()) | set(_MATCH_KEYS)) > len(set(_MATCH_KEYS)):
          if "id" not in fields_update:
              # in case of update, the ID is necessary
              # if not specified in the pillar,
              # will try to get it from the prev_ele
              fields_update["id"] = prev_ele["id"]
          return fields_update
  
  
  def _compute_diff(expected_endpoints, configured_endpoints):
      """
      Compares configured endpoints with the expected configuration and returns the differences.
      """
      new_endpoints = []
      update_endpoints = []
      remove_endpoints = []
  
      ret = _compute_diff_ret()
  
      # noth configured => configure with expected endpoints
      if not configured_endpoints:
          ret.update({"add": expected_endpoints})
          return ret
  
      # noting expected => remove everything
      if not expected_endpoints:
          ret.update({"remove": configured_endpoints})
          return ret
  
      expected_endpoints_clear = _clear_ignore_list(expected_endpoints)
      configured_endpoints_clear = _clear_ignore_list(configured_endpoints)
  
      for expected_endpoint_clear in expected_endpoints_clear:
          if expected_endpoint_clear not in configured_endpoints_clear:
              # none equal => add or update
              matching_ele = _find_match(
                  expected_endpoint_clear, configured_endpoints_clear
              )
              if not matching_ele:
                  # new element => add
                  new_endpoints.append(expected_endpoint_clear)
              else:
                  # element matched, but some fields are different
                  update_fields = _update_on_fields(matching_ele, expected_endpoint_clear)
                  if update_fields:
                      update_endpoints.append(update_fields)
      for configured_endpoint_clear in configured_endpoints_clear:
          if configured_endpoint_clear not in expected_endpoints_clear:
              matching_ele = _find_match(
                  configured_endpoint_clear, expected_endpoints_clear
              )
              if not matching_ele:
                  #  no match found => remove
                  remove_endpoints.append(configured_endpoint_clear)
  
      return {
          "add": new_endpoints,
          "update": update_endpoints,
          "remove": remove_endpoints,
      }
  
  
  # ----------------------------------------------------------------------------------------------------------------------
  # callable functions
  # ----------------------------------------------------------------------------------------------------------------------
  
  
  def create(
      name,
      endpoint="incidents",
      api_url=None,
      page_id=None,
      api_key=None,
      api_version=None,
      **kwargs
  ):
      """
      Insert a new entry under a specific endpoint.
  
      endpoint: incidents
          Insert under this specific endpoint.
  
      page_id
          Page ID. Can also be specified in the config file.
  
      api_key
          API key. Can also be specified in the config file.
  
      api_version: 1
          API version. Can also be specified in the config file.
  
      api_url
          Custom API URL in case the user has a StatusPage service running in a custom environment.
  
      kwargs
          Other params.
  
      SLS Example:
  
      .. code-block:: yaml
  
          create-my-component:
              statuspage.create:
                  - endpoint: components
                  - name: my component
                  - group_id: 993vgplshj12
      """
      ret = _default_ret(name)
      endpoint_sg = endpoint[:-1]  # singular
      if __opts__["test"]:
          ret["comment"] = "The following {endpoint} would be created:".format(
              endpoint=endpoint_sg
          )
          ret["result"] = None
          ret["changes"][endpoint] = {}
          for karg, warg in six.iteritems(kwargs):
              if warg is None or karg.startswith("__"):
                  continue
              ret["changes"][endpoint][karg] = warg
          return ret
      sp_create = __salt__["statuspage.create"](
          endpoint=endpoint,
          api_url=api_url,
          page_id=page_id,
          api_key=api_key,
          api_version=api_version,
          **kwargs
      )
      if not sp_create.get("result"):
          ret["comment"] = "Unable to create {endpoint}: {msg}".format(
              endpoint=endpoint_sg, msg=sp_create.get("comment")
          )
      else:
          ret["comment"] = "{endpoint} created!".format(endpoint=endpoint_sg)
          ret["result"] = True
          ret["changes"] = sp_create.get("out")
  
  
  def update(
      name,
      endpoint="incidents",
      id=None,
      api_url=None,
      page_id=None,
      api_key=None,
      api_version=None,
      **kwargs
  ):
      """
      Update attribute(s) of a specific endpoint.
  
      id
          The unique ID of the enpoint entry.
  
      endpoint: incidents
          Endpoint name.
  
      page_id
          Page ID. Can also be specified in the config file.
  
      api_key
          API key. Can also be specified in the config file.
  
      api_version: 1
          API version. Can also be specified in the config file.
  
      api_url
          Custom API URL in case the user has a StatusPage service running in a custom environment.
  
      SLS Example:
  
      .. code-block:: yaml
  
          update-my-incident:
              statuspage.update:
                  - id: dz959yz2nd4l
                  - status: resolved
      """
      ret = _default_ret(name)
      endpoint_sg = endpoint[:-1]  # singular
      if not id:
          log.error("Invalid %s ID", endpoint_sg)
          ret["comment"] = "Please specify a valid {endpoint} ID".format(
              endpoint=endpoint_sg
          )
          return ret
      if __opts__["test"]:
          ret["comment"] = "{endpoint} #{id} would be updated:".format(
              endpoint=endpoint_sg, id=id
          )
          ret["result"] = None
          ret["changes"][endpoint] = {}
          for karg, warg in six.iteritems(kwargs):
              if warg is None or karg.startswith("__"):
                  continue
              ret["changes"][endpoint][karg] = warg
          return ret
      sp_update = __salt__["statuspage.update"](
          endpoint=endpoint,
          id=id,
          api_url=api_url,
          page_id=page_id,
          api_key=api_key,
          api_version=api_version,
          **kwargs
      )
      if not sp_update.get("result"):
          ret["comment"] = "Unable to update {endpoint} #{id}: {msg}".format(
              endpoint=endpoint_sg, id=id, msg=sp_update.get("comment")
          )
      else:
          ret["comment"] = "{endpoint} #{id} updated!".format(endpoint=endpoint_sg, id=id)
          ret["result"] = True
          ret["changes"] = sp_update.get("out")
  
  
  def delete(
      name,
      endpoint="incidents",
      id=None,
      api_url=None,
      page_id=None,
      api_key=None,
      api_version=None,
  ):
      """
      Remove an entry from an endpoint.
  
      endpoint: incidents
          Request a specific endpoint.
  
      page_id
          Page ID. Can also be specified in the config file.
  
      api_key
          API key. Can also be specified in the config file.
  
      api_version: 1
          API version. Can also be specified in the config file.
  
      api_url
          Custom API URL in case the user has a StatusPage service running in a custom environment.
  
      SLS Example:
  
      .. code-block:: yaml
  
          delete-my-component:
              statuspage.delete:
                  - endpoint: components
                  - id: ftgks51sfs2d
      """
      ret = _default_ret(name)
      endpoint_sg = endpoint[:-1]  # singular
      if not id:
          log.error("Invalid %s ID", endpoint_sg)
          ret["comment"] = "Please specify a valid {endpoint} ID".format(
              endpoint=endpoint_sg
          )
          return ret
      if __opts__["test"]:
          ret["comment"] = "{endpoint} #{id} would be removed!".format(
              endpoint=endpoint_sg, id=id
          )
          ret["result"] = None
      sp_delete = __salt__["statuspage.delete"](
          endpoint=endpoint,
          id=id,
          api_url=api_url,
          page_id=page_id,
          api_key=api_key,
          api_version=api_version,
      )
      if not sp_delete.get("result"):
          ret["comment"] = "Unable to delete {endpoint} #{id}: {msg}".format(
              endpoint=endpoint_sg, id=id, msg=sp_delete.get("comment")
          )
      else:
          ret["comment"] = "{endpoint} #{id} deleted!".format(endpoint=endpoint_sg, id=id)
          ret["result"] = True
  
  
  def managed(
      name,
      config,
      api_url=None,
      page_id=None,
      api_key=None,
      api_version=None,
      pace=_PACE,
      allow_empty=False,
  ):
      """
      Manage the StatusPage configuration.
  
      config
          Dictionary with the expected configuration of the StatusPage.
          The main level keys of this dictionary represent the endpoint name.
          If a certain endpoint does not exist in this structure, it will be ignored / not configured.
  
      page_id
          Page ID. Can also be specified in the config file.
  
      api_key
          API key. Can also be specified in the config file.
  
      api_version: 1
          API version. Can also be specified in the config file.
  
      api_url
          Custom API URL in case the user has a StatusPage service running in a custom environment.
  
      pace: 1
          Max requests per second allowed by the API.
  
      allow_empty: False
          Allow empty config.
  
      SLS example:
  
      .. code-block:: yaml
  
          my-statuspage-config:
              statuspage.managed:
                  - config:
                      components:
                          - name: component1
                            group_id: uy4g37rf
                          - name: component2
                            group_id: 3n4uyu4gf
                      incidents:
                          - name: incident1
                            status: resolved
                            impact: major
                            backfilled: false
                          - name: incident2
                            status: investigating
                            impact: minor
      """
      complete_diff = {}
      ret = _default_ret(name)
      if not config and not allow_empty:
          ret.update(
              {
                  "result": False,
                  "comment": "Cannot remove everything. To allow this, please set the option `allow_empty` as True.",
              }
          )
          return ret
      is_empty = True
      for endpoint_name, endpoint_expected_config in six.iteritems(config):
          if endpoint_expected_config:
              is_empty = False
          endpoint_existing_config_ret = __salt__["statuspage.retrieve"](
              endpoint=endpoint_name,
              api_url=api_url,
              page_id=page_id,
              api_key=api_key,
              api_version=api_version,
          )
          if not endpoint_existing_config_ret.get("result"):
              ret.update({"comment": endpoint_existing_config_ret.get("comment")})
              return ret  # stop at first error
          endpoint_existing_config = endpoint_existing_config_ret.get("out")
          complete_diff[endpoint_name] = _compute_diff(
              endpoint_expected_config, endpoint_existing_config
          )
      if is_empty and not allow_empty:
          ret.update(
              {
                  "result": False,
                  "comment": "Cannot remove everything. To allow this, please set the option `allow_empty` as True.",
              }
          )
          return ret
      any_changes = False
      for endpoint_name, endpoint_diff in six.iteritems(complete_diff):
          if (
              endpoint_diff.get("add")
              or endpoint_diff.get("update")
              or endpoint_diff.get("remove")
          ):
              any_changes = True
      if not any_changes:
          ret.update({"result": True, "comment": "No changes required.", "changes": {}})
          return ret
      ret.update({"changes": complete_diff})
      if __opts__.get("test"):
          ret.update(
              {
                  "comment": "Testing mode. Would apply the following changes:",
                  "result": None,
              }
          )
          return ret
      for endpoint_name, endpoint_diff in six.iteritems(complete_diff):
          endpoint_sg = endpoint_name[:-1]  # singular
          for new_endpoint in endpoint_diff.get("add"):
              log.debug("Defining new %s %s", endpoint_sg, new_endpoint)
              adding = __salt__["statuspage.create"](
                  endpoint=endpoint_name,
                  api_url=api_url,
                  page_id=page_id,
                  api_key=api_key,
                  api_version=api_version,
                  **new_endpoint
              )
              if not adding.get("result"):
                  ret.update({"comment": adding.get("comment")})
                  return ret
              if pace:
                  time.sleep(1 / pace)
          for update_endpoint in endpoint_diff.get("update"):
              if "id" not in update_endpoint:
                  continue
              endpoint_id = update_endpoint.pop("id")
              log.debug("Updating %s #%s: %s", endpoint_sg, endpoint_id, update_endpoint)
              updating = __salt__["statuspage.update"](
                  endpoint=endpoint_name,
                  id=endpoint_id,
                  api_url=api_url,
                  page_id=page_id,
                  api_key=api_key,
                  api_version=api_version,
                  **update_endpoint
              )
              if not updating.get("result"):
                  ret.update({"comment": updating.get("comment")})
                  return ret
              if pace:
                  time.sleep(1 / pace)
          for remove_endpoint in endpoint_diff.get("remove"):
              if "id" not in remove_endpoint:
                  continue
              endpoint_id = remove_endpoint.pop("id")
              log.debug("Removing %s #%s", endpoint_sg, endpoint_id)
              removing = __salt__["statuspage.delete"](
                  endpoint=endpoint_name,
                  id=endpoint_id,
                  api_url=api_url,
                  page_id=page_id,
                  api_key=api_key,
                  api_version=api_version,
              )
              if not removing.get("result"):
                  ret.update({"comment": removing.get("comment")})
                  return ret
              if pace:
                  time.sleep(1 / pace)
      ret.update({"result": True, "comment": "StatusPage updated."})
      return ret
  