#!/bin/bash
#
# munbyn-thermal-setup - set up CUPS print queues for Munbyn ITPP047/ITPP098
# thermal receipt printers.
#
# Network first (WLAN/LAN via raw port 9100), USB as fallback.
# The printer does not advertise itself via mDNS/Bonjour, so network
# discovery actively scans the local subnet for ESC/POS raw printers and,
# after setup, announces the printer via Avahi so it also shows up in the
# desktop "Add Printer" dialogs.
#
# SPDX-License-Identifier: MIT
# Copyright (C) 2026 Christian Kvasny <chris@ckvsoft.at>

set -u

# CUPS tools live in /usr/sbin, which is not always in a user's PATH.
export PATH="$PATH:/usr/sbin:/sbin"

PPD_PATH="/usr/share/cups/model/Munbyn/munbyn-itpp047.ppd"
MDNS_CONF="/etc/munbyn-thermal/mdns.conf"
DEFAULT_QUEUE="Munbyn-ITPP047"
DEFAULT_PORT=9100

QUIET=0
log()  { [ "$QUIET" -eq 1 ] || echo "$@"; }
warn() { echo "$@" >&2; }

usage() {
    cat <<EOF
Usage: munbyn-thermal-setup [OPTION]...

Set up a CUPS queue for a Munbyn ITPP047/ITPP098 thermal receipt printer.

Options:
  --scan                 Discover printers (mDNS + active ESC/POS LAN scan).
  --auto                 Pick the single ESC/POS printer found in the LAN and
                         set up a queue for it.
  --add-ip IP            Add a queue for a network printer at IP (raw port 9100).
  --port N               Raw port for --add-ip (default: $DEFAULT_PORT).
  --add-uri URI          Add a queue for an explicit CUPS device URI.
  --usb                  Add a queue for the first USB Munbyn/POS printer found.
  --name NAME            Queue name (default: $DEFAULT_QUEUE).
  --no-mdns              Do not announce the printer via mDNS/Avahi.
  --default [NAME]       Set the (given or last created) queue as default.
  --remove NAME          Delete an existing queue and stop the mDNS announce.
  --list                 List queues that use the Munbyn PPD.
  --quiet                Reduce output (used by the udev helper).
  -h, --help             Show this help.

Examples:
  munbyn-thermal-setup --scan
  sudo munbyn-thermal-setup --auto
  sudo munbyn-thermal-setup --add-ip 10.1.1.31 --name Kassa-Bon --default
  sudo munbyn-thermal-setup --usb
EOF
}

need_root() {
    if [ "$(id -u)" -ne 0 ] && ! id -nG 2>/dev/null | tr ' ' '\n' | grep -qx lpadmin; then
        warn "This action requires root or membership in the 'lpadmin' group."
        exit 1
    fi
}

ppd_arg() {
    if [ -f "$PPD_PATH" ]; then
        printf '%s' "$PPD_PATH"
    else
        warn "PPD not found: $PPD_PATH (is munbyn-thermal installed?)"
        exit 1
    fi
}

# --- Discovery ------------------------------------------------------------

# Return the local IPv4 /24 prefix (e.g. 10.1.1) of the default interface.
local_prefix() {
    local addr
    addr=$(ip -o -4 addr show scope global 2>/dev/null \
           | awk '$4 ~ /\/24$/ {print $4; exit}')
    [ -n "$addr" ] || return 1
    echo "${addr%/*}" | sed 's/\.[0-9]*$//'
}

# True if host:9100 answers like an ESC/POS printer (real-time status query).
probe_escpos() {
    local ip="$1" reply
    exec 3<>"/dev/tcp/$ip/$DEFAULT_PORT" 2>/dev/null || return 1
    printf '\x10\x04\x01' >&3 2>/dev/null
    IFS= read -r -t 3 -N 1 -u 3 reply 2>/dev/null
    exec 3>&- 3<&- 2>/dev/null
    [ -n "$reply" ]
}

# Active scan of the local /24 for ESC/POS printers on port 9100.
scan_lan_escpos() {
    local prefix
    prefix=$(local_prefix) || { warn "Kein /24-Netz gefunden, überspringe LAN-Scan."; return; }
    log "== LAN-Scan ${prefix}.0/24 auf ESC/POS-Drucker (Port $DEFAULT_PORT) ==" >&2
    local ip
    for ip in $(seq 1 254); do
        ( timeout 1 bash -c "exec 3<>/dev/tcp/$prefix.$ip/$DEFAULT_PORT" 2>/dev/null \
            && echo "$prefix.$ip" ) &
    done
    wait 2>/dev/null
}

scan_mdns() {
    command -v avahi-browse >/dev/null 2>&1 || return 0
    log "== mDNS/Bonjour-Drucker =="
    local t
    for t in _pdl-datastream._tcp _ipp._tcp _printer._tcp; do
        timeout 6 avahi-browse -rtp "$t" 2>/dev/null | \
            awk -F';' '$1=="="{n=$4; gsub(/\\032/," ",n); printf "  %s  (%s)\n", n, $8}'
    done
}

scan_printers() {
    scan_mdns
    log ""
    scan_lan_escpos | while read -r ip; do
        if probe_escpos "$ip"; then
            echo "  $ip  ->  ESC/POS-Drucker (Munbyn?)  [socket://$ip:$DEFAULT_PORT]"
        else
            echo "  $ip  ->  Port $DEFAULT_PORT offen (kein ESC/POS)"
        fi
    done
}

# Print the first ESC/POS printer found in the LAN.
find_network_escpos() {
    local ip
    for ip in $(scan_lan_escpos); do
        if probe_escpos "$ip"; then
            printf '%s' "$ip"
            return 0
        fi
    done
    return 1
}

find_usb_uri() {
    command -v lpinfo >/dev/null 2>&1 || return 1
    local uri count
    uri=$(lpinfo -v 2>/dev/null | awk '$1=="direct" && $2 ~ /^usb:/ {print $2}' \
          | grep -iE 'munbyn|itpp|pos|thermal|receipt' | head -n1)
    if [ -z "$uri" ]; then
        count=$(lpinfo -v 2>/dev/null | awk '$1=="direct" && $2 ~ /^usb:/' | wc -l)
        [ "$count" -eq 1 ] && uri=$(lpinfo -v 2>/dev/null | awk '$1=="direct" && $2 ~ /^usb:/ {print $2}')
    fi
    [ -n "$uri" ] || return 1
    printf '%s' "$uri"
}

# --- mDNS announcement ----------------------------------------------------

# Announce a network printer via Avahi so it shows up in "Add Printer".
announce_mdns() {
    local uri="$1" ip host
    case "$uri" in
        socket://*) ip="${uri#socket://}"; ip="${ip%%:*}" ;;
        *) return 0 ;;
    esac

    if ! command -v avahi-publish-service >/dev/null 2>&1; then
        log "mDNS-Ankündigung übersprungen (avahi-utils nicht installiert)."
        return 0
    fi

    host="munbyn-$(echo "$ip" | tr . -).local"
    install -d -m 755 /etc/munbyn-thermal
    cat > "$MDNS_CONF" <<EOF
IP=$ip
NAME="Munbyn ITPP047"
HOST=$host
PORT=$DEFAULT_PORT
EOF

    if command -v systemctl >/dev/null 2>&1; then
        systemctl daemon-reload >/dev/null 2>&1 || true
        systemctl enable munbyn-thermal.service >/dev/null 2>&1 || true
        systemctl restart munbyn-thermal.service >/dev/null 2>&1 || true
    fi
    log "Drucker wird per mDNS angekündigt -> erscheint in 'Drucker hinzufügen'."
}

stop_mdns() {
    rm -f "$MDNS_CONF"
    if command -v systemctl >/dev/null 2>&1; then
        systemctl restart munbyn-thermal.service >/dev/null 2>&1 || true
    fi
}

# --- Queue management -----------------------------------------------------

add_queue() {
    local uri="$1" name="$2" ppd
    need_root
    ppd="$(ppd_arg)"
    log "Creating queue '$name' -> $uri"
    lpadmin -p "$name" -E -v "$uri" -P "$ppd" \
        -D "Munbyn ITPP047/ITPP098 (80 mm)" \
        -o MunbynPaperReduction=Bottom \
        -o MunbynCutAfterJob=Partial || return 1
    lpadmin -p "$name" -o printer-is-shared=false 2>/dev/null || true
    cupsenable "$name" 2>/dev/null || true
    cupsaccept "$name" 2>/dev/null || true
    LAST_QUEUE="$name"
    log "Done. Test print:  lp -d $name /usr/share/cups/data/testprint"
    return 0
}

set_default() {
    local name="${1:-${LAST_QUEUE:-$DEFAULT_QUEUE}}"
    need_root
    log "Setting default printer to '$name'"
    lpadmin -d "$name"
}

list_queues() {
    lpstat -v 2>/dev/null | grep -i "$DEFAULT_QUEUE\|munbyn\|itpp" || echo "  (none)"
}

remove_queue() {
    need_root
    lpadmin -x "$1" 2>/dev/null || true
    stop_mdns
}

# --- Argument parsing -----------------------------------------------------

ACTION=""
ARG_IP=""; ARG_URI=""; ARG_NAME="$DEFAULT_QUEUE"; ARG_PORT="$DEFAULT_PORT"; PROBE_IP=""
DO_DEFAULT=0; DO_MDNS=1; DEFAULT_NAME=""; REMOVE_NAME=""; LAST_QUEUE=""

while [ $# -gt 0 ]; do
    case "$1" in
        --scan)     ACTION="scan" ;;
        --detect)   ACTION="detect" ;;
        --probe)    ACTION="probe"; PROBE_IP="$2"; shift ;;
        --auto)     ACTION="auto" ;;
        --usb)      ACTION="usb" ;;
        --add-ip)   ACTION="uri"; ARG_IP="$2"; shift ;;
        --add-uri)  ACTION="uri"; ARG_URI="$2"; shift ;;
        --port)     ARG_PORT="$2"; shift ;;
        --name)     ARG_NAME="$2"; shift ;;
        --no-mdns)  DO_MDNS=0 ;;
        --default)  DO_DEFAULT=1
                    case "${2:-}" in ""|--*) ;; *) DEFAULT_NAME="$2"; shift ;; esac ;;
        --remove)   ACTION="remove"; REMOVE_NAME="$2"; shift ;;
        --list)     ACTION="list" ;;
        --quiet)    QUIET=1 ;;
        -h|--help)  usage; exit 0 ;;
        *)          warn "Unknown option: $1"; usage; exit 2 ;;
    esac
    shift
done

case "$ACTION" in
    scan)   scan_printers ;;
    detect) ip="$(find_network_escpos || true)"; printf '%s' "$ip"; exit 0 ;;
    probe)  if probe_escpos "$PROBE_IP"; then exit 0; else exit 1; fi ;;
    auto)
        ip="$(find_network_escpos)" || { warn "Kein ESC/POS-Drucker im LAN gefunden."; exit 1; }
        uri="socket://$ip:$DEFAULT_PORT"
        add_queue "$uri" "$ARG_NAME" || exit 1
        [ "$DO_MDNS" -eq 1 ] && announce_mdns "$uri" ;;
    usb)
        uri="$(find_usb_uri)" || { warn "No USB printer found."; exit 1; }
        add_queue "$uri" "$ARG_NAME" || exit 1 ;;
    uri)
        if [ -n "$ARG_IP" ]; then
            uri="socket://$ARG_IP:$ARG_PORT"
        else
            uri="$ARG_URI"
        fi
        [ -n "$uri" ] || { warn "No URI given."; exit 2; }
        add_queue "$uri" "$ARG_NAME" || exit 1
        [ "$DO_MDNS" -eq 1 ] && announce_mdns "$uri" ;;
    remove) remove_queue "$REMOVE_NAME" ;;
    list)   list_queues ;;
    "")     usage; exit 0 ;;
esac

if [ "$DO_DEFAULT" -eq 1 ]; then
    set_default "$DEFAULT_NAME"
fi

exit 0
