#!/bin/bash

# Create a dedicated config file from a template
#
# usage: ynh_config_add --template="template" --destination="destination"
# | arg: -t, --template=     - Template config file to use
# | arg: -d, --destination=  - Destination of the config file
# | arg: -j, --jinja         - Use jinja template instead of legacy __MY_VAR__
#
# examples:
#    ynh_config_add --template=".env" --destination="$install_dir/.env" use the template file "../conf/.env"
#    ynh_config_add --jinja --template="config.j2" --destination="$install_dir/config" use the template file "../conf/config.j2"
#    ynh_config_add --template="/etc/nginx/sites-available/default" --destination="etc/nginx/sites-available/mydomain.conf"
#
##
## How it works in "legacy" mode
##
# The template can be by default the name of a file in the conf directory
# of a YunoHost Package, a relative path or an absolute path.
#
# The helper will use the template `template` to generate a config file
# `destination` by replacing the following keywords with global variables
# that should be defined before calling this helper :
# ```
#   __USER__                by $app
#   __YNH_NODE_LOAD_PATH__  by $ynh_node_load_PATH
# ```
# And any dynamic variables that should be defined before calling this helper like:
# ```
#   __DOMAIN__   by $domain
#   __PATH__     by $path
#   __APP__      by $app
#   __VAR_1__    by $var_1
#   __VAR_2__    by $var_2
# ```
#
##
## When --jinja is enabled
##
# For a full documentation of the template you can refer to: https://jinja.palletsprojects.com/en/3.1.x/templates/
# In Yunohost context there are no really some specificity except that all variable passed are of type string.
# So here are some example of recommended usage:
#
# If you need a conditional block
#
# {% if should_my_block_be_shown == 'true' %}
# ...
# {% endif %}
#
# or
#
# {% if should_my_block_be_shown == '1' %}
# ...
# {% endif %}
#
# If you need to iterate with loop:
#
# {% for yolo in var_with_multiline_value.splitlines() %}
# ...
# {% endfor %}
#
# or
#
# {% for jail in my_var_with_coma.split(',') %}
# ...
# {% endfor %}
#
# The helper will verify the checksum and backup the destination file
# if it's different before applying the new template.
#
# And it will calculate and store the destination file checksum
# into the app settings when configuration is done.
#
# Requires YunoHost version 4.1.0 or higher.
ynh_config_add() {
    # ============ Argument parsing =============
    local -A args_array=([t]=template= [d]=destination= [j]=jinja)
    local template
    local destination
    local jinja
    ynh_handle_getopts_args "$@"
    jinja="${jinja:-0}"
    # ===========================================

    local template_path
    if [ -f "$YNH_APP_BASEDIR/conf/$template" ]; then
        template_path="$YNH_APP_BASEDIR/conf/$template"
    elif [ -f "$template" ]; then
        template_path=$template
    else
        ynh_die "The provided template $template doesn't exist"
    fi

    ynh_backup_if_checksum_is_different --file="$destination"

    # Make sure to set the permissions before we copy the file
    # This is to cover a case where an attacker could have
    # created a file beforehand to have control over it
    # (cp won't overwrite ownership / modes by default...)
    touch $destination
    chmod 640 $destination
    _ynh_apply_default_permissions $destination

    if [[ "$jinja" == 1 ]]
    then
        # This is ran in a subshell such that the "export" does not "contaminate" the main process
        (
            export $(compgen -v)
            j2 "$template_path" -f env -o $destination
        )
    else
        cp -f "$template_path" "$destination"
        ynh_replace_vars --file="$destination"
    fi

    ynh_store_file_checksum --file="$destination"
}

# Replace variables in a file
#
# [internal]
#
# usage: ynh_replace_vars --file="file"
# | arg: -f, --file=     - File where to replace variables
#
# The helper will replace the following keywords with global variables
# that should be defined before calling this helper :
#   __PATH__                by $path
#   __PATH__/               by $path/ if $path != /, or just / otherwise (instead of //)
#   __USER__                by $app
#   __YNH_NODE_LOAD_PATH__  by $ynh_node_load_PATH
#
# And any dynamic variables that should be defined before calling this helper like:
#   __DOMAIN__   by $domain
#   __APP__      by $app
#   __VAR_1__    by $var_1
#   __VAR_2__    by $var_2
#
# Requires YunoHost version 4.1.0 or higher.
ynh_replace_vars() {
    # ============ Argument parsing =============
    local -A args_array=([f]=file=)
    local file
    ynh_handle_getopts_args "$@"
    # ===========================================

    # Replace specific YunoHost variables
    if test -n "${path:-}"; then
        # path_slash_less is path, or a blank value if path is only '/'
        local path_slash_less=${path%/}
        ynh_replace --match="__PATH__/" --replace="$path_slash_less/" --file="$file"
        ynh_replace --match="__PATH__" --replace="$path" --file="$file"
    fi
    if test -n "${app:-}"; then
        ynh_replace --match="__USER__" --replace="$app" --file="$file"
    fi
    if test -n "${ynh_node_load_PATH:-}"; then
        ynh_replace --match="__YNH_NODE_LOAD_PATH__" --replace="$ynh_node_load_PATH" --file="$file"
    fi

    # Replace others variables

    # List other unique (__ __) variables in $file
    local uniques_vars=($(grep -oP '__[A-Z0-9]+?[A-Z0-9_]*?[A-Z0-9]*?__' $file | sort --unique | sed "s@__\([^.]*\)__@\L\1@g"))

    set +o xtrace # set +x

    # Do the replacement
    local delimit=@
    for one_var in "${uniques_vars[@]}"; do
        # Validate that one_var is indeed defined
        # -v checks if the variable is defined, for example:
        #     -v FOO  tests if $FOO is defined
        #     -v $FOO tests if ${!FOO} is defined
        # More info: https://stackoverflow.com/questions/3601515/how-to-check-if-a-variable-is-set-in-bash/17538964#comment96392525_17538964
        [[ -v "${one_var:-}" ]] || ynh_die "Variable \$$one_var wasn't initialized when trying to replace __${one_var^^}__ in $file"

        # Escape delimiter in match/replace string
        match_string="__${one_var^^}__"
        match_string=${match_string//${delimit}/"\\${delimit}"}
        replace_string="${!one_var}"
        replace_string=${replace_string//\\/\\\\}
        replace_string=${replace_string//${delimit}/"\\${delimit}"}

        # Actually replace (sed is used instead of ynh_replace_string to avoid triggering an epic amount of debug logs)
        sed --in-place "s${delimit}${match_string}${delimit}${replace_string}${delimit}g" "$file"
    done
    set -o xtrace # set -x
}

# Get a value from heterogeneous file (yaml, json, php, python...)
#
# usage: ynh_read_var_in_file --file=PATH --key=KEY
# | arg: -f, --file=     - the path to the file
# | arg: -k, --key=     - the key to get
# | arg: -a, --after=     - the line just before the key (in case of multiple lines with the name of the key in the file)
#
# This helpers match several var affectation use case in several languages
# We don't use jq or equivalent to keep comments and blank space in files
# This helpers work line by line, it is not able to work correctly
# if you have several identical keys in your files
#
# Example of line this helpers can managed correctly
# .yml
#     title: YunoHost documentation
#     email: 'yunohost@yunohost.org'
# .json
#     "theme": "colib'ris",
#     "port": 8102
#     "some_boolean":     false,
#     "user": null
# .ini
#     some_boolean = On
#     action = "Clear"
#     port = 20
# .php
#     $user=
#     user => 20
# .py
#     USER = 8102
#     user = 'https://donate.local'
#     CUSTOM['user'] = 'YunoHost'
#
# Requires YunoHost version 4.3 or higher.
ynh_read_var_in_file() {
    # ============ Argument parsing =============
    local -A args_array=([f]=file= [k]=key= [a]=after=)
    local file
    local key
    local after
    ynh_handle_getopts_args "$@"
    after="${after:-}"
    # ===========================================

    [[ -f $file ]] || ynh_die "File $file does not exists"

    set +o xtrace # set +x

    # Get the line number after which we search for the variable
    local line_number=1
    if [[ -n "$after" ]]; then
        line_number=$(grep -m1 -n $after $file | cut -d: -f1)
        if [[ -z "$line_number" ]]; then
            set -o xtrace # set -x
            return 1
        fi
    fi

    local filename="$(basename -- "$file")"
    local ext="${filename##*.}"
    local endline=',;'
    local assign="=>|:|="
    local comments="#"
    local string="\"'"
    if [[ "$ext" =~ ^ini|env|toml|yml|yaml$ ]]; then
        endline='#'
    fi
    if [[ "$ext" =~ ^ini|env$ ]]; then
        comments="[;#]"
    fi
    if [[ "php" == "$ext" ]] || [[ "$ext" == "js" ]]; then
        comments="//"
    fi
    local list='\[\s*['$string']?\w+['$string']?\]'
    local var_part='^\s*((const|var|let)\s+)?\$?(\w+('$list')*(->|\.|\[))*\s*'
    var_part+="[$string]?${key}[$string]?"
    var_part+='\s*\]?\s*'
    var_part+="($assign)"
    var_part+='\s*'

    # Extract the part after assignation sign
    local expression_with_comment="$((tail +$line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL) | head -n1)"
    if [[ "$expression_with_comment" == "YNH_NULL" ]]; then
        set -o xtrace # set -x
        echo YNH_NULL
        return 0
    fi

    # Remove comments if needed
    local expression="$(echo "$expression_with_comment" | sed "s@${comments}[^$string]*\$@@g" | sed "s@\s*[$endline]*\s*]*\$@@")"

    local first_char="${expression:0:1}"
    if [[ "$first_char" == '"' ]]; then
        echo "$expression" | grep -m1 -o -P '"\K([^"](\\")?)*[^\\](?=")' | head -n1 | sed 's/\\"/"/g'
    elif [[ "$first_char" == "'" ]]; then
        echo "$expression" | grep -m1 -o -P "'\K([^'](\\\\')?)*[^\\\\](?=')" | head -n1 | sed "s/\\\\'/'/g"
    else
        echo "$expression"
    fi
    set -o xtrace # set -x
}

# Set a value into heterogeneous file (yaml, json, php, python...)
#
# usage: ynh_write_var_in_file --file=PATH --key=KEY --value=VALUE
# | arg: -f, --file=     - the path to the file
# | arg: -k, --key=     - the key to set
# | arg: -v, --value=     - the value to set
# | arg: -a, --after=     - the line just before the key (in case of multiple lines with the name of the key in the file)
#
# Requires YunoHost version 4.3 or higher.
ynh_write_var_in_file() {
    # ============ Argument parsing =============
    local -A args_array=([f]=file= [k]=key= [v]=value= [a]=after=)
    local file
    local key
    local value
    local after
    ynh_handle_getopts_args "$@"
    after="${after:-}"
    # ===========================================

    [[ -f $file ]] || ynh_die "File $file does not exists"

    set +o xtrace # set +x

    # Get the line number after which we search for the variable
    local after_line_number=1
    if [[ -n "$after" ]]; then
        after_line_number=$(grep -m1 -n $after $file | cut -d: -f1)
        if [[ -z "$after_line_number" ]]; then
            set -o xtrace # set -x
            return 1
        fi
    fi

    local filename="$(basename -- "$file")"
    local ext="${filename##*.}"
    local endline=',;'
    local assign="=>|:|="
    local comments="#"
    local string="\"'"
    if [[ "$ext" =~ ^ini|env|toml|yml|yaml$ ]]; then
        endline='#'
    fi
    if [[ "$ext" =~ ^ini|env$ ]]; then
        comments="[;#]"
    fi
    if [[ "php" == "$ext" ]] || [[ "$ext" == "js" ]]; then
        comments="//"
    fi
    local list='\[\s*['$string']?\w+['$string']?\]'
    local var_part='^\s*((const|var|let)\s+)?\$?(\w+('$list')*(->|\.|\[))*\s*'
    var_part+="[$string]?${key}[$string]?"
    var_part+='\s*\]?\s*'
    var_part+="($assign)"
    var_part+='\s*'

    # Extract the part after assignation sign
    local expression_with_comment="$((tail +$after_line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL) | head -n1)"
    if [[ "$expression_with_comment" == "YNH_NULL" ]]; then
        set -o xtrace # set -x
        return 1
    fi
    local value_line_number="$(tail +$after_line_number ${file} | grep -m1 -n -i -P $var_part'\K.*$' | cut -d: -f1)"
    value_line_number=$((after_line_number + value_line_number))
    local range="${after_line_number},${value_line_number} "

    # Remove comments if needed
    local expression="$(echo "$expression_with_comment" | sed "s@${comments}[^$string]*\$@@g" | sed "s@\s*[$endline]*\s*]*\$@@")"
    endline=${expression_with_comment#"$expression"}
    endline="$(echo "$endline" | sed 's/\\/\\\\/g')"
    value="$(echo "$value" | sed 's/\\/\\\\/g')"
    value=${value//&/"\&"}
    local first_char="${expression:0:1}"
    delimiter=$'\001'
    if [[ "$first_char" == '"' ]]; then
        # \ and sed is quite complex you need 2 \\ to get one in a sed
        # So we need \\\\ to go through 2 sed
        value="$(echo "$value" | sed 's/"/\\\\"/g')"
        sed -ri "${range}s$delimiter"'(^'"${var_part}"'")([^"]|\\")*("[\s;,]*)(\s*'$comments'.*)?$'$delimiter'\1'"${value}"'"'"${endline}${delimiter}i" ${file}
    elif [[ "$first_char" == "'" ]]; then
        # \ and sed is quite complex you need 2 \\ to get one in a sed
        # However double quotes implies to double \\ to
        # So we need \\\\\\\\ to go through 2 sed and 1 double quotes str
        value="$(echo "$value" | sed "s/'/\\\\\\\\'/g")"
        sed -ri "${range}s$delimiter(^${var_part}')([^']|\\')*('"'[\s,;]*)(\s*'$comments'.*)?$'$delimiter'\1'"${value}'${endline}${delimiter}i" ${file}
    else
        if [[ "$value" == *"'"* ]] || [[ "$value" == *'"'* ]] || [[ "$ext" =~ ^php|py|json|js$ ]]; then
            value='\"'"$(echo "$value" | sed 's/"/\\\\"/g')"'\"'
        fi
        if [[ "$ext" =~ ^yaml|yml$ ]]; then
            value=" $value"
        fi
        sed -ri "${range}s$delimiter(^${var_part}).*\$$delimiter\1${value}${endline}${delimiter}i" ${file}
    fi
    set -o xtrace # set -x
}
