#!/bin/bash

# Multi-instance support.
#
# Some apps are worth running more than once on a single box — e.g. two
# WordPress/Bookstack sites, or a "family" and a "work" Nextcloud kept in
# separate trust/blast-radius/backup domains. Internal multi-tenancy answers
# logical separation; this answers instance-level isolation (independent data,
# version cadence, admin, restore granularity).
#
# The model: an instance is just another app. It gets its own slug
# (<type>_<id>), its own CFG_<SLUG>_* namespace, its own deployed dir, DB row,
# IP/port allocation, subdomain and backups — so the entire downstream pipeline
# (scan, install, services, routing, updater, backups) treats it like any other
# app with ZERO changes. Everything instance-specific happens here, on a cloned
# copy of the type's template, leaving the shipped template and the core engine
# untouched.
#
# Only apps that opt in via CFG_<TYPE>_MULTI_INSTANCE=true can be instanced;
# structurally-singleton apps (Traefik, DNS, VPN, the *arr stack, LibrePortal
# itself) never get the flag.

# Read a CFG_<TYPE>_<KEY> value straight from a type's template config, without
# relying on it already being sourced.
instanceTypeCfg() {
    local type="$1" key="$2"
    local cfg="${install_containers_dir%/}/$type/$type.config"
    [[ -f "$cfg" ]] || return 1
    local line
    line=$(grep -E "^CFG_${type^^}_${key}=" "$cfg" | head -n1)
    [[ -z "$line" ]] && return 1
    line="${line#*=}"
    line="${line//$'\r'/}"
    line="${line#\"}"
    line="${line%\"}"
    printf '%s' "$line"
}

# Turn a user-supplied instance name into the <id> half of the slug. App configs
# are SOURCED, so the slug (uppercased) must be a valid shell identifier — that
# means [a-z0-9] only (underscores are fine, hyphens are not). Hostname-safety is
# handled separately by the subdomain, which may contain hyphens.
instanceIdPart() {
    local raw="${1,,}"
    raw="${raw//[^a-z0-9]/}"
    printf '%s' "$raw"
}

# Upsert a single CFG line in a config file (append if absent, else update).
_instanceSetCfg() {
    local key="$1" val="$2" file="$3"
    if grep -qE "^${key}=" "$file"; then
        updateConfigOption "$key" "$val" "$file" >/dev/null
    else
        echo "${key}=\"${val}\"" >> "$file"
    fi
}

# Rewrite field 10 (the subdomain column) of the instance's primary webui port so
# the instance routes to its own host instead of inheriting the type's. Empty
# subdomain would otherwise resolve to <slug>.<domain> — and the slug carries an
# underscore, which isn't valid in a hostname.
_instanceSetSubdomain() {
    local slug_u="$1" subdomain="$2" file="$3"
    local key="CFG_${slug_u}_PORT_1"
    local line
    line=$(grep -E "^${key}=" "$file" | head -n1)
    [[ -z "$line" ]] && return 0
    local val="${line#*=}"
    val="${val//$'\r'/}"
    val="${val#\"}"
    val="${val%\"}"
    local IFS='|'
    local -a f=($val)
    while [[ ${#f[@]} -lt 11 ]]; do f+=(""); done
    f[10]="$subdomain"
    local newval="${f[*]}"
    updateConfigOption "$key" "$newval" "$file" >/dev/null
}

# Rewrite identity-bearing tokens in the cloned compose so the instance's
# containers, Traefik routers and backup labels are unique. image: lines are
# deliberately left untouched (rule 2/3 anchor on their line prefix; the
# *-service / *_db tokens never appear in an image path).
_instanceRewriteCompose() {
    local type="$1" slug="$2" dir="$3"
    local f="$dir/docker-compose.yml"
    [[ -f "$f" ]] || return 0
    # 1. Traefik router/service names (<type>-service) and the db container/host
    #    (<type>_db) — these tokens are unambiguous, rewrite everywhere.
    sed -i -E "s/\b${type}-service\b/${slug}-service/g; s/\b${type}_db\b/${slug}_db/g" "$f"
    # 2. The standalone app container (container_name: <type>) — anchored so the
    #    image: line ending in <type> is never touched.
    sed -i -E "s/(container_name:[[:space:]]*)${type}\b/\1${slug}/g" "$f"
    # 3. The files-backup label's container ref (libreportal.backup.files: "<type>:/...").
    sed -i -E "s/(libreportal\.backup\.files:[[:space:]]*\")${type}\b/\1${slug}/g" "$f"
}

# Clone + prefix-rename the per-app tools/scripts so an instance's helpers target
# its own container and don't collide (by function name) with the type's. This is
# best-effort: it keeps the tool tree internally consistent, but apps with unusual
# tool wiring may need review before their flag is flipped.
_instanceRewriteTools() {
    local type="$1" slug="$2" dir="$3"
    local d f base
    for d in "$dir/tools" "$dir/scripts"; do
        [[ -d "$d" ]] || continue
        for f in "$d/${type}_"*.sh "$d/${type}.tools.json"; do
            [[ -e "$f" ]] || continue
            base="$(basename "$f")"
            mv "$f" "$d/${base/#${type}/${slug}}"
        done
        for f in "$d"/*.sh "$d"/*.json; do
            [[ -e "$f" ]] || continue
            # Uniform lowercase-prefix rename keeps file names, function defs and
            # tools.json ids consistent; then fix container-exec + config refs.
            sed -i -E "s/\b${type}_/${slug}_/g" "$f"
            sed -i -E "s/(docker[[:space:]]+(exec|logs|restart|stop|start|inspect)[[:space:]]+)${type}\b/\1${slug}/g" "$f"
            sed -i -E "s/\b${type}\.config\b/${slug}.config/g" "$f"
        done
    done
}

# Provision and install a new instance of a multi-instance-capable app.
#   instanceCreate <type> <name> [domain_index] [subdomain]
instanceCreate() {
    local type="$1" rawname="$2" domain_idx="$3" subdomain="$4"

    local type_dir="${install_containers_dir%/}/$type"
    if [[ -z "$type" || ! -d "$type_dir" || ! -f "$type_dir/$type.config" ]]; then
        isError "Instance create: unknown app type '$type'."
        return 1
    fi

    local capable
    capable=$(instanceTypeCfg "$type" "MULTI_INSTANCE")
    if [[ "$capable" != "true" ]]; then
        isError "App type '$type' is not multi-instance-capable. Set CFG_${type^^}_MULTI_INSTANCE=true on a reviewed app to allow it."
        return 1
    fi

    local id
    id=$(instanceIdPart "$rawname")
    if [[ -z "$id" ]]; then
        isError "Instance create: '$rawname' has no usable letters/digits for an instance name."
        return 1
    fi

    local slug="${type}_${id}"
    local slug_u="${slug^^}"
    if [[ -d "${install_containers_dir%/}/$slug" || -d "${containers_dir%/}/$slug" ]]; then
        isError "An app or instance named '$slug' already exists. Pick a different name."
        return 1
    fi

    # Default the host to a hyphen-safe form of the slug; let the caller override.
    [[ -z "$subdomain" ]] && subdomain="${slug//_/-}"

    isNotice "Creating new '$type' instance '$id' (slug: $slug, host: ${subdomain}.<domain>)"

    # 1. Clone the type's template tree into a new instance template.
    local inst_dir="${install_containers_dir%/}/$slug"
    cp -r "$type_dir" "$inst_dir"
    if [[ ! -d "$inst_dir" ]]; then
        isError "Instance create: failed to clone template for '$slug'."
        return 1
    fi

    # 2. Rename the files that are keyed by the type slug.
    [[ -f "$inst_dir/$type.config" ]] && mv "$inst_dir/$type.config" "$inst_dir/$slug.config"
    [[ -f "$inst_dir/$type.svg" ]]    && cp "$inst_dir/$type.svg" "$inst_dir/$slug.svg"
    [[ -f "$inst_dir/$type.png" ]]    && cp "$inst_dir/$type.png" "$inst_dir/$slug.png"

    local cfg="$inst_dir/$slug.config"

    # 3. Re-namespace the config (CFG_<TYPE>_* -> CFG_<SLUG>_*) then stamp the
    #    instance metadata. Secrets keep their RANDOMIZED* placeholders so the
    #    install-time scanner mints fresh ones — instances never share secrets.
    sed -i -E "s/CFG_${type^^}_/CFG_${slug_u}_/g" "$cfg"

    local type_title
    type_title=$(instanceTypeCfg "$type" "TITLE")
    [[ -z "$type_title" ]] && type_title="$type"

    _instanceSetCfg "CFG_${slug_u}_INSTANCE_OF"    "$type"                  "$cfg"
    _instanceSetCfg "CFG_${slug_u}_MULTI_INSTANCE" "false"                  "$cfg"
    _instanceSetCfg "CFG_${slug_u}_TITLE"          "${type_title} · ${id}"  "$cfg"
    [[ -n "$domain_idx" ]] && _instanceSetCfg "CFG_${slug_u}_DOMAIN" "$domain_idx" "$cfg"
    _instanceSetSubdomain "$slug_u" "$subdomain" "$cfg"

    # 4. Make the cloned compose + tools target the instance's own identity.
    _instanceRewriteCompose "$type" "$slug" "$inst_dir"
    _instanceRewriteTools   "$type" "$slug" "$inst_dir"

    isSuccessful "Instance template ready: $slug (instance of $type)"

    # 5. Hand off to the standard installer — from here it's just another app.
    if ! declare -F dockerInstallApp >/dev/null 2>&1; then
        isError "dockerInstallApp unavailable; instance template created but not installed."
        return 1
    fi
    dockerInstallApp "$slug" "" "false"
}

# List instances, optionally filtered to one type. An instance is any app whose
# config declares CFG_<SLUG>_INSTANCE_OF.
instanceList() {
    local want_type="$1"
    local dir folder slug instance_of
    for dir in "${install_containers_dir%/}"/*/; do
        folder="$(basename "$dir")"
        [[ -f "$dir/$folder.config" ]] || continue
        instance_of=$(grep -E "^CFG_${folder^^}_INSTANCE_OF=" "$dir/$folder.config" 2>/dev/null | head -n1)
        [[ -z "$instance_of" ]] && continue
        instance_of="${instance_of#*=}"; instance_of="${instance_of//\"/}"; instance_of="${instance_of//$'\r'/}"
        [[ -n "$want_type" && "$instance_of" != "$want_type" ]] && continue
        echo "$folder (instance of $instance_of)"
    done
}

# Remove an instance: standard uninstall (deployed dir + DB + compose down) then
# drop the instance's template clone. Refuses to touch a non-instance app.
instanceRemove() {
    local slug="$1"
    local cfg="${install_containers_dir%/}/$slug/$slug.config"
    if [[ ! -f "$cfg" ]]; then
        isError "Instance remove: no such instance '$slug'."
        return 1
    fi
    if ! grep -qE "^CFG_${slug^^}_INSTANCE_OF=" "$cfg"; then
        isError "'$slug' is a base app, not an instance — uninstall it via 'libreportal app uninstall $slug'."
        return 1
    fi
    if declare -F dockerUninstallApp >/dev/null 2>&1; then
        dockerUninstallApp "$slug" "false" "false"
    fi
    rm -rf "${install_containers_dir%/}/$slug"
    isSuccessful "Removed instance '$slug'."
}
