#!/bin/bash
# shellcheck disable=SC2120

source /app/functions.sh

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

function create_link {
    local -r source=${1?missing source argument}
    local -r target=${2?missing target argument}
    if [[ -f "$target" ]] && [[ "$(readlink "$target")" == "$source" ]]; then
      set_ownership_and_permissions "$target"
      [[ "$(lc $DEBUG)" == true ]] && echo "$target already linked to $source"
      return 1
    else
      ln -sf "$source" "$target" \
        && set_ownership_and_permissions "$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 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
    [[ "$(lc $DEBUG)" == true ]] && echo "Symlinked domains: ${SYMLINKED_DOMAINS[*]}"

    # Create an array containing domains that are considered
    # enabled (ie present on /app/letsencrypt_service_data).
    # shellcheck source=/dev/null
    source /app/letsencrypt_service_data
    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
    [[ "$(lc $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.
    if [[ ${#SYMLINKED_DOMAINS[@]} -gt 0 ]]; then
        mapfile -t DISABLED_DOMAINS < <(echo "${SYMLINKED_DOMAINS[@]}" \
                                             "${ENABLED_DOMAINS[@]}" \
                                             "${ENABLED_DOMAINS[@]}" \
                                             | tr ' ' '\n' | sort | uniq -u)
    fi
    [[ "$(lc $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
      [[ "$(lc $DEBUG)" == true ]] && echo "Some domains are disabled :"
      for disabled_domain in "${DISABLED_DOMAINS[@]}"; do
          [[ "$(lc $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
              [[ "$(lc $DEBUG)" == true ]] && echo "No .companion file found in ${cert_folder}. ${disabled_domain} is not managed by letsencrypt-nginx-proxy-companion. Skipping domain."
              continue
          else
              [[ "$(lc $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
                  [[ "$(lc $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 {

    check_nginx_proxy_container_run || return

    [[ -f /app/letsencrypt_service_data ]] || return

    # Load relevant container settings
    unset LETSENCRYPT_CONTAINERS
    # shellcheck source=/dev/null
    source /app/letsencrypt_service_data

    should_reload_nginx='false'
    for cid in "${LETSENCRYPT_CONTAINERS[@]}"; do
        should_restart_container='false'
        # Derive host and email variable names
        host_varname="LETSENCRYPT_${cid}_HOST"
        # Array variable indirection hack: http://stackoverflow.com/a/25880676/350221
        hosts_array_unfiltered="${host_varname}[@]"
        hosts_array=()
        for domain in "${!hosts_array_unfiltered}"; do
          [[ $domain != *.local ]] && \
          [[ $domain != *.onion ]] && \
          [[ $domain != *.lan ]]  && hosts_array+=( $domain )
        done

        hosts_array_expanded=("${hosts_array[@]}")
        # First domain will be our base domain
        base_domain="${hosts_array_expanded[0]}"

        params_d_str=""

        # Use container's LETSENCRYPT_EMAIL if set, fallback to DEFAULT_EMAIL
        email_varname="LETSENCRYPT_${cid}_EMAIL"
        email_address="${!email_varname}"
        if [[ "$email_address" != "<no value>" ]]; then
            params_d_str+=" --email $email_address"
        elif [[ -n "${DEFAULT_EMAIL:-}" ]]; then
            params_d_str+=" --email $DEFAULT_EMAIL"
        fi

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

        test_certificate_varname="LETSENCRYPT_${cid}_TEST"
        le_staging_uri="https://acme-staging-v02.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}"
        if [[ "$account_alias" == "<no value>" ]]; then
            account_alias=default
        fi

        [[ "$(lc $DEBUG)" == true ]] && params_d_str+=" -v"
        [[ $REUSE_PRIVATE_KEYS == true ]] && params_d_str+=" --reuse_key"

        min_validity="LETSENCRYPT_${cid}_MIN_VALIDITY"
        min_validity="${!min_validity}"
        if [[ "$min_validity" == "<no value>" ]]; then
            min_validity=$DEFAULT_MIN_VALIDITY
        fi
        # Sanity Check
        # Upper Bound
        if [[ $min_validity -gt $MIN_VALIDITY_CAP ]]; then
            min_validity=$MIN_VALIDITY_CAP
        fi
        # Lower Bound
        if [[ $min_validity -lt $(($seconds_to_wait * 2)) ]]; then
            min_validity=$(($seconds_to_wait * 2))
        fi

        if [[ "${1}" == "--force-renew" ]]; then
            # Manually set to highest certificate lifetime given by LE CA
            params_d_str+=" --valid_min 7776000"
        else
            params_d_str+=" --valid_min $min_validity"
        fi

        # 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

        if [[ -e "./account_key.json" ]] && [[ ! -e "./account_reg.json" ]]; then
          # If there is an account key present without account registration, this is
          # a leftover from the ACME v1 version of simp_le. Remove this account key.
          rm -f ./account_key.json
          [[ "$(lc $DEBUG)" == true ]] \
            && echo "Debug: removed ACME v1 account key $certificate_dir/account_key.json"
        fi

        # The ACME account key and registration full path are derived from the
        # endpoint URI + the account alias (set to 'default' if no alias is provided)
        account_dir="../accounts/${acme_ca_uri#*://}"
        if [[ $REUSE_ACCOUNT_KEYS == true ]]; then
            for type in "key" "reg"; do
                file_full_path="${account_dir}/${account_alias}_${type}.json"
                simp_le_file="./account_${type}.json"
                if [[ -f "$file_full_path" ]]; then
                    # If there is no symlink to the account file, create it
                    if [[ ! -L "$simp_le_file" ]]; then
                        ln -sf "$file_full_path" "$simp_le_file" \
                          && set_ownership_and_permissions "$simp_le_file"
                    # If the symlink target the wrong account file, replace it
                    elif [[ "$(readlink -f "$simp_le_file")" != "$file_full_path" ]]; then
                        ln -sf "$file_full_path" "$simp_le_file" \
                          && set_ownership_and_permissions "$simp_le_file"
                    fi
                fi
            done
        fi

        echo "Creating/renewal $base_domain certificates... (${hosts_array_expanded[*]})"
        /usr/bin/simp_le \
            -f account_key.json -f account_reg.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
            mkdir -p "$account_dir"
            for type in "key" "reg"; do
                file_full_path="${account_dir}/${account_alias}_${type}.json"
                simp_le_file="./account_${type}.json"
                # If the account file to be reused does not exist yet, copy it
                # from the CWD and replace the file in CWD with a symlink
                if [[ ! -f "$file_full_path" && -f "$simp_le_file" ]]; then
                    cp "$simp_le_file" "$file_full_path"
                    ln -sf "$file_full_path" "$simp_le_file"
                fi
            done
        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' && should_restart_container='true'
            else
              create_links "$base_domain" "$domain" && should_reload_nginx='true' && should_restart_container='true'
            fi
          done
          touch "${certificate_dir}/.companion"
          # Set ownership and permissions of the files inside $certificate_dir
          for file in .companion cert.pem key.pem chain.pem fullchain.pem account_key.json account_reg.json; do
            file_path="${certificate_dir}/${file}"
            [[ -e "$file_path" ]] && set_ownership_and_permissions "$file_path"
          done
          account_path="/etc/nginx/certs/accounts/${acme_ca_uri#*://}"
          account_key_perm_path="${account_path}/${account_alias}_key.json"
          account_reg_perm_path="${account_path}/${account_alias}_reg.json"
          # Account key and registration files do not necessarily exists after
          # simp_le exit code 1. Check if they exist before perm check (#591).
          [[ -f "$account_key_perm_path" ]] && set_ownership_and_permissions "$account_key_perm_path"
          [[ -f "$account_reg_perm_path" ]] && set_ownership_and_permissions "$account_reg_perm_path"
          # Set ownership and permissions of the ACME account folder and its
          # parent folders (up to /etc/nginx/certs/accounts included)
          until [[ "$account_path" == /etc/nginx/certs ]]; do
            set_ownership_and_permissions "$account_path"
            account_path="$(dirname "$account_path")"
          done
          # Queue nginx reload if a certificate was issued or renewed
          [[ $simp_le_return -eq 0 ]] && should_reload_nginx='true' && should_restart_container='true'
        fi

        # Restart container if certs are updated and the respective environmental variable is set
        restart_container_var="LETSENCRYPT_${cid}_RESTART_CONTAINER"
        if [[ $(lc "${!restart_container_var:-}") == true ]] && [[ "$should_restart_container" == 'true' ]]; then
            echo "Restarting container (${cid})..."
            docker_restart "${cid}"
        fi

    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=
