#!/bin/bash

# shellcheck source=functions.sh
source /app/functions.sh

CERTS_UPDATE_INTERVAL="${CERTS_UPDATE_INTERVAL:-3600}"
ACME_CA_URI="${ACME_CA_URI:-"https://acme-v02.api.letsencrypt.org/directory"}"
ACME_CA_TEST_URI="https://acme-staging-v02.api.letsencrypt.org/directory"
DEFAULT_KEY_SIZE="${DEFAULT_KEY_SIZE:-4096}"
RENEW_PRIVATE_KEYS="$(lc "${RENEW_PRIVATE_KEYS:-true}")"

# Backward compatibility environment variable
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
      set_ownership_and_permissions "$target"
      [[ "$DEBUG" == 1 ]] && 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 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" == 1 ]] && 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
      local -n hosts_array="LETSENCRYPT_${cid}_HOST"
      for domain in "${hosts_array[@]}"; do
        # Add domain to the array storing currently enabled domains.
        ENABLED_DOMAINS+=("$domain")
      done
    done
    [[ "$DEBUG" == 1 ]] && 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" == 1 ]] && 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" == 1 ]] && echo "Some domains are disabled :"
      for disabled_domain in "${DISABLED_DOMAINS[@]}"; do
          [[ "$DEBUG" == 1 ]] && 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" == 1 ]] && echo "No .companion file found in ${cert_folder}. ${disabled_domain} is not managed by acme-companion. Skipping domain."
              continue
          else
              [[ "$DEBUG" == 1 ]] && echo "${disabled_domain} is managed by acme-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" == 1 ]] && echo "Removing /etc/nginx/certs/${file}"
                  rm -f "/etc/nginx/certs/${file}"
              fi
          done
      done
      return 0
    else
      return 1
    fi
}

function update_cert {
    local cid="${1:?}"
    local -n hosts_array="LETSENCRYPT_${cid}_HOST"
    # First domain will be our base domain
    local base_domain="${hosts_array[0]}"

    local should_restart_container='false'

    # Base CLI parameters array, used for both --register-account and --issue
    local -a params_base_arr
    params_base_arr+=(--log /dev/null)
    [[ "$DEBUG" == 1 ]] && params_base_arr+=(--debug 2)

    # Alternative trusted root CA path, used for test with Pebble
    if [[ -n "${CA_BUNDLE// }" ]]; then
        if [[ -f "$CA_BUNDLE" ]]; then
            params_base_arr+=(--ca-bundle "$CA_BUNDLE")
            [[ "$DEBUG" == 1 ]] && echo "Debug: acme.sh will use $CA_BUNDLE as trusted root CA."
        else
            echo "Warning: the path to the alternate CA bundle ($CA_BUNDLE) is not valid, using default Alpine trust store."
        fi
    fi

    # CLI parameters array used for --register-account
    local -a params_register_arr

    # CLI parameters array used for --issue
    local -a params_issue_arr
    params_issue_arr+=(--webroot /usr/share/nginx/html)

    local -n cert_keysize="LETSENCRYPT_${cid}_KEYSIZE"
    if [[ -z "$cert_keysize" ]] || \
        [[ ! "$cert_keysize" =~ ^(2048|3072|4096|ec-256|ec-384)$ ]]; then
        cert_keysize=$DEFAULT_KEY_SIZE
    fi
    params_issue_arr+=(--keylength "$cert_keysize")

    # OCSP-Must-Staple extension
    local -n ocsp="ACME_${cid}_OCSP"
    if [[ $(lc "$ocsp") == true ]]; then
        params_issue_arr+=(--ocsp-must-staple)
    fi

    local -n accountemail="LETSENCRYPT_${cid}_EMAIL"
    local config_home
    # If we don't have a LETSENCRYPT_EMAIL from the proxied container
    # and DEFAULT_EMAIL is set to a non empty value, use the latter.
    if [[ -z "$accountemail" ]]; then
        if [[ -n "${DEFAULT_EMAIL// }" ]]; then
            accountemail="$DEFAULT_EMAIL"
        else
            unset accountemail
        fi
    fi
    if [[ -n "${accountemail// }" ]]; then
        # If we got an email, use it with the corresponding config home
        config_home="/etc/acme.sh/$accountemail"
    else
        # If we did not get any email at all, use the default (empty mail) config
        config_home="/etc/acme.sh/default"
    fi 

    local -n acme_ca_uri="ACME_${cid}_CA_URI"
    if [[ -z "$acme_ca_uri" ]]; then
        # Use default or user provided ACME end point
        acme_ca_uri="$ACME_CA_URI"
    fi
    # LETSENCRYPT_TEST overrides LETSENCRYPT_ACME_CA_URI
    local -n test_certificate="LETSENCRYPT_${cid}_TEST"
    if [[ $(lc "$test_certificate") == true ]]; then
        # Use Let's Encrypt ACME V2 staging end point
        acme_ca_uri="$ACME_CA_TEST_URI"
    fi

    # Set relevant --server parameter and ca folder name
    params_base_arr+=(--server "$acme_ca_uri")
    local ca_dir="${acme_ca_uri##*://}" \
        && ca_dir="${ca_dir%%/*}" \
        && ca_dir="${ca_dir%%:*}"

    local certificate_dir
    # If we're going to use one of LE stating endpoints ...
    if [[ "$acme_ca_uri" =~ ^https://acme-staging.* ]]; then
        # Unset accountemail
        # force config dir to 'staging'
        unset accountemail
        config_home="/etc/acme.sh/staging"
        # Prefix test certificate directory with _test_
        certificate_dir="/etc/nginx/certs/_test_$base_domain"
    else
        certificate_dir="/etc/nginx/certs/$base_domain"
    fi
    params_issue_arr+=( \
        --cert-file "${certificate_dir}/cert.pem" \
        --key-file "${certificate_dir}/key.pem" \
        --ca-file "${certificate_dir}/chain.pem" \
        --fullchain-file "${certificate_dir}/fullchain.pem" \
    )

    [[ ! -d "$config_home" ]] && mkdir -p "$config_home"
    params_base_arr+=(--config-home "$config_home")
    local account_file="${config_home}/ca/${ca_dir}/account.json"

    # External Account Binding (EAB)
    local -n eab_kid="ACME_${cid}_EAB_KID"
    local -n eab_hmac_key="ACME_${cid}_EAB_HMAC_KEY"
    if [[ ! -f "$account_file" ]]; then
        if [[ -n "${eab_kid}" && -n "${eab_hmac_key}" ]]; then
            # Register the ACME account with the per container EAB credentials.
            params_register_arr+=(--eab-kid "$eab_kid" --eab-hmac-key "$eab_hmac_key")
        elif [[ -n "${ACME_EAB_KID// }" && -n "${ACME_EAB_HMAC_KEY// }" ]]; then
            # We don't have per-container EAB kid and hmac key or Zero SSL API key.
            # Register the ACME account with the default EAB credentials.
            params_register_arr+=(--eab-kid "$ACME_EAB_KID" --eab-hmac-key "$ACME_EAB_HMAC_KEY")
        elif [[ -n "${accountemail// }" ]]; then
            # We don't have per container nor default EAB credentials, register a new account.
            params_register_arr+=(--accountemail "$accountemail")
        fi
    fi

    # Zero SSL
    if [[ "$acme_ca_uri" == "https://acme.zerossl.com/v2/DV90" ]]; then
        # Test if we already have:
        #   - an account file
        #   - the --accountemail account registration parameter
        #   - the --eab-kid and --eab-hmac-key account registration parameters
        local account_ok='false'
        if [[ -f "$account_file" ]]; then
            account_ok='true'
        elif in_array '--accountemail' 'params_register_arr'; then
            account_ok='true'
        elif in_array '--eab-kid' 'params_register_arr' && in_array '--eab-hmac-key' 'params_register_arr'; then
            account_ok='true'
        fi

        if [[ $account_ok == 'false' ]]; then
            local -n zerossl_api_key="ZEROSSL_${cid}_API_KEY"
            if [[ -z "$zerossl_api_key" ]]; then
                # Try using the default API key
                zerossl_api_key="${ZEROSSL_API_KEY:-}"
            fi

            if [[ -n "${zerossl_api_key// }" ]]; then
                # Generate a set of ACME EAB credentials using the ZeroSSL API.
                local zerossl_api_response
                if zerossl_api_response="$(curl -s -X POST "https://api.zerossl.com/acme/eab-credentials?access_key=${zerossl_api_key}")"; then
                    if [[ "$(jq -r .success <<< "$zerossl_api_response")" == 'true' ]]; then
                        eab_kid="$(jq -r .eab_kid <<< "$zerossl_api_response")"
                        eab_hmac_key="$(jq -r .eab_hmac_key <<< "$zerossl_api_response")"
                        params_register_arr+=(--eab-kid "$eab_kid" --eab-hmac-key "$eab_hmac_key")
                        [[ "$DEBUG" == 1 ]] && echo "Successfull EAB credentials request against the ZeroSSL API, got the following EAB kid : ${eab_kid}"
                    else
                        # The JSON response body indicated an unsuccesfull API call.
                        echo "Warning: the EAB credentials request against the ZeroSSL API was not successfull."
                    fi
                else
                    # curl failed.
                    echo "Warning: curl failed to make an HTTP POST request to https://api.zerossl.com/acme/eab-credentials."
                fi
            else
                # We don't have a Zero SSL ACME account, EAB credentials, a ZeroSSL API key or an account email :
                # skip certificate account registration and certificate issuance. 
                echo "Error: usage of ZeroSSL require an email bound account. No EAB credentials, ZeroSSL API key or email were provided for this certificate, creation aborted."
                return 1
            fi        
        fi
    fi
    
    # Account registration and update if required
    if [[ ! -f "$account_file" ]]; then
        params_register_arr=("${params_base_arr[@]}" "${params_register_arr[@]}")
        [[ "$DEBUG" == 1 ]] && echo "Calling acme.sh --register-account with the following parameters : ${params_register_arr[*]}"
        acme.sh --register-account "${params_register_arr[@]}"
    fi
    if [[ -n "${accountemail// }" ]] && ! grep -q "mailto:$accountemail" "$account_file"; then
        local -a params_update_arr=("${params_base_arr[@]}" --accountemail "$accountemail")
        [[ "$DEBUG" == 1 ]] && echo "Calling acme.sh --update-account with the following parameters : ${params_update_arr[*]}"
        acme.sh --update-account "${params_update_arr[@]}"
    fi

    # If we still don't have an account.json file by this point, we've got an issue
    if [[ ! -f "$account_file" ]]; then
        echo "Error: no ACME account was found or registered for $accountemail and $acme_ca_uri, certificate creation aborted."
        return 1
    fi

    # acme.sh pre and post hooks
    local -n acme_pre_hook="ACME_${cid}_PRE_HOOK"
    if [[ -n "${acme_pre_hook}" ]]; then
        # Use per-container pre hook
	    params_issue_arr+=(--pre-hook "$acme_pre_hook")
    elif [[ -n ${ACME_PRE_HOOK// } ]]; then
        # Use default pre hook
        params_issue_arr+=(--pre-hook "$ACME_PRE_HOOK")
    fi

    local -n acme_post_hook="ACME_${cid}_POST_HOOK"
    if [[ -n "${acme_post_hook}" ]]; then
        # Use per-container post hook
	    params_issue_arr+=(--post-hook "$acme_post_hook")
    elif [[ -n ${ACME_POST_HOOK// } ]]; then
        # Use default post hook
        params_issue_arr+=(--post-hook "$ACME_POST_HOOK")
    fi

    local -n acme_preferred_chain="ACME_${cid}_PREFERRED_CHAIN"
    if [[ -n "${acme_preferred_chain}" ]]; then
        # Using amce.sh --preferred-chain to select alternate chain.
        params_issue_arr+=(--preferred-chain "$acme_preferred_chain")
    fi
    if [[ "$RENEW_PRIVATE_KEYS" != 'false' && "$REUSE_PRIVATE_KEYS" != 'true' ]]; then
        params_issue_arr+=(--always-force-new-domain-key)      
    fi
    [[ "${2:-}" == "--force-renew" ]] && params_issue_arr+=(--force)

    # Create directory for the first domain
    mkdir -p "$certificate_dir"
    set_ownership_and_permissions "$certificate_dir"

    for domain in "${hosts_array[@]}"; do
        # Add all the domains to certificate
        params_issue_arr+=(--domain "$domain")
        # Add location configuration for the domain
        add_location_configuration "$domain" || reload_nginx
    done

    params_issue_arr=("${params_base_arr[@]}" "${params_issue_arr[@]}")
    [[ "$DEBUG" == 1 ]] && echo "Calling acme.sh --issue with the following parameters : ${params_issue_arr[*]}"
    echo "Creating/renewal $base_domain certificates... (${hosts_array[*]})"
    acme.sh --issue "${params_issue_arr[@]}"

    local acmesh_return=$?

    # 0 = success, 2 = RENEW_SKIP
    if [[ $acmesh_return == 0 || $acmesh_return == 2 ]]; then
        for domain in "${hosts_array[@]}"; do
            if [[ $acme_ca_uri =~ ^https://acme-staging.* ]]; 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
        echo "${COMPANION_VERSION:-}" > "${certificate_dir}/.companion"
        set_ownership_and_permissions "${certificate_dir}/.companion"
        # Make private key root readable only
        for file in cert.pem key.pem chain.pem fullchain.pem; do
            local file_path="${certificate_dir}/${file}"
            [[ -e "$file_path" ]] && set_ownership_and_permissions "$file_path"
        done
        local acme_private_key="$(find /etc/acme.sh -name "*.key" -path "*${hosts_array[0]}*")"
        [[ -e "$acme_private_key" ]] && set_ownership_and_permissions "$acme_private_key"
        # Queue nginx reload if a certificate was issued or renewed
        [[ $acmesh_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
    local -n restart_container="LETSENCRYPT_${cid}_RESTART_CONTAINER"
    if [[ $(lc "$restart_container") == true ]] && [[ "$should_restart_container" == 'true' ]]; then
        echo "Restarting container (${cid})..."
        docker_restart "${cid}"
    fi

    for domain in "${hosts_array[@]}"; do
        if [[ -f "/etc/nginx/conf.d/standalone-cert-$domain.conf" ]]; then
            [[ "$DEBUG" == 1 ]] && 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

}

function update_certs {
    local -a LETSENCRYPT_CONTAINERS
    local -a LETSENCRYPT_STANDALONE_CERTS

    pushd /etc/nginx/certs > /dev/null || return
    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
                local -n hosts_array="LETSENCRYPT_${cid}_HOST"
                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
        # Pass the eventual --force-renew arg to update_cert() as second arg
        update_cert "$cid" "${1:-}"
    done

    cleanup_links && should_reload_nginx='true'

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

    popd > /dev/null || return
}

# 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 ${CERTS_UPDATE_INTERVAL}s"
sleep $CERTS_UPDATE_INTERVAL & pid=$!
wait
pid=
