# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: Copyright 2025 SUSE LLC
# SPDX-FileCopyrightText: Copyright 2025 Tobias Görgens

tik_prompt_passphrase() {
    # Prompt the user for a passphrase
    local title="$1"
    local cancel_label="${2:-Cancel}"
    local passphrase=""

    if $gui; then
        passphrase="$(zenity --password --title="${title}" --cancel-label="${cancel_label}")" || true
    else
        cenity result --password --title="${title}" --cancel-label="${cancel_label}" || true
        passphrase="${result}"
    fi

    echo -n "${passphrase}"
}

tik_mount_prepare() {
    TIK_ROOT_MNT="${TIK_ROOT_MNT:=/var/lib/tik/root}"
    export TIK_ROOT_MNT
    prun /usr/bin/mkdir -p "${TIK_ROOT_MNT}"

    TIK_MOUNTED_POINTS=""
    TIK_MOUNTED_TARGET=0
    export TIK_MOUNTED_TARGET

    TIK_TARGET_FSTAB=""
    export TIK_TARGET_FSTAB
    TIK_ASSEMBLED_FSTAB=""
    export TIK_ASSEMBLED_FSTAB

    TIK_ESP_PART=""
    export TIK_ESP_PART

    TIK_OPENED_MAPPER=""
    export TIK_OPENED_MAPPER
}

tik_is_root_partition() {
    local dev="$1"
    local fs

    [ -n "${dev}" ] || return 1

    for fs in btrfs ext4 xfs f2fs; do
        probe_partitions "${dev}" "${fs}" "/etc/fstab"
        if [ -n "${probedpart}" ]; then
            echo "${probedpart}"
            return 0
        fi
        probe_partitions "${dev}" "${fs}" "/etc/os-release"
        if [ -n "${probedpart}" ]; then
            echo "${probedpart}"
            return 0
        fi
    done
    return 1
}

tik_crypt_open() {
    # $1 = mapper override
    # $2 = mode: "required" | "optional"
    local mapper_override="$1"
    local mode="${2:-required}"
    local crypt_part="$3"

    TIK_CRYPT_PART=""
    TIK_ROOT_DEV=""

    [ -n "${crypt_part}" ] || return 0

    local crypt_byid="/dev/disk/by-id/${crypt_part}"
    local crypt_real=""

    if [ -e "${crypt_byid}" ]; then
        crypt_real="$(/usr/bin/readlink -f "${crypt_byid}")"
    else
        crypt_real="$(/usr/bin/readlink -f "/dev/disk/by-id/${crypt_part}")"
    fi

    [ -n "${crypt_real}" ] || crypt_real="${crypt_byid}"

    TIK_CRYPT_PART="${crypt_real}"
    export TIK_CRYPT_PART

    local mapper_name="${mapper_override:-${TIK_CRYPT_MAPPER:-cr_root}}"

    log "[tik_crypt_open] encrypted partition detected: ${TIK_CRYPT_PART} (mapper=${mapper_name})"

    if [ -e "/dev/mapper/${mapper_name}" ]; then
        # Mapper already exists. Only reuse it if it maps to the partition we are trying to open.
        local mapped_dev=""
        mapped_dev="$(/usr/sbin/cryptsetup status "${mapper_name}" 2>/dev/null | /usr/bin/awk -F': *' '$1=="device"{print $2; exit}')"

        if [ -n "${mapped_dev}" ]; then
            mapped_dev="$(/usr/bin/readlink -f "${mapped_dev}")"
        fi

        if [ -n "${mapped_dev}" ] && [ "${mapped_dev}" = "${TIK_CRYPT_PART}" ]; then
            log "[tik_crypt_open] mapper ${mapper_name} already open for ${mapped_dev}, reusing"
            TIK_ROOT_DEV="/dev/mapper/${mapper_name}"
            export TIK_ROOT_DEV
            TIK_OPENED_MAPPER="${mapper_name}"
            export TIK_OPENED_MAPPER
            return 0
        fi

        error "Mapper <tt>${mapper_name}</tt> is already open for <tt>${mapped_dev:-unknown}</tt>, cannot open <tt>${TIK_CRYPT_PART}</tt>."
    fi

    if [ -n "${tik_keyfile}" ] && [ -f "${tik_keyfile}" ]; then
        prun /usr/sbin/cryptsetup luksOpen --key-file="${tik_keyfile}" "${TIK_CRYPT_PART}" "${mapper_name}"
        TIK_ROOT_DEV="/dev/mapper/${mapper_name}"
        export TIK_ROOT_DEV
        TIK_OPENED_MAPPER="${mapper_name}"
        export TIK_OPENED_MAPPER
        return 0
    fi

    while true; do
        local pw
        local cancel_label="Cancel"

        if [ "${mode}" = "optional" ]; then
            cancel_label="Skip"
        fi

        pw="$(tik_prompt_passphrase "Encrypted partition (${crypt_part}) detected" "${cancel_label}")"

        if [ -z "${pw}" ]; then
            if [ "${mode}" = "optional" ]; then
                log "[tik_crypt_open] user skipped unlocking"
                return 0
            fi
            error "Encrypted system detected but no passphrase provided"
        fi

        echo -n "${pw}" | prun /usr/sbin/cryptsetup luksOpen "${crypt_byid}" "${mapper_name}"
        if [ "${retval}" = "0" ]; then
            TIK_ROOT_DEV="/dev/mapper/${mapper_name}"
            export TIK_ROOT_DEV
            TIK_OPENED_MAPPER="${mapper_name}"
            export TIK_OPENED_MAPPER
            return 0
        fi

        d --warning --no-wrap --title="Incorrect passphrase" --text="Failed to unlock encrypted partition ${crypt_part}."
    done
}

tik_mountopts_filter() {
    local opts="$1"
    local ignore_list="$2"

    if [ -z "${opts}" ] || [ "${opts}" = "-" ]; then
        echo "${opts}"
        return 0
    fi

    # Build ignore tables from ignore_list
    local -A drop_exact=()
    local -A drop_key=()
    local ig

    IFS=',' read -r -a _ignore_arr <<< "${ignore_list}"
    for ig in "${_ignore_arr[@]}"; do
        ig="${ig#"${ig%%[![:space:]]*}"}"
        ig="${ig%"${ig##*[![:space:]]}"}"
        [ -n "${ig}" ] || continue

        drop_exact["${ig}"]=1

        # If ignore entry is "key=" drop any key=...
        if [[ "${ig}" == *"=" ]]; then
            drop_key["${ig%=}"]=1
        # If ignore entry is "key=value" also mark key for dropping
        elif [[ "${ig}" == *"="* ]]; then
            drop_key["${ig%%=*}"]=1
        else
            drop_key["${ig}"]=1
        fi
    done

    local out=()
    local o k
    IFS=',' read -r -a _opts_arr <<< "${opts}"
    for o in "${_opts_arr[@]}"; do
        o="${o#"${o%%[![:space:]]*}"}"
        o="${o%"${o##*[![:space:]]}"}"
        [ -n "${o}" ] || continue

        # Exact ignore match
        if [ -n "${drop_exact[${o}]}" ]; then
            continue
        fi

        # key=value case
        if [[ "${o}" == *"="* ]]; then
            k="${o%%=*}"
            if [ -n "${drop_key[${k}]}" ] || [ -n "${drop_exact[${k}=]}" ]; then
                continue
            fi
        else
            # plain key case
            if [ -n "${drop_key[${o}]}" ]; then
                continue
            fi
        fi

        out+=("${o}")
    done

    if [ "${#out[@]}" -eq 0 ]; then
        echo "-"
    else
        local joined
        (IFS=','; joined="${out[*]}"; echo "${joined}")
    fi
}

tik_mountopts_ignore_list() {
    echo "${TIK_MOUNTOPTS_IGNORE_LIST:-ro,ro=vfs}"
}

tik_mountopts_apply_filter() {
    local opts="$1"
    local ignore_opts
    ignore_opts="$(tik_mountopts_ignore_list)"
    tik_mountopts_filter "${opts}" "${ignore_opts}"
}

tik_fstab_get_mount_opts() {
    local fstab="$1"
    local mountpoint="$2"
    local line spec mp fstype opts rest

    [ -f "${fstab}" ] || return 1

    while IFS= read -r line; do
        # strip leading whitespace
        line="${line#"${line%%[![:space:]]*}"}"
        [ -n "${line}" ] || continue
        [[ "${line}" = \#* ]] && continue

        # split into fields
        read -r spec mp fstype opts rest <<< "${line}"
        [ -n "${spec}" ] || continue
        [ -n "${mp}" ] || continue

        if [ "${mp}" = "${mountpoint}" ]; then
            echo "${opts}"
            return 0
        fi
    done < "${fstab}"

    return 1
}

tik_fstab_read_entry() {
    # Reads a single fstab-like line and outputs 4 fields via globals:
    #   TIK_FSTAB_SPEC, TIK_FSTAB_MP, TIK_FSTAB_FSTYPE, TIK_FSTAB_OPTS
    # Returns:
    #   0 = parsed entry
    #   1 = skip (blank/comment/invalid)
    local line="$1"
    local rest

    TIK_FSTAB_SPEC=""
    TIK_FSTAB_MP=""
    TIK_FSTAB_FSTYPE=""
    TIK_FSTAB_OPTS=""

    line="${line#"${line%%[![:space:]]*}"}"
    [ -n "${line}" ] || return 1
    [[ "${line}" = \#* ]] && return 1

    read -r TIK_FSTAB_SPEC TIK_FSTAB_MP TIK_FSTAB_FSTYPE TIK_FSTAB_OPTS rest <<< "${line}"

    [ -n "${TIK_FSTAB_SPEC}" ] || return 1
    [ -n "${TIK_FSTAB_MP}" ] || return 1
    [ -n "${TIK_FSTAB_FSTYPE}" ] || return 1

    return 0
}

tik_mount_root() {
    local rootdev=$1
    local mnt=$2

    # Temporarily mount the root to determine the real mount options
    local tmp_mnt
    local root_opts
    tmp_mnt="$(prun /usr/bin/mktemp -d /tmp/tik-rootprobe.XXXXXXXXXX)"
    log "[tik_mount_root] probing mount options for / by temporarily mounting ${rootdev} on ${tmp_mnt}"

    prun /usr/bin/mount -o ro "${rootdev}" "${tmp_mnt}"

    if [ -f "${tmp_mnt}/etc/fstab" ]; then
        root_opts="$(tik_fstab_get_mount_opts "${tmp_mnt}/etc/fstab" "/")"
        log "[tik_mount_root] probed root mount options: '${root_opts}'"
    else
        log "[tik_mount_root] no ${tmp_mnt}/etc/fstab found while probing, falling back to default mount options"
        root_opts=""
    fi

    prun-opt /usr/bin/umount "${tmp_mnt}"
    prun-opt /usr/bin/rmdir "${tmp_mnt}"

    if [ -n "${root_opts}" ] && [ "${root_opts}" != "-" ]; then
        root_opts="$(tik_mountopts_apply_filter "${root_opts}")"
        log "[tik_mount_root] filtered root mount options: '${root_opts}'"
    fi

    if [ -n "${root_opts}" ] && [ "${root_opts}" != "-" ]; then
        tik_mount "${rootdev}" "${mnt}" "${root_opts}"
    else
        tik_mount "${rootdev}" "${mnt}"
    fi
}

tik_is_safe_mount() {
    local spec=$1
    local mp=$2
    local fstype=$3

    # Require absolute mountpoints
    if [ -z "${mp}" ] || [ "${mp#"/"}" = "${mp}" ]; then
        return 1
    fi

    # Basic path traversal guard
    if echo "${mp}" | grep -qE '(^|/)\.\.($|/)'; then
        return 1
    fi

    # Do not allow overriding the root mount or pseudo filesystems
    case "${mp}" in
        "/"|"/proc"|"/sys"|"/dev"|"/run"|"/tmp")
            return 1
            ;;
    esac

    return 0
}

tik_rewrite_overlay_opts() {
    local opts="$1"
    local root="$2"

    opts="$(echo "$opts" | sed -E "s#lowerdir=/#lowerdir=${root}/#g; s#upperdir=/#upperdir=${root}/#g; s#workdir=/#workdir=${root}/#g")"
    opts="$(echo "$opts" | sed -E "s#lowerdir=${root}//#lowerdir=${root}/#g; s#upperdir=${root}//#upperdir=${root}/#g; s#workdir=${root}//#workdir=${root}/#g")"
    opts="$(echo "$opts" | sed -E "s#lowerdir=(${root}/[^,]*):/#lowerdir=\1:${root}/#g")"

    echo "$opts"
}

tik_is_opt_set() {
    echo ",$1," | grep -q ",$2,"
}

tik_override_mountpoint() {
    local fstab=$1
    local mp=$2
    local newline=$3

    local tmp
    tmp="$(prun /usr/bin/mktemp /tmp/tik-fstab.XXXXXXXXXX)"

    local replaced=0
    local line spec cur_mp rest

    {
        while IFS= read -r line || [ -n "${line}" ]; do
            # Keep comments as-is
            if [[ "${line}" =~ ^[[:space:]]*# ]]; then
                printf '%s\n' "${line}"
                continue
            fi

            # If we can't read at least 2 fields, keep as-is
            spec=""
            cur_mp=""
            rest=""
            read -r spec cur_mp rest <<< "${line}"
            if [ -z "${spec}" ] || [ -z "${cur_mp}" ]; then
                printf '%s\n' "${line}"
                continue
            fi

            # Replace first matching mountpoint
            if [ "${replaced}" -eq 0 ] && [ "${cur_mp}" = "${mp}" ]; then
                printf '%s\n' "${newline}"
                replaced=1
                continue
            fi

            printf '%s\n' "${line}"
        done < <(prun /usr/bin/cat "${fstab}")

        # If not replaced, append new entry
        if [ "${replaced}" -eq 0 ]; then
            printf '%s\n' "${newline}"
        fi
    } | prun /usr/bin/tee "${tmp}" >/dev/null

    prun /usr/bin/cp -a "${tmp}" "${fstab}"
    prun /usr/bin/rm -f "${tmp}"
}

tik_can_write_etc() {
    local mnt=$1
    local probe="${mnt}/etc/.tik-write-probe.$$"
    prun-opt /usr/bin/sh -c "touch '${probe}' && rm -f '${probe}'"
    [ "${retval}" = "0" ]
}

tik_fstab_assemble() {
    local mnt=$1

    local fstab="${mnt}/etc/fstab"
    local fstab_repart="${mnt}/etc/fstab.repart"
    local fstab_tik="${mnt}/etc/fstab.tik"

    local assembled
    assembled="$(prun /usr/bin/mktemp /tmp/tik-assembled-fstab.XXXXXXXXXX)"
    TIK_ASSEMBLED_FSTAB="${assembled}"
    export TIK_ASSEMBLED_FSTAB

    # Prefer systemd-repart generated fstab if present, otherwise use existing fstab.
    if [ -f "${fstab_repart}" ]; then
        log "[tik_fstab_assemble] using ${fstab_repart} as base fstab"
        prun /usr/bin/cp -a "${fstab_repart}" "${assembled}"
    elif [ -f "${fstab}" ]; then
        log "[tik_fstab_assemble] using ${fstab} as base fstab"
        prun /usr/bin/cp -a "${fstab}" "${assembled}"
    else
        log "[tik_fstab_assemble] no base fstab found, leaving assembled fstab empty at ${assembled}"
        prun /usr/bin/tee "${assembled}" >/dev/null <<EOF
# tik: no base fstab found during installation
EOF
    fi

    # Apply /etc/fstab.tik if present
    if [ -f "${fstab_tik}" ]; then
        log "[tik_fstab_assemble] found ${fstab_tik}, applying overrides into ${assembled}"

        while IFS= read -r line; do
            line="${line#"${line%%[![:space:]]*}"}"
            [ -z "${line}" ] && continue
            [[ "${line}" = \#* ]] && continue

            set -- ${line}
            local spec="$1"
            local mp="$2"
            local fstype="$3"

            [ -z "${spec}" ] && continue
            [ -z "${mp}" ] && continue
            [ -z "${fstype}" ] && continue

            if ! tik_is_safe_mount "${spec}" "${mp}" "${fstype}"; then
                log "[tik_fstab_assemble] skipping unsafe fstab.tik entry: ${line}"
                continue
            fi

            tik_override_mountpoint "${assembled}" "${mp}" "${line}"
            log "[tik_fstab_assemble] applied override/append for ${mp}"
        done < <(prun /usr/bin/cat "${fstab_tik}")
    else
        log "[tik_fstab_assemble] no ${fstab_tik} found, not applying tik overrides"
    fi

    echo "${assembled}"
}

tik_fstab_install() {
    local mnt=$1
    local assembled=$2
    local fstab="${mnt}/etc/fstab"
    local backup="${mnt}/etc/fstab.tik.orig"

    if tik_can_write_etc "${mnt}"; then
        log "[tik_fstab_install] /etc is writable, installing assembled fstab to ${fstab}"
        if [ -f "${fstab}" ] && [ ! -f "${backup}" ]; then
            prun /usr/bin/cp -a "${fstab}" "${backup}"
            log "[tik_fstab_install] backed up original fstab to ${backup}"
        fi
        prun /usr/bin/cp -a "${assembled}" "${fstab}"
        prun /usr/bin/chmod 0644 "${fstab}"
        TIK_TARGET_FSTAB="${fstab}"
        export TIK_TARGET_FSTAB
        return 0
    fi

    log "[tik_fstab_install] /etc is not writable yet"
    TIK_TARGET_FSTAB="${assembled}"
    export TIK_TARGET_FSTAB
    return 1
}

tik_mount_from_fstab() {
    local mnt=$1
    local fstab

    if [ -n "${TIK_TARGET_FSTAB}" ]; then
        fstab="${TIK_TARGET_FSTAB}"
    else
        fstab="${mnt}/etc/fstab"
    fi

    if [ ! -f "${fstab}" ]; then
        log "[tik_mount_from_fstab] no fstab at ${fstab}, skipping fstab mounts"
        return 0
    fi

    log "[tik_mount_from_fstab] reading ${fstab}"

    while IFS= read -r line; do
        if ! tik_fstab_read_entry "${line}"; then
            continue
        fi

        local spec="${TIK_FSTAB_SPEC}"
        local mp="${TIK_FSTAB_MP}"
        local fstype="${TIK_FSTAB_FSTYPE}"
        local opts="${TIK_FSTAB_OPTS}"

        [ "${mp}" = "/" ] && continue
        [ "${fstype}" = "swap" ] && continue

        case "${fstype}" in
            proc|sysfs|devtmpfs|devpts|tmpfs|cgroup|cgroup2|securityfs|efivarfs)
                continue
                ;;
        esac

        local target="${mnt}${mp}"

        if [ "${fstype}" = "none" ] && (tik_is_opt_set "${opts}" "bind" || tik_is_opt_set "${opts}" "rbind"); then
            local src="${mnt}${spec}"
            tik_mount "${src}" "${target}" "${opts}" "none"

        elif [ "${fstype}" = "overlay" ]; then
            tik_mount "overlay" "${target}" "${opts}" "overlay" "TIK_MOUNTED_POINTS" "${mnt}"

        else
            if [ -n "${opts}" ] && [ "${opts}" != "-" ]; then
                # Ignore mount options that should not be applied during installation
                local filtered_opts
                filtered_opts="$(tik_mountopts_apply_filter "${opts}")"
                if [ -n "${filtered_opts}" ] && [ "${filtered_opts}" != "-" ]; then
                    tik_mount "${spec}" "${target}" "${filtered_opts}" "${fstype}"
                else
                    tik_mount "${spec}" "${target}" "" "${fstype}"
                fi
            else
                tik_mount "${spec}" "${target}" "" "${fstype}"
            fi
        fi
    done < <(prun /usr/bin/cat "${fstab}")
}

tik_mount_pseudofs() {
    local mnt=$1

    log "[tik_mount_pseudofs] Binding pseudo filesystems"

    tik_mount "/proc" "${mnt}/proc" "" "proc"

    tik_mount "/sys" "${mnt}/sys" "bind" "none"

    prun /usr/bin/mkdir -p "${mnt}/sys/kernel/security"
    tik_mount "securityfs" "${mnt}/sys/kernel/security" "" "securityfs"

    prun /usr/bin/mkdir -p "${mnt}/sys/firmware/efi/efivars"
    tik_mount "efivarfs" "${mnt}/sys/firmware/efi/efivars" "" "efivarfs"

    tik_mount "/dev" "${mnt}/dev" "bind" "none"
    tik_mount "/run" "${mnt}/run" "bind" "none"
    tik_mount "/tmp" "${mnt}/tmp" "bind" "none"
}

tik_track_mountpoint() {
    local mp="$1"
    local listvar="${2:-TIK_MOUNTED_POINTS}"

    [ -n "${mp}" ] || return 0

    local -n _ml="${listvar}"
    _ml="${mp}
${_ml}"
    export "${listvar}"
}

tik_untrack_mountpoint() {
    local remove_prefix="$1"
    local listvar="${2:-TIK_MOUNTED_POINTS}"
    local mp
    local newlist=""

    local -n _ml="${listvar}"

    while IFS= read -r mp; do
        [ -z "${mp}" ] && continue
        case "${mp}" in
            "${remove_prefix}"|${remove_prefix}/*)
                ;;
            *)
                newlist="${newlist}${mp}
"
                ;;
        esac
    done <<< "${_ml}"

    _ml="${newlist}"
    export "${listvar}"
}

tik_monitor_progress() {
    if [ -z "${TIK_PIPE}" ]; then
        return 0
    fi
    log "[progress] Monitoring installation progress"
    (tail -f "${TIK_PIPE}") | d --progress --title="${TIK_PROGRESS_TITLE}" --auto-close --no-cancel --width=400
    log "[progress] Progress UI closed"
}

tik_prepare_progress_pipe() {
    TIK_PIPE=/tmp/tikpipe
    export TIK_PIPE
    [ -p "${TIK_PIPE}" ] || mkfifo "${TIK_PIPE}"

    tik_monitor_progress &
    TIK_PROGRESS_PID=$!

    log "[tik_prepare_progress_pipe] progress pipe ready (pid=${TIK_PROGRESS_PID})"
}
