#!/bin/bash
# shellcheck disable=SC2120

source /app/functions.sh

seconds_to_wait=3600
ACME_CA_URI="${ACME_CA_URI:-https://acme-v01.api.letsencrypt.org/directory}"
DEFAULT_KEY_SIZE=4096
REUSE_ACCOUNT_KEYS="$(lc ${REUSE_ACCOUNT_KEYS:-true})"
REUSE_PRIVATE_KEYS="$(lc ${REUSE_PRIVATE_KEYS:-false})"

function create_link {
    local -r source=${1?missing source argument}
    local -r target=${2?missing target argument}
    if [[ -f "$target" ]] && [[ "$(readlink "$target")" == "$source" ]]; then
      [[ $DEBUG == true ]] && echo "$target already linked to $source"
      return 1
    else
      ln -sf "$source" "$target"
    fi
}

function create_links {
    local -r base_domain=${1?missing base_domain argument}
    local -r domain=${2?missing base_domain argument}

    if [[ ! -f "/etc/nginx/certs/$base_domain/fullchain.pem" || \
          ! -f "/etc/nginx/certs/$base_domain/key.pem" ]]; then
        return 1
    fi
    local return_code=1
    create_link "./$base_domain/fullchain.pem" "/etc/nginx/certs/$domain.crt"
    return_code=$(( $return_code & $? ))
    create_link "./$base_domain/key.pem" "/etc/nginx/certs/$domain.key"
    return_code=$(( $return_code & $? ))
    if [[ -f "/etc/nginx/certs/dhparam.pem" ]]; then
        create_link ./dhparam.pem "/etc/nginx/certs/$domain.dhparam.pem"
        return_code=$(( $return_code & $? ))
    fi
    if [[ -f "/etc/nginx/certs/$base_domain/chain.pem" ]]; then
        create_link "./$base_domain/chain.pem" "/etc/nginx/certs/$domain.chain.pem"
        return_code=$(( $return_code & $? ))
    fi
    return $return_code
}

function cleanup_links {
    local -a LETSENCRYPT_CONTAINERS
    local -a LETSENCRYPT_STANDALONE_CERTS
    local -a ENABLED_DOMAINS
    local -a SYMLINKED_DOMAINS
    local -a DISABLED_DOMAINS

    # Create an array containing domains for which a
    # symlinked private key exists in /etc/nginx/certs.
    for symlinked_domain in /etc/nginx/certs/*.crt; do
        [[ -L "$symlinked_domain" ]] || continue
        symlinked_domain="${symlinked_domain##*/}"
        symlinked_domain="${symlinked_domain%*.crt}"
        SYMLINKED_DOMAINS+=("$symlinked_domain")
    done
    [[ $DEBUG == true ]] && echo "Symlinked domains: ${SYMLINKED_DOMAINS[*]}"

    # Create an array containing domains that are considered
    # enabled (ie present on /app/letsencrypt_service_data or /app/letsencrypt_user_data).
    [[ -f /app/letsencrypt_service_data ]] && source /app/letsencrypt_service_data
    [[ -f /app/letsencrypt_user_data ]] && source /app/letsencrypt_user_data
    LETSENCRYPT_CONTAINERS+=( "${LETSENCRYPT_STANDALONE_CERTS[@]}" )
    for cid in "${LETSENCRYPT_CONTAINERS[@]}"; do
      host_varname="LETSENCRYPT_${cid}_HOST"
      hosts_array="${host_varname}[@]"
      for domain in "${!hosts_array}"; do
        # Add domain to the array storing currently enabled domains.
        ENABLED_DOMAINS+=("$domain")
      done
    done
    [[ $DEBUG == true ]] && echo "Enabled domains: ${ENABLED_DOMAINS[*]}"

    # Create an array containing only domains for which a symlinked private key exists
    # in /etc/nginx/certs but that no longer have a corresponding LETSENCRYPT_HOST set
    # on an active container or on /app/letsencrypt_user_data
    if [[ ${#SYMLINKED_DOMAINS[@]} -gt 0 ]]; then
        mapfile -t DISABLED_DOMAINS < <(echo "${SYMLINKED_DOMAINS[@]}" \
                                             "${ENABLED_DOMAINS[@]}" \
                                             "${ENABLED_DOMAINS[@]}" \
                                             | tr ' ' '\n' | sort | uniq -u)
    fi
    [[ $DEBUG == true ]] && echo "Disabled domains: ${DISABLED_DOMAINS[*]}"


    # Remove disabled domains symlinks if present.
    # Return 1 if nothing was removed and 0 otherwise.
    if [[ ${#DISABLED_DOMAINS[@]} -gt 0 ]]; then
      [[ $DEBUG == true ]] && echo "Some domains are disabled :"
      for disabled_domain in "${DISABLED_DOMAINS[@]}"; do
          [[ $DEBUG == true ]] && echo "Checking domain ${disabled_domain}"
          cert_folder="$(readlink -f /etc/nginx/certs/${disabled_domain}.crt)"
          # If the dotfile is absent, skip domain.
          if [[ ! -e "${cert_folder%/*}/.companion" ]]; then
              [[ $DEBUG == true ]] && echo "No .companion file found in ${cert_folder}. ${disabled_domain} is not managed by letsencrypt-nginx-proxy-companion. Skipping domain."
              continue
          else
              [[ $DEBUG == true ]] && echo "${disabled_domain} is managed by letsencrypt-nginx-proxy-companion. Removing unused symlinks."
          fi

          for extension in .crt .key .dhparam.pem .chain.pem; do
              file="${disabled_domain}${extension}"
              if [[ -n "${file// }" ]] && [[ -L "/etc/nginx/certs/${file}" ]]; then
                  [[ $DEBUG == true ]] && echo "Removing /etc/nginx/certs/${file}"
                  rm -f "/etc/nginx/certs/${file}"
              fi
          done
      done
      return 0
    else
      return 1
    fi
}

function update_certs {
    local -a LETSENCRYPT_CONTAINERS
    local -a LETSENCRYPT_STANDALONE_CERTS

    check_nginx_proxy_container_run || return

    # Load relevant container settings
    if [[ -f /app/letsencrypt_service_data ]]; then
        source /app/letsencrypt_service_data
    else
        echo "Warning: /app/letsencrypt_service_data not found, skipping data from containers."
    fi

    # Load settings for standalone certs
    if [[ -f /app/letsencrypt_user_data ]]; then
        if source /app/letsencrypt_user_data; then
            for cid in "${LETSENCRYPT_STANDALONE_CERTS[@]}"; do
                host_varname="LETSENCRYPT_${cid}_HOST"
                hosts_array="${host_varname}[@]"
                for domain in "${!hosts_array}"; do
                    add_standalone_configuration "$domain"
                done
            done
            reload_nginx
            LETSENCRYPT_CONTAINERS+=( "${LETSENCRYPT_STANDALONE_CERTS[@]}" )
        else
            echo "Warning: could not source /app/letsencrypt_user_data, skipping user data"
        fi
    fi

    should_reload_nginx='false'
    for cid in "${LETSENCRYPT_CONTAINERS[@]}"; do
        # Derive host and email variable names
        host_varname="LETSENCRYPT_${cid}_HOST"
        # Array variable indirection hack: http://stackoverflow.com/a/25880676/350221
        hosts_array="${host_varname}[@]"
        hosts_array_expanded=("${!hosts_array}")
        # First domain will be our base domain
        base_domain="${hosts_array_expanded[0]}"

        params_d_str=""

        email_varname="LETSENCRYPT_${cid}_EMAIL"
        email_address="${!email_varname:-"<no value>"}"
        if [[ "$email_address" != "<no value>" ]]; then
            params_d_str+=" --email $email_address"
        fi

        keysize_varname="LETSENCRYPT_${cid}_KEYSIZE"
        cert_keysize="${!keysize_varname:-"<no value>"}"
        if [[ "$cert_keysize" == "<no value>" ]]; then
            cert_keysize=$DEFAULT_KEY_SIZE
        fi

        test_certificate_varname="LETSENCRYPT_${cid}_TEST"
        le_staging_uri="https://acme-staging.api.letsencrypt.org/directory"
        if [[ $(lc "${!test_certificate_varname:-}") == true ]] || \
          [[ "$ACME_CA_URI" == "$le_staging_uri" ]]; then
            # Use staging Let's Encrypt ACME end point
            acme_ca_uri="$le_staging_uri"
            # Prefix test certificate directory with _test_
            certificate_dir="/etc/nginx/certs/_test_$base_domain"
        else
            # Use default or user provided ACME end point
            acme_ca_uri="$ACME_CA_URI"
            certificate_dir="/etc/nginx/certs/$base_domain"
        fi

        account_varname="LETSENCRYPT_${cid}_ACCOUNT_ALIAS"
        account_alias="${!account_varname:-"<no value>"}"
        if [[ "$account_alias" == "<no value>" ]]; then
            account_alias=default
        fi

        [[ $DEBUG == true ]] && params_d_str+=" -v"
        [[ $REUSE_PRIVATE_KEYS == true ]] && params_d_str+=" --reuse_key"
        [[ "${1}" == "--force-renew" ]] && params_d_str+=" --valid_min 7776000"

        # Create directory for the first domain,
        # make it root readable only and make it the cwd
        mkdir -p "$certificate_dir"
        set_ownership_and_permissions "$certificate_dir"
        pushd "$certificate_dir" || return

        for domain in "${!hosts_array}"; do
            # Add all the domains to certificate
            params_d_str+=" -d $domain"
            # Add location configuration for the domain
            add_location_configuration "$domain" || reload_nginx
        done

        # The ACME account key full path is derived from the endpoint URI
        # + the account alias (set to 'default' if no alias is provided)
        account_key_dir="../accounts/${acme_ca_uri#*://}"
        account_key_full_path="${account_key_dir}/${account_alias}.json"
        if [[ $REUSE_ACCOUNT_KEYS == true ]]; then
            if [[ -f "$account_key_full_path" ]]; then
                # If there is no symlink to the account key, create it
                if [[ ! -L ./account_key.json ]]; then
                    ln -sf "$account_key_full_path" ./account_key.json
                # If the symlink target the wrong account key, replace it
                elif [[ "$(readlink -f ./account_key.json)" != "$account_key_full_path" ]]; then
                    ln -sf "$account_key_full_path" ./account_key.json
                fi
            fi
        fi

        echo "Creating/renewal $base_domain certificates... (${hosts_array_expanded[*]})"
        /usr/bin/simp_le \
            -f account_key.json -f key.pem -f chain.pem -f fullchain.pem -f cert.pem \
            $params_d_str \
            --cert_key_size=$cert_keysize \
            --server=$acme_ca_uri \
            --default_root /usr/share/nginx/html/

        simp_le_return=$?

        if [[ $REUSE_ACCOUNT_KEYS == true ]]; then
            # If the account key to be reused does not exist yet, copy it
            # from the CWD and replace the file in CWD with a symlink
            if [[ ! -f "$account_key_full_path" && -f ./account_key.json ]]; then
                mkdir -p "$account_key_dir"
                cp ./account_key.json "$account_key_full_path"
                ln -sf "$account_key_full_path" ./account_key.json
            fi
        fi

        popd || return

        if [[ $simp_le_return -ne 2 ]]; then
          for domain in "${!hosts_array}"; do
            if [[ "$acme_ca_uri" == "$le_staging_uri" ]]; then
              create_links "_test_$base_domain" "$domain" && should_reload_nginx='true'
            else
              create_links "$base_domain" "$domain" && should_reload_nginx='true'
            fi
            touch "${certificate_dir}/.companion"
            set_ownership_and_permissions "${certificate_dir}/.companion"
          done
          # Make private key root readable only
          for filename in cert key chain fullchain; do
            set_ownership_and_permissions "${certificate_dir}/${filename}.pem"
          done
          # Make the account key and its parent folders (up to
          # /etc/nginx/certs/accounts included) root readable only
          account_key_perm_path="/etc/nginx/certs/accounts/${acme_ca_uri#*://}/${account_alias}.json"
          until [[ "$account_key_perm_path" == /etc/nginx/certs ]]; do
            set_ownership_and_permissions "$account_key_perm_path"
            account_key_perm_path="$(dirname "$account_key_perm_path")"
          done
          # Queue nginx reload if a certificate was issued or renewed
          [[ $simp_le_return -eq 0 ]] && should_reload_nginx='true'
        fi

        for domain in "${!hosts_array}"; do
            if [[ -f "/etc/nginx/conf.d/standalone-cert-$domain.conf" ]]; then
                [[ $DEBUG == true ]] && echo "Debug: removing standalone configuration file /etc/nginx/conf.d/standalone-cert-$domain.conf"
                rm -f "/etc/nginx/conf.d/standalone-cert-$domain.conf" && should_reload_nginx='true'
            fi
        done

    done

    cleanup_links && should_reload_nginx='true'

    [[ "$should_reload_nginx" == 'true' ]] && reload_nginx
}

# Allow the script functions to be sourced without starting the Service Loop.
if [ "${1}" == "--source-only" ]; then
  return 0
fi

pid=
# Service Loop: When this script exits, start it again.
trap '[[ $pid ]] && kill $pid; exec $0' EXIT
trap 'trap - EXIT' INT TERM

update_certs

# Wait some amount of time
echo "Sleep for ${seconds_to_wait}s"
sleep $seconds_to_wait & pid=$!
wait
pid=
