# -*- mode: shell-script -*-

yaml_opt_bash_env() {
    local prefix="$1" key value
    while read-0 key value; do
        new_prefix="${prefix}_${key^^}"
        if [[ "$(echo "$value" | shyaml get-type)" == "struct" ]]; then
            echo "$value" | yaml_opt_bash_env "${new_prefix}"
        else
            printf "%s\0%s\0" "${new_prefix/-/_}" "$value"
        fi
    done < <(shyaml key-values-0)
}


yaml_opt_bash_env_ignore_first_level() {
    local prefix="$1" key value
    while read-0 key value; do
        new_prefix="${prefix}_${key^^}"
        if [[ "$(echo "$value" | shyaml get-type)" == "struct" ]]; then
            echo "$value" | yaml_opt_bash_env "${new_prefix}"
        fi
    done < <(shyaml key-values-0)
}


get_dc_env() {
    local cfg="$1" action="$2" domain="$3"

    config="\
$SERVICE_NAME:
  docker-compose:
    environment:"
    if USER_EMAIL=$(echo "$cfg" | shyaml get-value email 2>/dev/null); then
        config+=$'\n'"      LETSENCRYPT_USER_MAIL: $USER_EMAIL"
    fi

    if environment_def="$(printf "%s" "$cfg" | shyaml -y get-value env 2>/dev/null)"; then
        while read-0 key value; do
            config+="$(printf "\n      %s: %s" "$key" "$value")"
        done < <(e "$environment_def" | yaml_opt_bash_env_ignore_first_level LEXICON)

        if ! provider=$(e "$environment_def" | shyaml get-value provider 2>/dev/null); then
            provider=
            ## If no provider is given, we fallback on the first found
            while read-0 key value; do
                [[ "$(echo "$value" | shyaml get-type)" == "struct" ]] && {
                    provider="$key"
                    break
                }
            done < <(e "$environment_def" | shyaml key-values-0)
            warn "No ${WHITE}provider${NORMAL} key given, had to infer it, chose '$key'."
        fi
        if [ "$provider" ]; then
            config+=$(echo -en "\n      LEXICON_PROVIDER: $provider")
        fi
    fi

    challenge_type=$(get_challenge_type "$cfg" "$action" "$domain")

    config+=$(echo -en "\n      CHALLENGE_TYPE: $challenge_type")
    info "Challenge type is $challenge_type"

    echo "$config"
}

compose_get_challenge_type() {
    local cfg="$1"
    e "$cfg" | shyaml get-value "challenge-type" 2>/dev/null
}

letsencrypt_get_challenge_type() {
    local domain="$1" renewal_file
    renewal_file="$SERVICE_DATASTORE"/etc/letsencrypt/renewal/"$domain".conf
    [ -e "$renewal_file" ] || return 1
    grep '^pref_challs' "$renewal_file" | cut -f 2 -d "=" | nspc
}

letsencrypt_set_renew_before_expiry() {
    local domain="$1" days="$2" renewal_file
    renewal_file="$SERVICE_DATASTORE"/etc/letsencrypt/renewal/"$domain".conf
    [ -e "$renewal_file" ] || return 1
    sed -ri "s/^(#\s+)?(renew_before_expiry\s*=)\s*[0-9]+(\s+days)$/\2 $days\3/g" "$renewal_file"
}

letsencrypt_get_renew_before_expiry() {
    local domain="$1" renewal_file
    renewal_file="$SERVICE_DATASTORE"/etc/letsencrypt/renewal/"$domain".conf
    [ -e "$renewal_file" ] || return 1
    if out=$(egrep "^renew_before_expiry\s*=\s*[0-9]+\s+days$" "$renewal_file" 2>/dev/null); then
        e "$out" | sed -r "s/^renew_before_expiry\s*=\s*([0-9]+)\s+days$/\1/g"
    else
        err "Couldn't find 'renew_before_expiry' in letsencrypt renewal" \
            "configuration for domain '$domain'."
        return 1
    fi
}


get_challenge_type() {
    local cfg="$1" action="$2" domain="$3" challenge_type renewal_file challenge
    case "$action" in
        create)
            if ! challenge_type=$(compose_get_challenge_type "$cfg"); then
                warn "No ${WHITE}challenge-type${NORMAL} provided, defaulting to 'http'."
                challenge_type=http
            fi
            echo "$challenge_type"
            ;;
        renew)
            challenge=$(letsencrypt_get_challenge_type "$domain")
            if [[ "$challenge" =~ ^http ]]; then
                echo "http"
            else
                echo "$challenge"
            fi
            ;;
        *)
            err "Invalid action '$action'."
            ;;
    esac
}


will_need_http_access() {
    local cfg="$1" action="$2" domain="$3" domains args_domains remaining

    challenge_type=$(get_challenge_type "$cfg" "$action" "$domain")
    [ "$challenge_type" == "http" ] || return 1


}


has_existing_cert() {
    local domain="$1"
    [ -d "$SERVICE_DATASTORE/etc/letsencrypt/live/$domain" ] || return 1
}

letsencrypt_cert_info() {
    local domain="$1"
    compose -q --no-init --no-relations run -T --rm "$SERVICE_NAME" \
            crt info "$domain"
}


letsencrypt_cert_delete() {
    local domain="$1"
    compose --debug --no-init --no-relations run --rm "$SERVICE_NAME" \
            certbot delete --cert-name "$domain"
}


valid_existing_cert() {
    local renew_before_expiry="$1" domain="$2" args_domains domains remaining
    shift
    args_domains=("$@")
    has_existing_cert "$domain" || return 1

    info "Querying $domain for previous info..."
    out=$(letsencrypt_cert_info "$domain")

    ## check if output is valid yaml
    err=$(e "$out" | shyaml get-value 2>&1 >/dev/null) || {
        err "Cert info on '$domain' output do not seem to be valid YAML:"
        echo "  cert info content:" >&2
        e "$out" | prefix "    ${GRAY}|$NORMAL " >&2
        echo >&2
        echo "  parsing error:" >&2
        e "$err" | prefix "    ${RED}!$NORMAL " >&2
        echo >&2
        return 3
    }

    domains=$(e "$out" | shyaml get-value domains) || return 1

    domains=$(printf "%s " $domains | tr " " "\n" | sort)
    args_domains=$(printf "%s " "${args_domains[@]}" | tr " " "\n" | sort)
    # info domains: "$domains"
    # info args_domain: "$args_domains"
    remaining=$(e "$out" | shyaml get-value remaining) || return 1
    if [ "$domains" != "$args_domains" ]; then
        info "Domains mismatch:"
        info "  old: $domains"
        info "  new: $args_domains"
        return 2
    fi

    if [ "$remaining" == EXPIRED ]; then
        info "Existing certificate expired."
        return 1
    fi

    if [ "$remaining" -lt "$renew_before_expiry" ]; then
        info "Existing certificate in renew period" \
             "($remaining remaining days of validity)."
        return 1
    fi
}


get_domain_list() {
    compose -q --no-init --no-relations run --rm "$SERVICE_NAME" crt list
}


crt() {
    local cfg="$1" action="$2" domain="$3" config \
          stopped_containers container_ids
    shift
    shift
    ## expiry was checked, launch the action on the real charm, but take care of
    ## correctly running it.
    ## - provide env
    ## - declare proper ports
    ## - stop containers and restart them if necessary

    config=$(get_dc_env "$cfg" "$action" "$domain") || return 1
    stopped_containers=()
    if will_need_http_access "$cfg" "$action" "$domain"; then
        container_ids=($(docker ps \
                                --filter label="compose.project=$PROJECT_NAME" \
                                --filter publish=80 \
                                --format "{{.ID}}"
                       )) || exit 1

        for container_id in "${container_ids[@]}"; do
            info "Attempting to clear port 80 by stopping $container_id"
            docker stop -t 5 "$container_id"
            stopped_containers+=("$container_id")
        done
        config+=$(echo -en "\n    ports:
    - \"0.0.0.0:80:80\"")
    fi

    compose_opts=()
    if [ "$DEBUG" ]; then
        compose_opts+=("--debug")
    else
        compose_opts+=("--quiet")
    fi
    compose "${compose_opts[@]}" --no-init --no-relations --add-compose-content "$config" \
         run --service-ports --rm "$SERVICE_NAME" crt "$action" "$@"
    errlvl="$?"

    for container_id in "${stopped_containers[@]}"; do
        info "Attempting restart $container_id"
        docker start "$container_id"
    done

    return "$errlvl"
}


crt_create() {
    local force service_def cfg renew_before_expiry msg domains
    usage="
  $exname [-h|--help]
  $exname MAIN_DOMAIN [ALT_DOMAINS...]"

    force=
    domains=()
    while [ "$1" ]; do
        case "$1" in
            "--help"|"-h")
                print_usage
                return 0
                ;;
            "--force"|"-f") force=1;;
            *) domains+=("$1");;
        esac
        shift
    done

    if [ "${#domains[@]}" == 0 ]; then
        err "At least one domain should be provided as argument."
        print_usage >&2
        return 1
    fi

    service_def=$(get_compose_service_def "$SERVICE_NAME") || return 1
    cfg=$(e "$service_def" | shyaml get-value "options" 2>/dev/null)
    renew_before_expiry=$(e "$cfg" | shyaml get-value "renew-before-expiry" 30 2>/dev/null)
    renew_before_expiry=${renew_before_expiry:-30}
    valid_existing_cert "$renew_before_expiry" "${domains[@]}"
    valid_existing_cert="$?"
    if [ -z "$force" ] && [ "$valid_existing_cert" == 0 ]; then
        if [ "${#domains[@]}" -gt 1 ]; then
            msg=" (with ${domains[*]:1})"
        fi
        info "A valid cert already exists for domain ${domains[0]}$msg."
        return 0
    fi

    if [ "$valid_existing_cert" == 2 ]; then
        err "Domain mismatch detected, lets delete previous cert."
        letsencrypt_cert_delete "${domains[0]}" || return 1
        err "Previous cert for ${domains[0]} deleted."
    fi

    if [ "$valid_existing_cert" == 3 ]; then
        err "Unexpected failure while checking previous cert info"
        return 1
    fi

    crt "$cfg" create "${domains[@]}" || {
        err "Certificate creation/renew failed for domain '${domains[0]}'."
        return 1
    }

    letsencrypt_set_renew_before_expiry "${domains[0]}" "$renew_before_expiry" || {
        err "Setting renew-before-expiry on '${domains[0]}' failed."
        return 1
    }
}


crt_renew() {
    local service_def cfg renew_before_expiry msg start domains_yml \
          domain domain_cfg
    usage="$
  $exname [-h|--help]
"

    while [ "$1" ]; do
        case "$1" in
            "--help"|"-h")
                print_usage
                return 0
                ;;
            *)
                err "No argument required"
                print_usage >&2
                return 1
                ;;
        esac
        shift
    done

    service_def=$(get_compose_service_def "$SERVICE_NAME") || return 1
    cfg=$(e "$service_def" | shyaml get-value "options" 2>/dev/null)
    default_renew_before_expiry=$(e "$cfg" | shyaml get-value "renew-before-expiry" 2>/dev/null)
    default_renew_before_expiry=${renew_before_expiry:-30}
    if ! renew_before_expiry=$(letsencrypt_get_renew_before_expiry "$domain") || \
            [ -z "$renew_before_expiry" ]; then
        renew_before_expiry=$default_renew_before_expiry
    fi
    start="$SECONDS"
    info "Get domain list.."
    domains_yml=$(get_domain_list) || return 1
    info "  .. Done ${GRAY}($((SECONDS - start))s)${NORMAL}"

    [ "$domains_yml" ] || {
        info "No domain founds"
        return 0
    }

    failed=()
    while read-0 domain domain_cfg; do
        remaining=$(e "$domain_cfg" | shyaml get-value "remaining") || return 1
        if [ "$remaining" == EXPIRED ] || [ "$remaining" -lt "$renew_before_expiry" ]; then
            if [ "$remaining" == EXPIRED ]; then
                info "Renewing domain $domain (expired)."
            else
                info "Renewing domain $domain ($remaining days left)."
            fi
            crt "$cfg" renew "$domain"
            if [ "$?" != "0" ]; then
                failed+=("$domain")
                err "Certificate renew of '$domain' failed."
            fi
        else
            info "Domain $domain does not need renewing ($remaining days left)."
        fi
    done < <(e "$domains_yml" | shyaml key-values-0)

    if [ "${#failed[@]}" -gt 0 ]; then
        err "At least one domain failed to be renewed: ${failed[@]}"
        return 1
    fi
}