#!/bin/bash

usage() {
    PREFIX="miaou:usage" echo '<init>'
    exit 0
}

yqm() {
    #read only
    yq "$1" "$EXPANDED_CONF"
}

yqmi() {
    # for update
    yq "$1" "$EXPANDED_CONF" -i
}

yqmt() {
    # tabular
    yq "$1" "$EXPANDED_CONF" -o t
}

compute_fqdn_middlepart() {
    case "$1" in
    prod)
        local fqdn_middlepart="."
        ;;
    beta)
        local fqdn_middlepart=".beta."
        ;;
    dev)
        local fqdn_middlepart=".dev."
        ;;
    *)
        echowarn "unknown target <${target}>, please fix with correct value from {prod, beta, dev} and try again..."
        exit 1
        ;;
    esac
    builtin echo "$fqdn_middlepart"
}

# archive_conf(FILE)
# save patch in archived folder of current file
function archive_conf() {
    PREFIX="miaou:conf:archive"

    file="$1"
    filename=$(basename "$file")
    mkdir -p "$MIAOU_CONFIGDIR/archived/$filename"
    previous="$MIAOU_CONFIGDIR/archived/$filename/previous"

    # shellcheck disable=SC2012
    latest_patch=$(ls -1tr "$MIAOU_CONFIGDIR/archived/$filename/" | tail -n1)

    if [[ -z "$latest_patch" ]]; then
        echo -n "archiving first file <$file> ..."
        cp "$file" "$previous"
        PREFIX="" echoinfo OK
    elif [[ "$file" -nt "$latest_patch" ]]; then
        patchname="$MIAOU_CONFIGDIR/archived/$filename/$(date +%F_%T)"
        if ! diff "$previous" "$file" >"$patchname"; then
            echo -n "archiving patch <$patchname> ..."
            cp "$file" "$previous"
            PREFIX="" echoinfo OK
        else
            rm "$patchname"
        fi
    fi
}

function archive_allconf() {
    mkdir -p "$MIAOU_CONFIGDIR"
    archive_conf "$CONF"
    archive_conf "$DEFAULTS"
}

function check_expand_conf() {
    if ! "$FORCE" && [ -f "$EXPANDED_CONF" ] && [ "$EXPANDED_CONF" -nt "$CONF" ] && [ "$EXPANDED_CONF" -nt "$DEFAULTS" ]; then
        return 1
    fi
}

function expand_conf() {
    PREFIX="miaou:conf"

    if [[ -f "$EXPANDED_CONF" ]]; then
        current_target=$(grep -Es "^target:" /etc/miaou/defaults.yaml | cut -d ' ' -f2)
        previous_target=$(grep -Es "^target:" "$EXPANDED_CONF" | cut -d ' ' -f2)
        [[ "$current_target" != "$previous_target" ]] && echoerr "TARGET <$previous_target> mismatched <$current_target>" && exit 101
    fi

    # initialize expanded conf by merging default
    # shellcheck disable=SC2016
    yq eval-all '. as $item ireduce ({}; . * $item )' "$CONF" "$DEFAULTS" >"$EXPANDED_CONF"

    # append unique container unless overridden
    mapfile -t services_app_only < <(yqmt '.services.[].[] | has("container") | select ( . == false) | [(parent|key)+" " +key]')

    for i in "${services_app_only[@]}"; do
        read -r -a item <<<"$i"
        domain=${item[0]}
        subdomain=${item[1]}
        app=$(yqm ".services.\"$domain\".\"$subdomain\".app")
        container=$(get_container_for_domain_subdomain_app "$domain" "$subdomain" "$app")
        yqmi ".services.\"$domain\".\"$subdomain\".container=\"$container\""
    done

    # append enabled=true unless overridden
    mapfile -t services_app_only < <(yqmt '.services.[].[] | has("enabled") | select ( . == false) | [(parent|key)+" " +key] | unique ')
    # echo "found <${#services_app_only[@]}> enabled services"
    for i in "${services_app_only[@]}"; do
        read -r -a item <<<"$i"
        domain=${item[0]}
        subdomain=${item[1]}
        yqmi ".services.\"$domain\".\"$subdomain\".enabled=true"
    done

    # compute fqdn
    target=$(yqm '.target')
    fqdn_middlepart=$(compute_fqdn_middlepart "$target")

    # write fqdn_middlepart
    yqmi ".expanded.fqdn_middlepart = \"$fqdn_middlepart\""

    # add monitored.containers section
    yqmi '.expanded.monitored.containers = ([ .services[] | to_entries | .[] | .value | select (.enabled == true  ) | .container ] | unique)'

    # add monitored.hosts section
    # yqmi '.expanded.monitored.hosts = [( .services[][] | select (.enabled == true  ) | {"domain": ( parent | key ), "subdomain": key, "fqdn": key + (parent | parent | parent | .expanded.fqdn_middlepart) + ( parent | key ), "container":.container, "port":.port, "app":.app })]'

    # add services section
    if [[ ${#services_app_only[@]} -gt 0 ]]; then
        yqmi '.expanded.services = [( .services[][] | select (.enabled == true  ) | {"domain": ( parent | key ), "subdomain": key, "fqdn": key + (parent | parent | parent | .expanded.fqdn_middlepart) + ( parent | key ), "container":.container, "port":.port, "app":.app, "name": .name // "", "exp": .exp // false })]'
    else
        yqmi '.expanded.services = []'
    fi

    # change fqdn when exp:true found
    readarray -t services < <(yqmt '.expanded.services[] | [ .[] ]')
    index=0
    for i in "${services[@]}"; do
        read -r -a item <<<"$i"
        exp=${item[-1]} # the last item
        if [[ "$exp" == true ]]; then
            domain=${item[0]}
            subdomain=${item[1]}
            fqdn=${item[2]}
            if [[ $target != beta ]]; then
                echowarn "experimental service <$subdomain.exp.$domain> not recommended for target <$target>"
            else
                echo "experimental service <$subdomain.exp.$domain> detected"
            fi
            yqmi ".expanded.services[$index].fqdn = \"$subdomain.exp.$domain\""
        fi
        index=$((index + 1))
    done

    # add firewall section, bridge_subnet + mail_passthrough if any
    bridge_subnet=$(lxc network get lxdbr0 ipv4.address)
    yqmi ".firewall.bridge_subnet = \"$bridge_subnet\""

    container_mail_passthrough=$(yqm ".firewall.container_mail_passthrough")
}

function build_routes() {
    PREFIX="miaou:routes"

    mkdir -p "$MIAOU_CONFIGDIR/dnsmasq"

    mapfile -t fqdns < <(yqm '.expanded.services[].fqdn')
    echo "building ${#fqdns[@]} routes..."
    raw_dnsmasq=''
    for i in "${fqdns[@]}"; do
        raw_dnsmasq+="address=/$i/$DMZ_IP\\n"
        # append domains to conf
        echo "container <$DMZ_CONTAINER.lxd> serves https://$i"
    done

    builtin echo -e "$raw_dnsmasq" >"$MIAOU_CONFIGDIR/dnsmasq/raw.new"
    lxc network get $BRIDGE raw.dnsmasq | head -n-1 >"$MIAOU_CONFIGDIR/dnsmasq/raw.old"

    if ! diff -q "$MIAOU_CONFIGDIR/dnsmasq/raw.old" "$MIAOU_CONFIGDIR/dnsmasq/raw.new" &>/dev/null; then
        echo -n "new routes detected, reloading dnsmasq + nftables..."
        builtin echo -e "$raw_dnsmasq" | lxc network set $BRIDGE raw.dnsmasq -
        sudo systemctl reload nftables.service
        PREFIX="" echoinfo OK
    fi
}

function build_dmz_certbot {
    PREFIX="miaou:certbot"

    if [[ "$target" != dev ]]; then

        #TODO: check public ip available
        my_ip=$(dig +short myip.opendns.com @resolver1.opendns.com)
        public_hostname=$(hostname -f)
        public_ip=$(dig +short A "${public_hostname}" "@${FDN_NAMESERVER}")

        if hostname -I | grep -q "$my_ip"; then
            echo "My PUBLIC IP address is: <$my_ip>"
            echo "My PUBLIC hostname is: <$public_hostname>"
            [[ $my_ip != "$public_ip" ]] && echoerr "This machine provides wrong public IP: <$public_ip>" && exit 101
        else
            echoerr "This machine can not respond to its PUBLIC IP address: <$my_ip>" #FIXME: && exit 100
        fi

        default_registrar=$(yqm '.registrar.default')
        [[ $default_registrar != 'OVH' ]] && echoerr "Sorry, no OVH registrar detected, please provide other registrar protocol" && exit 101

        readarray -t services < <(yqmt '.expanded.services[] | [ .domain, .fqdn ]')
        for service in "${services[@]}"; do
            read -r -a item <<<"$service"

            domain=${item[0]}
            fqdn=${item[1]}
            subDomain=${fqdn%".${domain}"}

            echo "TODO: fqdn=${fqdn}, domain=${domain}"
            local server_ip
            server_ip=$(dig +short A "$fqdn")

            if [[ $server_ip == "$my_ip" ]]; then
                echo "CNAME <$fqdn> approved successfuly!"
            else
                if [[ -n $server_ip ]]; then
                    local server_name
                    server_name=$(dig +short CNAME "$fqdn")
                    echowarn "CNAME <$fqdn> points to another server: <$server_name>"
                else
                    echo registering "$domain" "$subDomain" to ... "$public_hostname"

                    echo NOT YET! #FIXME: to delete
                    # "$MIAOU_BASEDIR"/lib/registrar/ovh-domain.sh set "$domain" "$subDomain" "$public_hostname"

                    echo "TODO: TEST with dig, wait for reply in 4s then certbot!!!"
                    exit 5
                fi
            fi
        done
    else
        echo "bypass certbot certificate generation due to target=<$target>"
    fi
}

function build_dmz_reverseproxy() {
    PREFIX="miaou:reverseproxy"

    mkdir -p "$MIAOU_CONFIGDIR/nginx"
    tera -t "$MIAOU_BASEDIR/templates/nginx/_default.j2" "$EXPANDED_CONF" -o "$MIAOU_CONFIGDIR/nginx/_default" &>/dev/null
    tera -t "$MIAOU_BASEDIR/templates/nginx/hosts.j2" "$EXPANDED_CONF" -o "$MIAOU_CONFIGDIR/nginx/hosts" &>/dev/null
    for f in "$MIAOU_CONFIGDIR"/nginx/*; do
        lxc file push --uid=0 --gid=0 "$f" "$DMZ_CONTAINER/etc/nginx/sites-available/" &>/dev/null
    done
    cat <<EOF | PREFIX="miaou:build:dmz" lxc_exec "$DMZ_CONTAINER"
        cd /etc/nginx/sites-enabled/
        for i in ../sites-available/*; do
            ln -sf \$i
        done
        nginx -tq && systemctl restart nginx
EOF
    echo "nginx reloaded successfully!"
}

function monit_show() {
    PREFIX="monit:show"
    : $PREFIX

    readarray -t services < <(yqmt '.expanded.services[] | [ .container, .port, .fqdn, .app ]')
    echo "======================"
    echo "${#services[@]} available services"
    echo "======================"

    for service in "${services[@]}"; do
        read -r -a item <<<"$service"

        container=${item[0]}
        port=${item[1]}
        fqdn=${item[2]}
        app=${item[3]}

        [[ -n ${PREFIX:-} ]] && printf "${DARK}%25.25s${NC} " "${PREFIX}"

        if curl -m $MAX_WAIT -I -4so /dev/null "http://$container:$port"; then
            builtin echo -ne "${GREEN}✔${NC}"
        else
            builtin echo -ne "${RED}✘${NC}"
        fi
        printf "\t%16.16s\thttps://%-40s\thttp://%s\n" "$app" "$fqdn" "$container:$port"
    done

}

function build_monit() {

    # test whether monitored items actually run safely
    PREFIX="monit:build"
    readarray -t hosts < <(yqmt '.expanded.services[] | [ .container, .port, .fqdn ]')
    echo -n "monitoring ${#hosts[@]} hosts ..."
    for host in "${hosts[@]}"; do
        read -r -a item <<<"$host"
        container=${item[0]}
        port=${item[1]}
        fqdn=${item[2]}

        if ! (lxc exec "$container" -- ss -tln | grep -q "\(0.0.0.0\|*\):$port"); then
            PREFIX='' echo
            echoerr "no HTTP server responds on <$container.lxd:$port>"
            echoerr "please review configuration <miaou.yaml> for fqdn: $fqdn"
            exit 2
        fi

        if ! curl_check_unsecure "https://$fqdn"; then
            PREFIX='' echo
            echoerr "DMZ does not seem to dispatch https://$fqdn please review DMZ Nginx proxy"
            exit 3
        elif [[ "$target" != 'dev' ]] && ! curl_check "https://$fqdn"; then
            PREFIX='' echo
            echowarn "T=$target missing valid certificate for fqdn <https://$fqdn> please review DMZ certbot"
        fi

    done

    # templates for monit
    mkdir -p "$MIAOU_CONFIGDIR/monit"
    tera -t "$MIAOU_BASEDIR/templates/monit/containers.j2" "$EXPANDED_CONF" -o "$MIAOU_CONFIGDIR/monit/containers" >/dev/null
    tera -t "$MIAOU_BASEDIR/templates/monit/hosts.j2" "$EXPANDED_CONF" -o "$MIAOU_CONFIGDIR/monit/hosts" >/dev/null
    PREFIX="" echo OK
}

# count_service_for_container(container: string)
# returns how many services run inside container according to expanded conf
function count_service_for_container() {
    container_mail_passthrough="$1"
    count=$(yqm ".expanded.services.[] | select(.container == \"$container_mail_passthrough\") | .fqdn" | wc -l)
    builtin echo "$count"
}

function build_nftables() {
    PREFIX="miaou:nftables"
    mkdir -p "$MIAOU_CONFIGDIR/nftables.rules.d"

    container_mail_passthrough=$(yqm '.firewall.container_mail_passthrough')
    if [[ "$container_mail_passthrough" != null ]]; then
        ip_mail_passthrough=$(lxc list "$container_mail_passthrough" -c4 -f csv | grep eth0 | cut -d ' ' -f1)
        [[ -z "$ip_mail_passthrough" ]] && echoerr "container <$container_mail_passthrough> passthrough unknown ip!" && exit 55
        echo "submission protocol granted as passthrough from container <$container_mail_passthrough> ip <$ip_mail_passthrough>"

        count=$(count_service_for_container "$container_mail_passthrough")
        [[ $count == 0 ]] && echowarn "no service detected => no passthrough, no change!"
        [[ $count -gt 1 ]] && echoerr "count <$count> services detected on container <$container_mail_passthrough>, please disable some and leave only one service for safety!!!" && exit 56

        ip_mail_passthrough=$ip_mail_passthrough tera -e --env-key env -t "$MIAOU_BASEDIR/templates/nftables/lxd.table.j2" "$EXPANDED_CONF" -o "$MIAOU_CONFIGDIR/nftables.rules.d/lxd.table" &>/dev/null
    else
        echo "submission protocol prevented from any container"
        tera -t "$MIAOU_BASEDIR/templates/nftables/lxd.table.j2" "$EXPANDED_CONF" -o "$MIAOU_CONFIGDIR/nftables.rules.d/lxd.table" &>/dev/null
    fi

    if ! diff -q "$MIAOU_CONFIGDIR/nftables.rules.d/lxd.table" /etc/nftables.rules.d/lxd.table &>/dev/null; then
        sudo_required "reloading nftables"
        echo -n "reloading nftables..."
        sudo cp "$MIAOU_CONFIGDIR/nftables.rules.d/lxd.table" /etc/nftables.rules.d/lxd.table
        sudo systemctl reload nftables
        PREFIX="" echo DONE
    fi
}

# check whether http server responds something, required <url>, ie: http://example.com:8001, https://example.com
function curl_check() {
    arg1_required "$@"
    # echo "curl $1"
    curl -m $MAX_WAIT -sLI4 "$1" | grep -q "^HTTP.* [2|3|4].*"
}

# check whether https server responds 200 OK, even unsecured certificate (auto-signed in mode DEV)
function curl_check_unsecure() {
    arg1_required "$@"
    curl -m $MAX_WAIT -skLI4 "$1" | grep -q "^HTTP.* [2|3|4].*"
}

function get_dmz_ip() {
    if ! container_running "$DMZ_CONTAINER"; then
        echowarn "Container running dmz <$DMZ_CONTAINER> seems down"
        echoerr "please \`lxc start $DMZ_CONTAINER\` or initialize first!"
        exit 1
    fi

    dmz_ip=$(host "$DMZ_CONTAINER.lxd" | cut -d ' ' -f4)
    if ! valid_ipv4 "$dmz_ip"; then
        echowarn "dmz seems up but no valid ip <$dmz_ip> found!"
        echoerr "please fix this networking issue, then retry..."
        exit 1
    else
        builtin echo "$dmz_ip"
    fi
}

function fetch_container_of_type() {
    local type="$1"
    readarray -t dmzs < <(yqm ".containers.[].[] | select(.==\"$type\") | parent | key")
    case ${#dmzs[@]} in
    0) : ;;
    1) builtin echo "${dmzs[0]}" ;;
    *) for d in "${dmzs[@]}"; do
        builtin echo "$d"
    done ;;
    esac
}

function get_container_for_domain_subdomain_app() {
    local domain="$1"
    local subdomain="$2"
    local app="$3"
    readarray -t containers < <(fetch_container_of_type "$app")
    case ${#containers[@]} in
    0) echoerr "no container of type <$app> found amongst containers for $subdomain.$domain\nHINT : Please, either :\n1. define at least one container for recipe <$app>\n2. remove all services related to recipe <$app>" && exit 1 ;;
    1) builtin echo "${containers[0]}" ;;
    *)
        for d in "${containers[@]}"; do
            echowarn "container of type $app found in <$d>"
        done
        echoerr "multiple containers (${#containers[@]}) provided same app <$app>, therefore container is mandatory alongside $subdomain.$domain" && exit 2
        ;;
    esac
}

function get_unique_container_dmz() {
    readarray -t containers < <(fetch_container_of_type "dmz")
    case ${#containers[@]} in
    0) echoerr "no container of type <dmz> found amongst containers" && exit 1 ;;
    1) builtin echo "${containers[0]}" ;;

    *)
        for d in "${containers[@]}"; do
            echowarn "container of type dmz found in <$d>"
        done
        echoerr "multiple dmz (${#containers[@]}) are not allowed, please select only one " && exit 2
        ;;
    esac
}

function prepare_dmz_container() {
    "$MIAOU_BASEDIR"/recipes/dmz/install.sh "$DMZ_CONTAINER"
}

function check_resolv_conf() {
    local bridge_gw resolver

    bridge_gw=$(lxc network get lxdbr0 ipv4.address | cut -d'/' -f1)
    resolver=$(grep nameserver /etc/resolv.conf | head -n1 | cut -d ' ' -f2)
    [[ "$bridge_gw" != "$resolver" ]] && return 21

    PREFIX="miaou:resolver" echo "nameserver for containers is <$resolver>"

    return 0
}

function prepare_containers() {
    PREFIX="miaou:containers"
    readarray -t containers < <(yqmt ".containers.[] |  [ key, .[] ] ")
    echo "preparing ${#containers[@]} containers for recipes..."
    recipe_count=0
    for i in "${containers[@]}"; do
        read -r -a item <<<"$i"
        container=${item[0]}
        for ((j = 1; j < ${#item[@]}; j++)); do
            recipe_count=$((recipe_count + 1))
            service="${item[$j]}"
            recipe_install="$MIAOU_BASEDIR/recipes/$service/install.sh"
            if [[ -f "$recipe_install" ]]; then
                echowarn "container <$container> installing new recipe [$service]..."
                "$recipe_install" "$container"
                echoinfo DONE
            else
                echoerr "FAILURE, for container <$container>, install recipe [$service] not found!"
                echoerr "please review configuration, mismatch recipe name maybe?"
                exit 50
            fi
            echo "container <$container> accepts recipe [$service]"
        done
    done
    echo "approved ${#containers[@]} containers ready to accept ${recipe_count} recipes"
}

function build_services() {
    PREFIX="miaou:services"
    readarray -t services < <(yqmt '.expanded.services[] | [ .[] ]')

    echo "building ${#services[@]} services..."
    for i in "${services[@]}"; do

        read -r -a item <<<"$i"
        fqdn=${item[2]}
        container=${item[3]}
        port=${item[4]}
        app=${item[5]}
        name=${item[6]:-}

        domain=${item[0]}
        subdomain=${item[1]}
        data=$(yqm ".services.\"$domain\".\"$subdomain\".data|explode(.)")

        recipe="$MIAOU_BASEDIR/recipes/$app/crud.sh"
        if [[ -f "$recipe" ]]; then
            echo "read [$app:$name] provided by container <$container>"
            command="env MIAOU_BASEDIR=$MIAOU_BASEDIR MIAOU_CONFIGDIR=$MIAOU_CONFIGDIR $recipe --port $port --container $container --name $name --fqdn $fqdn --domain $domain --subdomain $subdomain"
            [[ "$data" != "null" ]] && command+=" --data \"$data\""
            if ! eval "$command -r" >/dev/null; then
                echoinfo "CREATE RECIPE"

                # echo "$command -c"
                eval "$command -c"
                echoinfo "CREATE RECIPE: OK"
            fi
        else
            echowarn "for container <$container>, crud recipe [$app] not found!"
        fi
    done
}

### MAIN

. "$MIAOU_BASEDIR/lib/init.sh"

readonly CONF="/etc/miaou/miaou.yaml"
readonly DEFAULTS="/etc/miaou/defaults.yaml"
readonly EXPANDED_CONF="$MIAOU_CONFIGDIR/miaou.expanded.yaml"
readonly BRIDGE="lxdbr0"
readonly MAX_WAIT=5 # timeout in seconds

# shellcheck disable=SC2034
declare -a options=("$@")

FORCE=false
if containsElement options "-f" || containsElement options "--force"; then
    FORCE=true
fi

if containsElement options "history"; then
    echo "TODO: HISTORY"
    exit 0
fi

if containsElement options "config"; then
    editor /etc/miaou/miaou.yaml
    if diff -q /etc/miaou/miaou.yaml "$HOME"/.config/miaou/archived/miaou.yaml/previous; then
        exit 0
    fi
fi

if check_expand_conf; then
    archive_allconf
    expand_conf
    check_resolv_conf
    build_nftables
    prepare_containers
    DMZ_CONTAINER=$(get_unique_container_dmz)
    build_dmz_reverseproxy
    DMZ_IP=$(get_dmz_ip)
    build_services
    build_dmz_certbot
    build_routes
    build_monit
else
    monit_show
fi
