{#- -*- coding: utf-8 -*- #}
{#- vim: ft=jinja #}

{#- Get the `tplroot` from `tpldir` #}
{%- set tplroot = tpldir.split("/")[0] %}
{%- from tplroot ~ "/libmatchers.jinja" import parse_matchers, query_map with context %}

{%- set _default_config_dirs = [
      "parameters/",
      tplroot ~ "/parameters"
    ] %}

{%- macro mapstack(
      matchers,
      defaults=None,
      dirs=_default_config_dirs,
      log_prefix="libmapstack: "
    ) %}
{#-
    Load configuration in the order of `matchers` and merge
    successively the values with `defaults`.

    The `matchers` are processed using `libmatchers.jinja` to select
    the configuration sources from where the values are loaded.

    Parameters:

      - `matchers`: list of matchers in the form
        `[<TYPE>[:<OPTION>[:<DELIMITER>]]@]<QUERY>`

      - `defaults`: dictionary of default values to start the merging,
        they are considered built-ins. It must conform to the same
        layout as the YAML files: a mandatory `values` key and two
        optional `strategy` and `merge_lists` keys.

      - `dirs`: list of directory where to look-up the configuration
        file matching the matchers, by default a global `salt://parameters/`
        and a per formula `salt://<tplroot>/parameters`

      - `log_prefix`: prefix used in the log outputs, by default it is
        `libmapstack: `

    Example: On a Debian system with `roles=["nginx/server", "telegraf"]`

      {%- set settings = mapstack(
            matchers=[
              "Y:G@os_family",
              "I@" ~ tplroot,
              "Y:C@roles",
            ],
            dirs=["defaults", tplroot ~ "/parameters"],
          )
          | load_yaml %}

      This will merge the values:

        - starting with the default empty dictionary `{}` (no
          `defaults` parameter)

        - from the YAML files

          - `salt://defaults/os_family/Debian.yaml`

          - `salt://{{ tplroot }}/parameters/os_family/Debian.yaml`

        - from the pillar `salt["pillar.get"](tplroot)`

        - from the `nginx/server` YAML files:

          - `salt://defaults/roles/nginx/server.yaml`

          - `salt://{{ tplroot }}/parameters/roles/nginx/server.yaml`

        - from the `telegraf` YAML files:

          - `salt://defaults/roles/telegraf.yaml`

          - `salt://{{ tplroot }}/parameters/roles/telegraf.yaml`

    Each YAML file and the `defaults` parameters must conform to the
    following layout:

      - a mandatory `values` key to store the configuration values

      - two optional keys to configure the use of `salt.slsutil.merge`

        - an optional `strategy` key to configure the merging
          strategy, for example `strategy: 'recurse'`, the default is
          `smart`

        - an optional `merge_lists` key to configure if lists should
          be merged or overridden for the `recurse` and `overwrite`
          strategies, for example `merge_lists: 'true'`
#}
{%-   set stack = defaults | default({"values": {} }, boolean=True) %}

{#-   Build configuration file names based on matchers #}
{%-   set config_get_strategy = salt["config.get"](tplroot ~ ":strategy", None) %}
{%-   set matchers = parse_matchers(
        matchers,
        config_get_strategy=config_get_strategy,
        log_prefix=log_prefix
      )
      | load_yaml %}

{%-   do salt["log.debug"](
        log_prefix
        ~ "built-in configuration:\n"
        ~ {"values": defaults | traverse("values")}
        | yaml(False)
      ) %}

{%-   for param_dir in dirs %}
{%-     for matcher in matchers %}
{#-       `slsutil.merge` options from #}
{#-       1. the `value` #}
{#-       2. the `defaults` #}
{#-       3. the built-in #}
{%-       set strategy = matcher.value
          | traverse(
            "strategy",
            defaults
            | traverse(
              "strategy",
              "smart"
            )
          ) %}
{%-       set merge_lists = matcher.value
          | traverse(
            "merge_lists",
            defaults
            | traverse(
              "merge_lists",
              False
            )
          )
          | to_bool %}

{%-       if matcher.type in query_map.keys() %}
{#-         No value is an empty list, must be a dict for `stack.update` #}
{%-         set normalized_value = matcher.value | default({}, boolean=True) %}

{#-         Merge in `mapdata.<query>` instead of directly in `mapdata` #}
{%-         set is_sub_key = matcher.option | default(False) == "SUB" %}
{%-         if is_sub_key %}
{#-           Merge values with `mapdata.<key>`, `<key>` and `<key>:lookup` are merged together #}
{%-           set value = { matcher.query | regex_replace(":lookup$", ""): normalized_value } %}
{%-         else %}
{%-           set value = normalized_value %}
{%-         endif %}

{%-         do salt["log.debug"](
              log_prefix
              ~ "merge "
              ~ "sub key " * is_sub_key
              ~ "'"
              ~ matcher.query
              ~ "' retrieved with '"
              ~ matcher.query_method
              ~ "', merge: strategy='"
              ~ strategy
              ~ "', lists='"
              ~ merge_lists
              ~ "':\n"
              ~ value
              | yaml(False)
            ) %}

{%-         do stack.update(
              {
                "values": salt["slsutil.merge"](
                  stack["values"],
                  value,
                  strategy=strategy,
                  merge_lists=merge_lists,
                )
              }
            ) %}

{%-       else %}
{#-         Load YAML file matching the grain/pillar/... #}
{#-         Fallback to use the source name as a direct filename #}

{%-         if matcher.value is sequence and matcher.value | length == 0 %}
{#-           Mangle `matcher.value` to use it as literal path #}
{%-           set query_parts = matcher.query.split("/") %}
{%-           set yaml_dirname = query_parts[0:-1] | join("/") %}
{%-           set yaml_names = query_parts[-1] %}
{%-         else %}
{%-           set yaml_dirname = matcher.query %}
{%-           set yaml_names = matcher.value %}
{%-         endif %}

{#-         Some configuration return list #}
{%-         if yaml_names is string %}
{%-           set yaml_names = [yaml_names] %}
{%-         elif yaml_names is sequence %}
{#-           Convert to strings if it's a sequence of numbers #}
{%-           set yaml_names = yaml_names | map("string") | list %}
{%-         else %}
{%-           set yaml_names = [yaml_names | string] %}
{%-         endif %}

{#-         Try to load a `.yaml.jinja` file for each `.yaml` file #}
{%-         set all_yaml_names = [] %}
{%-         for name in yaml_names %}
{%-           set extension = name.rpartition(".")[2] %}
{%-           if extension not in ["yaml", "jinja"] %}
{%-             do all_yaml_names.extend([name ~ ".yaml", name ~ ".yaml.jinja"]) %}
{%-           elif extension == "yaml" %}
{%-             do all_yaml_names.extend([name, name ~ ".jinja"]) %}
{%-           else %}
{%-             do all_yaml_names.append(name) %}
{%-           endif %}
{%-         endfor %}

{#-         `yaml_dirname` can be an empty string with literal path like `myconf.yaml` #}
{%-         set yaml_dir = [
              param_dir,
              yaml_dirname
            ]
            | select
            | join("/") %}

{%-         for yaml_name in all_yaml_names %}
{%-           set yaml_filename = [
                yaml_dir.rstrip("/"),
                yaml_name
              ]
              | select
              | join("/") %}

{%-           do salt["log.debug"](
                log_prefix
                ~ "load configuration values from "
                ~ yaml_filename
              ) %}
{%-           load_yaml as yaml_values %}
{%-             include yaml_filename ignore missing %}
{%-           endload %}

{%-           if yaml_values %}
{%-             do salt["log.debug"](
                  log_prefix
                  ~ "loaded configuration values from "
                  ~ yaml_filename
                  ~ ":\n"
                  ~ yaml_values
                  | yaml(False)
                ) %}

{#-             `slsutil.merge` options from #}
{#-             1. the `value` #}
{#-             2. the `defaults` #}
{#-             3. the built-in #}
{%-             set strategy = yaml_values
                  | traverse(
                    "strategy",
                    defaults
                    | traverse(
                      "strategy",
                      "smart"
                    )
                  ) %}
{%-             set merge_lists = yaml_values
                  | traverse(
                    "merge_lists",
                    defaults
                    | traverse(
                      "merge_lists",
                      False
                    )
                  )
                  | to_bool %}
{%-             do stack.update(
                  {
                    "values": salt["slsutil.merge"](
                      stack["values"],
                      yaml_values
                      | traverse("values", {}),
                      strategy=strategy,
                      merge_lists=merge_lists,
                    )
                  }
                ) %}
{%-             do salt["log.debug"](
                  log_prefix
                  ~ "merged configuration values from "
                  ~ yaml_filename
                  ~ ", merge: strategy='"
                  ~ strategy
                  ~ "', merge_lists='"
                  ~ merge_lists
                  ~ "':\n"
                  ~ {"values": stack["values"]}
                  | yaml(False)
                ) %}
{%-           endif %}
{%-         endfor %}
{%-       endif %}
{%-     endfor %}
{%-   endfor %}

{%-   do salt["log.debug"](
        log_prefix
        ~ "final configuration values:\n"
        ~ {"values": stack["values"]}
        | yaml(False)
      ) %}

{#- Output stack as YAML, caller should use with something like #}
{#- `{%- set config = mapstack(matchers=["foo"]) | load_yaml %}` #}
{{ stack | yaml }}

{%- endmacro %}