#!/usr/bin/env bash
# =============================================================================
# onx-htaccess-rebuild — Rebuild a domain's .htaccess from managed sections.
#
# Purpose:
#   This is the SINGLE entry point for writing Apache per-directory directives.
#   Every customer module (IP blocker, hotlink, mod_security, redirects, mime,
#   indexes, optimize, leech, directory privacy, error pages) collapses into
#   the "sections" envelope below and is rendered into one .htaccess file in
#   a deterministic order with explicit section markers.
#
#   Always-emitted scaffold:
#     - Header banner (DO NOT EDIT MANUALLY warning)
#     - 10 named sections (even when empty — keeps grepability stable)
#     - Footer trailing `Include /.../.htaccess.local` so customers keep an
#       escape hatch for hand-rolled rules.
#
#   Every directive that depends on an Apache module is wrapped in
#   <IfModule …>…</IfModule> so a missing mod never 500's the site.
#
# Input (stdin JSON):
#   {
#     "account_id": 1,
#     "username":   "onx_acme01",
#     "domain":     "acme.com.tr",
#     "docroot":    "/home/onx_acme01/public_html",
#     "sections": {
#       "ip_blocker":          [ {ip, action} ],
#       "hotlink_protection":  { enabled, allowed_referrers, redirect_url, extensions },
#       "directory_privacy":   [ {path, users:[{username, htpasswd_hash}]} ],
#       "leech_protection":    [ {path, max_logins, window} ],
#       "error_pages":         [ {code, path} ],
#       "mod_security":        { enabled, excluded_rules },
#       "indexing":            { enabled },
#       "mime_types":          [ {directive, type, extensions} ],
#       "redirects":           [ {type, from, to, regex} ],
#       "optimization":        { gzip, browser_cache, cache_ttl_days }
#     }
#   }
#
# Output (stdout JSON):
#   {
#     "domain":                 "acme.com.tr",
#     "htaccess_path":          "/home/onx_acme01/public_html/.htaccess",
#     "size_bytes":             N,
#     "sections_written":       [...],
#     "backup_path":            "/home/…/.htaccess.onx-bak-{ts}",
#     "apache_reload_required": false
#   }
#
# Exit codes: 0=ok 1=bad-input 2=preflight 3=exec 4=rolled-back 5=rollback-fail
#
# Deployed to: /usr/local/onoxsoft/bin/onx-htaccess-rebuild
# =============================================================================

set -euo pipefail

SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=_lib/common.sh
source "${SCRIPT_DIR}/_lib/common.sh"

trap 'onx_rollback_run' ERR

require_root

onx_json_input

# ── Parse top-level fields ───────────────────────────────────────────────────
ACCOUNT_ID=$(onx_json_field "account_id" "0")
USERNAME=$(onx_json_field   "username")
DOMAIN=$(onx_json_field     "domain")
DOCROOT=$(onx_json_field    "docroot")

[[ "${ACCOUNT_ID}" =~ ^[0-9]+$ ]] && [[ "${ACCOUNT_ID}" -gt 0 ]] \
    || onx_die 1 "account_id must be positive integer"
onx_validate_username "${USERNAME}"
onx_validate_domain   "${DOMAIN}"
[[ -n "${DOCROOT}" ]] || onx_die 1 "docroot is required"

# ── Path traversal guard ─────────────────────────────────────────────────────
# realpath -m resolves symlinks even when target does not yet exist.
EXPECTED_HOME="/home/${USERNAME}"
RESOLVED_DOCROOT=$(realpath -m "${DOCROOT}" 2>/dev/null || printf '%s' "${DOCROOT}")
case "${RESOLVED_DOCROOT}" in
    "${EXPECTED_HOME}"|"${EXPECTED_HOME}"/*) : ;;
    *) onx_die 1 "docroot '${DOCROOT}' resolves outside ${EXPECTED_HOME}" ;;
esac

HTACCESS_PATH="${RESOLVED_DOCROOT}/.htaccess"
HTACCESS_LOCAL="${RESOLVED_DOCROOT}/.htaccess.local"

# ── Sections envelope ────────────────────────────────────────────────────────
SECTIONS=$(printf '%s' "${INPUT}" | jq -c '.sections // {}')

# Helper — fetch a section as a compact JSON value (object/array/null)
section_json() {
    printf '%s' "${SECTIONS}" | jq -c --arg k "$1" '(.[$k] // null)'
}

# Helper — emit a comment-line section header.
section_header() {
    printf '\n# ──── %s ────\n' "$1"
}

# ── Track which sections produced output ─────────────────────────────────────
declare -a SECTIONS_WRITTEN=()

# ── Build the new .htaccess into a tempfile ──────────────────────────────────
TMP_FILE="$(mktemp -t onx-htaccess.XXXXXX)"
chmod 0644 "${TMP_FILE}"

{
    # Header banner ----------------------------------------------------------
    printf '# ── Onoxsoft managed sections ── DO NOT EDIT MANUALLY ──\n'
    printf '# Domain:  %s\n' "${DOMAIN}"
    printf '# Account: %s (id=%s)\n' "${USERNAME}" "${ACCOUNT_ID}"
    printf '# Written: %s\n' "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
    printf '# Custom rules can be added in .htaccess.local (auto-included)\n'

    # ── 1) ip_blocker ────────────────────────────────────────────────────
    IP_JSON=$(section_json ip_blocker)
    section_header "IP_BLOCKER"
    if [[ "${IP_JSON}" != "null" ]] && [[ "$(printf '%s' "${IP_JSON}" | jq 'length')" -gt 0 ]]; then
        printf '<IfModule mod_authz_core.c>\n'
        printf '    <RequireAll>\n'
        printf '        Require all granted\n'
        printf '%s' "${IP_JSON}" | jq -r '.[] | "        " + (if .action == "allow" then "Require" else "Require not" end) + " ip " + .ip'
        printf '    </RequireAll>\n'
        printf '</IfModule>\n'
        SECTIONS_WRITTEN+=("ip_blocker")
    else
        printf '# (no rules)\n'
    fi

    # ── 2) hotlink_protection ────────────────────────────────────────────
    HL_JSON=$(section_json hotlink_protection)
    section_header "HOTLINK_PROTECTION"
    if [[ "${HL_JSON}" != "null" ]] \
        && [[ "$(printf '%s' "${HL_JSON}" | jq -r '.enabled // false')" == "true" ]]; then
        EXTS=$(printf '%s' "${HL_JSON}" | jq -r '.extensions | join("|")')
        REDIRECT=$(printf '%s' "${HL_JSON}" | jq -r '.redirect_url // ""')
        printf '<IfModule mod_rewrite.c>\n'
        printf '    RewriteEngine On\n'
        printf '    RewriteCond %%{HTTP_REFERER} !^$\n'
        # Render each allowed referrer as a RewriteCond
        printf '%s' "${HL_JSON}" | jq -r '
            .allowed_referrers[]
            | "    RewriteCond %{HTTP_REFERER} !^https?://(www\\.)?" + (. | gsub("\\."; "\\.") | gsub("^\\*\\\\\\.";"")) + "(/|$) [NC]"
        '
        if [[ -n "${REDIRECT}" ]]; then
            printf '    RewriteRule \\.(%s)$ %s [L,R,NC]\n' "${EXTS}" "${REDIRECT}"
        else
            printf '    RewriteRule \\.(%s)$ - [F,NC]\n' "${EXTS}"
        fi
        printf '</IfModule>\n'
        SECTIONS_WRITTEN+=("hotlink_protection")
    else
        printf '# (disabled)\n'
    fi

    # ── 3) directory_privacy ─────────────────────────────────────────────
    DP_JSON=$(section_json directory_privacy)
    section_header "DIRECTORY_PRIVACY"
    if [[ "${DP_JSON}" != "null" ]] && [[ "$(printf '%s' "${DP_JSON}" | jq 'length')" -gt 0 ]]; then
        DP_COUNT=$(printf '%s' "${DP_JSON}" | jq 'length')
        for (( j=0; j<DP_COUNT; j++ )); do
            ENTRY=$(printf '%s' "${DP_JSON}" | jq -c ".[$j]")
            DP_PATH=$(printf '%s' "${ENTRY}" | jq -r '.path')
            # Strip leading slash for filesystem path
            FS_PATH="${RESOLVED_DOCROOT%/}/${DP_PATH#/}"
            HTPASSWD_FILE="${RESOLVED_DOCROOT}/.htpasswds${DP_PATH%/}"
            HTPASSWD_DIR="$(dirname "${HTPASSWD_FILE}")"

            # Write/refresh the htpasswd file (idempotent — we own it)
            if [[ "${MOCK_MODE}" != "1" ]]; then
                mkdir -p "${HTPASSWD_DIR}"
                : > "${HTPASSWD_FILE}.tmp"
                chmod 0640 "${HTPASSWD_FILE}.tmp"
                printf '%s' "${ENTRY}" | jq -r '.users[] | "\(.username):\(.htpasswd_hash)"' \
                    >> "${HTPASSWD_FILE}.tmp"
                mv -f "${HTPASSWD_FILE}.tmp" "${HTPASSWD_FILE}"
                chown root:apache "${HTPASSWD_FILE}" 2>/dev/null || true
            fi

            printf '<Directory "%s">\n' "${FS_PATH}"
            printf '    AuthType Basic\n'
            printf '    AuthName "Restricted"\n'
            printf '    AuthUserFile "%s"\n' "${HTPASSWD_FILE}"
            printf '    Require valid-user\n'
            printf '</Directory>\n'
        done
        SECTIONS_WRITTEN+=("directory_privacy")
    else
        printf '# (no protected paths)\n'
    fi

    # ── 4) leech_protection ──────────────────────────────────────────────
    LP_JSON=$(section_json leech_protection)
    section_header "LEECH_PROTECTION"
    if [[ "${LP_JSON}" != "null" ]] && [[ "$(printf '%s' "${LP_JSON}" | jq 'length')" -gt 0 ]]; then
        # Apache itself has no leech-counter primitive — we set env vars
        # that mod_evasive / fail2ban filters consume.
        printf '<IfModule mod_setenvif.c>\n'
        printf '%s' "${LP_JSON}" | jq -r '.[] |
            "    SetEnvIf Request_URI \"^" + .path + "\" ONX_LEECH_PATH=" + .path + " ONX_LEECH_MAX=" + (.max_logins|tostring) + " ONX_LEECH_WIN=" + (.window|tostring)
        '
        printf '</IfModule>\n'
        SECTIONS_WRITTEN+=("leech_protection")
    else
        printf '# (no leech rules)\n'
    fi

    # ── 5) error_pages ───────────────────────────────────────────────────
    EP_JSON=$(section_json error_pages)
    section_header "ERROR_PAGES"
    if [[ "${EP_JSON}" != "null" ]] && [[ "$(printf '%s' "${EP_JSON}" | jq 'length')" -gt 0 ]]; then
        printf '%s' "${EP_JSON}" | jq -r '.[] | "ErrorDocument \(.code) \(.path)"'
        SECTIONS_WRITTEN+=("error_pages")
    else
        printf '# (defaults)\n'
    fi

    # ── 6) mod_security ──────────────────────────────────────────────────
    MS_JSON=$(section_json mod_security)
    section_header "MOD_SECURITY"
    if [[ "${MS_JSON}" != "null" ]]; then
        MS_ON=$(printf '%s' "${MS_JSON}" | jq -r '.enabled // true')
        printf '<IfModule mod_security2.c>\n'
        if [[ "${MS_ON}" == "true" ]]; then
            printf '    SecRuleEngine On\n'
        else
            printf '    SecRuleEngine Off\n'
        fi
        EXCLUDED=$(printf '%s' "${MS_JSON}" | jq -r '(.excluded_rules // []) | length')
        if [[ "${EXCLUDED}" -gt 0 ]]; then
            printf '%s' "${MS_JSON}" | jq -r '(.excluded_rules // [])[] | "    SecRuleRemoveById \(.)"'
        fi
        printf '</IfModule>\n'
        SECTIONS_WRITTEN+=("mod_security")
    else
        printf '# (engine default)\n'
    fi

    # ── 7) indexing (Options +/-Indexes) ─────────────────────────────────
    IDX_JSON=$(section_json indexing)
    section_header "INDEXING"
    if [[ "${IDX_JSON}" != "null" ]]; then
        IDX_ON=$(printf '%s' "${IDX_JSON}" | jq -r '.enabled // false')
        if [[ "${IDX_ON}" == "true" ]]; then
            printf 'Options +Indexes\n'
        else
            printf 'Options -Indexes\n'
        fi
        SECTIONS_WRITTEN+=("indexing")
    else
        printf '# (server default)\n'
    fi

    # ── 8) mime_types ────────────────────────────────────────────────────
    MT_JSON=$(section_json mime_types)
    section_header "MIME_TYPES"
    if [[ "${MT_JSON}" != "null" ]] && [[ "$(printf '%s' "${MT_JSON}" | jq 'length')" -gt 0 ]]; then
        printf '<IfModule mod_mime.c>\n'
        printf '%s' "${MT_JSON}" | jq -r '
            .[]
            | "    " + .directive + " " + .type + " " + ((.extensions // []) | join(" "))
        '
        printf '</IfModule>\n'
        SECTIONS_WRITTEN+=("mime_types")
    else
        printf '# (no overrides)\n'
    fi

    # ── 9) redirects ─────────────────────────────────────────────────────
    R_JSON=$(section_json redirects)
    section_header "REDIRECTS"
    if [[ "${R_JSON}" != "null" ]] && [[ "$(printf '%s' "${R_JSON}" | jq 'length')" -gt 0 ]]; then
        printf '<IfModule mod_rewrite.c>\n'
        printf '    RewriteEngine On\n'
        R_COUNT=$(printf '%s' "${R_JSON}" | jq 'length')
        for (( k=0; k<R_COUNT; k++ )); do
            ROW=$(printf '%s' "${R_JSON}" | jq -c ".[$k]")
            R_TYPE=$(printf '%s' "${ROW}" | jq -r '.type // "301"')
            R_FROM=$(printf '%s' "${ROW}" | jq -r '.from')
            R_TO=$(printf '%s' "${ROW}" | jq -r '.to')
            R_REGEX=$(printf '%s' "${ROW}" | jq -r '.regex // false')
            # Validate type — only the three HTTP redirect codes we care about
            case "${R_TYPE}" in
                301|302|307) : ;;
                *) onx_die 1 "redirects[${k}].type must be 301|302|307, got '${R_TYPE}'" ;;
            esac
            if [[ "${R_REGEX}" == "true" ]]; then
                printf '    RewriteRule %s %s [R=%s,L]\n' "${R_FROM}" "${R_TO}" "${R_TYPE}"
            else
                # Anchor literal paths so /old-page does not also match /old-page-foo
                printf '    RewriteRule ^%s$ %s [R=%s,L]\n' "${R_FROM#/}" "${R_TO}" "${R_TYPE}"
            fi
        done
        printf '</IfModule>\n'
        SECTIONS_WRITTEN+=("redirects")
    else
        printf '# (no redirects)\n'
    fi

    # ── 10) optimization (mod_deflate + mod_expires) ─────────────────────
    OPT_JSON=$(section_json optimization)
    section_header "OPTIMIZATION"
    if [[ "${OPT_JSON}" != "null" ]]; then
        GZIP_ON=$(printf '%s' "${OPT_JSON}" | jq -r '.gzip // false')
        CACHE_ON=$(printf '%s' "${OPT_JSON}" | jq -r '.browser_cache // false')
        TTL_DAYS=$(printf '%s' "${OPT_JSON}" | jq -r '.cache_ttl_days // 30')
        [[ "${TTL_DAYS}" =~ ^[0-9]+$ ]] || TTL_DAYS=30

        if [[ "${GZIP_ON}" == "true" ]]; then
            printf '<IfModule mod_deflate.c>\n'
            printf '    AddOutputFilterByType DEFLATE text/html text/css text/javascript text/xml text/plain\n'
            printf '    AddOutputFilterByType DEFLATE application/javascript application/json application/xml\n'
            printf '    AddOutputFilterByType DEFLATE application/xhtml+xml application/rss+xml image/svg+xml\n'
            printf '    AddOutputFilterByType DEFLATE font/woff2 font/woff font/ttf font/otf\n'
            printf '</IfModule>\n'
        fi
        if [[ "${CACHE_ON}" == "true" ]]; then
            printf '<IfModule mod_expires.c>\n'
            printf '    ExpiresActive On\n'
            printf '    ExpiresDefault "access plus %s days"\n' "${TTL_DAYS}"
            printf '    ExpiresByType text/css "access plus %s days"\n' "${TTL_DAYS}"
            printf '    ExpiresByType application/javascript "access plus %s days"\n' "${TTL_DAYS}"
            printf '    ExpiresByType image/png  "access plus %s days"\n' "${TTL_DAYS}"
            printf '    ExpiresByType image/jpeg "access plus %s days"\n' "${TTL_DAYS}"
            printf '    ExpiresByType image/webp "access plus %s days"\n' "${TTL_DAYS}"
            printf '    ExpiresByType image/svg+xml "access plus %s days"\n' "${TTL_DAYS}"
            printf '    ExpiresByType font/woff2 "access plus 1 year"\n'
            printf '</IfModule>\n'
        fi
        if [[ "${GZIP_ON}" == "true" || "${CACHE_ON}" == "true" ]]; then
            SECTIONS_WRITTEN+=("optimization")
        else
            printf '# (gzip+cache disabled)\n'
        fi
    else
        printf '# (no optimization)\n'
    fi

    # Footer: include user-managed escape hatch -----------------------------
    printf '\n# ── .htaccess.local include ──\n'
    printf '<IfFile "%s">\n' "${HTACCESS_LOCAL}"
    printf '    Include "%s"\n' "${HTACCESS_LOCAL}"
    printf '</IfFile>\n'
} > "${TMP_FILE}"

# ── Backup current .htaccess ─────────────────────────────────────────────────
TS=$(date -u +"%Y%m%dT%H%M%SZ")
BACKUP_PATH="${HTACCESS_PATH}.onx-bak-${TS}"

if [[ -e "${HTACCESS_PATH}" ]] && [[ "${MOCK_MODE}" != "1" ]]; then
    cp -p "${HTACCESS_PATH}" "${BACKUP_PATH}" \
        || onx_die 3 "failed to backup existing .htaccess to ${BACKUP_PATH}"
    onx_rollback_register "mv -f '${BACKUP_PATH}' '${HTACCESS_PATH}'"
fi

# ── Install + syntax check ───────────────────────────────────────────────────
SIZE_BYTES=0
if [[ "${MOCK_MODE}" == "1" ]]; then
    SIZE_BYTES=$(wc -c < "${TMP_FILE}" | tr -d ' ')
    rm -f "${TMP_FILE}"
else
    mv -f "${TMP_FILE}" "${HTACCESS_PATH}" \
        || onx_die 3 "failed to install ${HTACCESS_PATH}"
    chown apache:apache "${HTACCESS_PATH}" 2>/dev/null \
        || chown www-data:www-data "${HTACCESS_PATH}" 2>/dev/null \
        || true
    chmod 0644 "${HTACCESS_PATH}"
    SIZE_BYTES=$(wc -c < "${HTACCESS_PATH}" | tr -d ' ')

    # apachectl configtest — full config syntax check.
    # If it fails, ERR trap fires → rollback restores the previous .htaccess.
    if command -v apachectl >/dev/null 2>&1; then
        if ! apachectl configtest >/dev/null 2>&1; then
            onx_die 3 "apachectl configtest failed after writing ${HTACCESS_PATH}"
        fi
    fi
fi

onx_audit "onx-htaccess" "rebuild user=${USERNAME} domain=${DOMAIN} sections=${#SECTIONS_WRITTEN[@]} bytes=${SIZE_BYTES}"

# ── Output ───────────────────────────────────────────────────────────────────
SECTIONS_JSON=$(printf '%s\n' "${SECTIONS_WRITTEN[@]:-}" \
    | jq -R . \
    | jq -sc 'map(select(length > 0))')

jq -nc \
    --arg domain "${DOMAIN}" \
    --arg path   "${HTACCESS_PATH}" \
    --argjson size "${SIZE_BYTES}" \
    --argjson sections "${SECTIONS_JSON}" \
    --arg backup "${BACKUP_PATH}" \
    --argjson reload false \
    '{
        domain: $domain,
        htaccess_path: $path,
        size_bytes: $size,
        sections_written: $sections,
        backup_path: $backup,
        apache_reload_required: $reload
    }'
