#!/usr/bin/env bash
# ansipa-policy.sh — shared library for ansipa policy enforcement.
#
# Sourced by ansipa-enforce-policies.sh before any policy file is loaded.
# Provides: constants, logging, group discovery, policystore sync,
# and the _smb_parse_cred() helper (shared across usr_smb and usr_policystore).
#
# After _ansipa_discover() the following variables are exported for policy files:
#   RAW_GROUPS            (comma-sep host-group string from ipa host-show)
#   _ALL_USER_GROUPS      (newline-sep full list from one ipa group-find call)
#   ACTIVE_DAEMON_ENABLE  ACTIVE_DAEMON_DISABLE  (arrays, unit name suffixes)
#   WANT_TIMESHIFT_BACKUP  WANT_CLAMSCAN  WANT_NO_LOCAL_USERS  (booleans)
#   ACTIVE_LOCAL_SUDO_USERS  ACTIVE_SSH_USERS  ACTIVE_MON_DEV  (arrays)
#   WANT_USR_ADMIN  (boolean)
#   ACTIVE_BLOCK_BINARIES  ACTIVE_BLOCK_IPA_GROUPS  (parallel arrays)
#   ACTIVE_PRT_GROUPS  ACTIVE_PRT_PRINTERS  (parallel arrays)
#   ACTIVE_MON_USR  (array, usr_mon_* suffixes)

# ── Constants ─────────────────────────────────────────────────────────────────
STATE_DIR="/var/lib/ansipa-policies"
BLOCK_DIR="/usr/local/bin"
CRON_DIR="/etc/cron.d"
LOG_TAG="ansipa-policies"
POLICY_DIR="/usr/local/lib/ansipa/policies.d"

# ── Logging ───────────────────────────────────────────────────────────────────
log()  { echo "[$LOG_TAG] $*"; logger -t "$LOG_TAG" "$*" 2>/dev/null || true; }
warn() { echo "[$LOG_TAG][WARN] $*" >&2; logger -t "$LOG_TAG" "WARN: $*" 2>/dev/null || true; }

# ── Hostname resolution ───────────────────────────────────────────────────────
# The `hostname` binary (from inetutils) is NOT installed on a minimal Arch
# system, so `hostname -f` aborts the enforcer with "command not found". Resolve
# the FQDN without depending on it: prefer hostnamectl, then the FQDN via getent
# on the static hostname, then /etc/hostname, then the kernel hostname. Every
# enrolled host has an FQDN (ipa-client-install sets it), so this resolves.
ansipa_fqdn() {
    local h=""
    if command -v hostname &>/dev/null; then
        h=$(hostname -f 2>/dev/null) || h=""
    fi
    [[ -z "$h" ]] && h=$(hostnamectl --static 2>/dev/null || true)
    [[ -z "$h" ]] && h=$(cat /etc/hostname 2>/dev/null || true)
    [[ -z "$h" ]] && h=$(cat /proc/sys/kernel/hostname 2>/dev/null || true)
    # Upgrade a short name to the FQDN via NSS if resolvable.
    if [[ "$h" != *.* ]] && command -v getent &>/dev/null; then
        local fqdn; fqdn=$(getent hosts "$h" 2>/dev/null | awk '{for(i=2;i<=NF;i++) if($i ~ /\./){print $i; exit}}')
        [[ -n "$fqdn" ]] && h="$fqdn"
    fi
    printf '%s' "$h"
}

# ── Group discovery ───────────────────────────────────────────────────────────
# Called once by the orchestrator.  Sets all policy-state variables needed by
# every policy file, using exactly two IPA queries (one host-show, one group-find).
_ansipa_discover() {
    # ── Host groups (device policies) ─────────────────────────────────────────
    RAW_GROUPS=$(ipa host-show "$HOST_FQDN" --all 2>/dev/null \
        | grep -i "Member of host-groups:" | sed 's/.*: //' || true)

    ACTIVE_DAEMON_ENABLE=()
    ACTIVE_DAEMON_DISABLE=()
    ACTIVE_LOCAL_SUDO_USERS=()
    ACTIVE_SSH_USERS=()
    ACTIVE_MON_DEV=()
    WANT_TIMESHIFT_BACKUP=false
    WANT_CLAMSCAN=false
    WANT_NO_LOCAL_USERS=false

    if [[ -n "$RAW_GROUPS" ]]; then
        while IFS=',' read -ra GRP_ARRAY; do
            for g in "${GRP_ARRAY[@]}"; do
                g="${g// /}"
                case "$g" in
                    dev_daemon-enable-*)  ACTIVE_DAEMON_ENABLE+=("${g#dev_daemon-enable-}") ;;
                    dev_daemon-disable-*) ACTIVE_DAEMON_DISABLE+=("${g#dev_daemon-disable-}") ;;
                    dev_timeshift-backup) WANT_TIMESHIFT_BACKUP=true ;;
                    dev_no-local-users)   WANT_NO_LOCAL_USERS=true ;;
                    dev_local-sudo-*)     ACTIVE_LOCAL_SUDO_USERS+=("${g#dev_local-sudo-}") ;;
                    dev_ssh_*)            ACTIVE_SSH_USERS+=("${g#dev_ssh_}") ;;
                    # dev_mon_clamscan runs the scan + emits its own CheckMK check;
                    # match it BEFORE the generic dev_mon_* catch-all so it sets
                    # WANT_CLAMSCAN instead of being treated as a dev_mon.sh check.
                    dev_mon_clamscan)     WANT_CLAMSCAN=true ;;
                    dev_mon_*)            ACTIVE_MON_DEV+=("${g#dev_mon_}") ;;
                esac
            done
        done <<< "$RAW_GROUPS"
    fi

    # ── User groups (one call, all prefixes) ──────────────────────────────────
    _ALL_USER_GROUPS=$(ipa group-find --pkey-only 2>/dev/null \
        | awk '/Group name:/ {print $NF}' | sort -u || true)

    WANT_USR_ADMIN=false
    ACTIVE_BLOCK_BINARIES=()
    ACTIVE_BLOCK_IPA_GROUPS=()
    ACTIVE_PRT_GROUPS=()
    ACTIVE_PRT_PRINTERS=()
    ACTIVE_MON_USR=()

    while IFS= read -r _grp; do
        [[ -z "$_grp" ]] && continue
        case "$_grp" in
            usr_admin)              WANT_USR_ADMIN=true ;;
            usr_block-binary-*)
                local _raw="${_grp#usr_block-binary-}"
                ACTIVE_BLOCK_BINARIES+=("${_raw//__/.}")
                ACTIVE_BLOCK_IPA_GROUPS+=("$_grp") ;;
            usr_prt_*)
                ACTIVE_PRT_GROUPS+=("$_grp")
                ACTIVE_PRT_PRINTERS+=("${_grp#usr_prt_}") ;;
            usr_mon_*)
                ACTIVE_MON_USR+=("${_grp#usr_mon_}") ;;
        esac
    done <<< "$_ALL_USER_GROUPS"
    unset _grp _raw

    log "Device policies — daemon-enable: ${ACTIVE_DAEMON_ENABLE[*]:-none}" \
        "| daemon-disable: ${ACTIVE_DAEMON_DISABLE[*]:-none}" \
        "| timeshift-backup: $WANT_TIMESHIFT_BACKUP | clamscan: $WANT_CLAMSCAN" \
        "| no-local-users: $WANT_NO_LOCAL_USERS | local-sudo: ${ACTIVE_LOCAL_SUDO_USERS[*]:-none}" \
        "| ssh-keys: ${ACTIVE_SSH_USERS[*]:-none} | mon-dev: ${ACTIVE_MON_DEV[*]:-none}"
    log "User policies  — admin: $WANT_USR_ADMIN" \
        "| block-binary: ${ACTIVE_BLOCK_BINARIES[*]:-none}" \
        "| printers: ${ACTIVE_PRT_PRINTERS[*]:-none}" \
        "| mon-usr: ${ACTIVE_MON_USR[*]:-none}"
}

# ── SMB credential parser ─────────────────────────────────────────────────────
# Used by both usr_smb.sh and usr_policystore.sh.
# Outputs "host samba_user password" when the IPA group description contains
# a cifs:// credential (written by ansipa-smb-setup.sh).
_smb_parse_cred() {
    local _desc
    _desc=$(ipa group-show "$1" --all 2>/dev/null \
        | awk -F': ' '/Description:/{print $2; exit}' || true)
    [[ "$_desc" =~ ^cifs://([^:]+):([^:]+):(.+)$ ]] && \
        echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]} ${BASH_REMATCH[3]}"
}

# ── Policystore sync ──────────────────────────────────────────────────────────
# Called by the orchestrator after discovery.  Reads the SMB credential from the
# usr_smb_adm_policystore IPA group description, then syncs *.sh files from the
# ansipa-policystore SMB share into $POLICY_DIR.
#
# On SMB failure: warns and returns 0; the orchestrator continues with whatever
# files are already in $POLICY_DIR (Ansible-deployed or previously synced).
_ansipa_sync_policystore() {
    command -v smbclient &>/dev/null || return 0

    local _cred_line _host _suser _spass
    _cred_line=$(_smb_parse_cred "usr_smb_adm_policystore" 2>/dev/null || true)
    if [[ -z "$_cred_line" ]]; then
        return 0   # group absent or no credential yet — use local files
    fi
    read -r _host _suser _spass <<< "$_cred_line"

    local _tmp
    _tmp=$(mktemp -d)

    # Build a temporary smbclient credentials file (avoids shell-quoting issues)
    local _creds_tmp
    _creds_tmp=$(mktemp)
    printf 'username=%s\npassword=%s\ndomain=WORKGROUP\n' "$_suser" "$_spass" \
        > "$_creds_tmp"
    chmod 600 "$_creds_tmp"

    if smbclient "//${_host}/ansipa-policystore" -A "$_creds_tmp" \
        -c "cd policies.d; mask *.sh; recurse ON; prompt OFF; lcd ${_tmp}; mget *" \
        &>/dev/null
    then
        # Atomic swap: remove stale files and replace with synced set.
        rm -f "$POLICY_DIR"/*.sh 2>/dev/null || true
        cp "$_tmp"/*.sh "$POLICY_DIR"/ 2>/dev/null \
            && log "Synced policy store from //${_host}/ansipa-policystore/policies.d" \
            || warn "Policystore sync: no .sh files on share — using local files"
    else
        warn "Policystore sync failed (SMB unreachable?) — using cached local files"
    fi

    rm -rf "$_tmp"
    rm -f "$_creds_tmp"
}
