#!/usr/bin/env bash

#########################################################################################
#### cron-lister.sh — List all cron jobs across users, system cron, and timers       ####
#### Scans user crontabs, /etc/cron.*, systemd timers, and anacron                   ####
####                                                                                 ####
#### Author: Phil Connor                                                             ####
#### Contact: contact@mylinux.work                                                   ####
#### License: MIT                                                                    ####
#### Version 1.00                                                                    ####
####                                                                                 ####
#### Usage:                                                                          ####
####   ./cron-lister.sh                                                              ####
####   ./cron-lister.sh --format raw                                                 ####
####                                                                                 ####
#### See --help for all options.                                                     ####
#########################################################################################

set -euo pipefail

# ── Defaults ──────────────────────────────────────────────────────────
FORMAT="${FORMAT:-table}"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"

# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
COUNT_USER_CRONTAB=0
COUNT_SYSTEM_CRONTAB=0
COUNT_CRON_D=0
COUNT_CRON_DIRS=0
COUNT_SYSTEMD_TIMER=0
COUNT_ANACRON=0

# ── Colors ────────────────────────────────────────────────────────────
setup_colors() {
    if [[ "$COLOR" == "never" ]]; then
        GREEN="" YELLOW="" BLUE="" MAGENTA="" CYAN="" BOLD="" DIM="" RESET=""
        return
    fi
    if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
        GREEN='\033[0;32m'
        YELLOW='\033[0;33m'
        BLUE='\033[0;34m'
        MAGENTA='\033[0;35m'
        CYAN='\033[0;36m'
        BOLD='\033[1m'
        DIM='\033[2m'
        RESET='\033[0m'
    else
        GREEN="" YELLOW="" BLUE="" MAGENTA="" CYAN="" BOLD="" DIM="" RESET=""
    fi
}

# ── Logging ───────────────────────────────────────────────────────────
log()     { echo -e "${DIM}[INFO]${RESET} $*"; }
warn()    { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${DIM}[DEBUG]${RESET} $*"; fi; }

# ── Helpers ───────────────────────────────────────────────────────────
section_header() {
    echo ""
    echo -e "  ${BOLD}${CYAN}── $1 ──${RESET}"
    echo ""
}

print_table_header() {
    printf "  ${BOLD}%-18s %-14s %-22s %s${RESET}\n" "SOURCE" "USER/UNIT" "SCHEDULE" "COMMAND"
    printf "  %s\n" "$(printf '%.0s─' {1..80})"
}

print_job() {
    local source="$1"
    local user="$2"
    local schedule="$3"
    local command="$4"

    # Truncate long commands
    if [[ ${#command} -gt 60 ]]; then
        command="${command:0:57}..."
    fi

    if [[ "$FORMAT" == "raw" ]]; then
        printf "%s\t%s\t%s\t%s\n" "$source" "$user" "$schedule" "$command"
        return
    fi

    local color
    case "$source" in
        user-crontab)   color="$GREEN" ;;
        /etc/crontab)   color="$BLUE" ;;
        /etc/cron.d/*)  color="$CYAN" ;;
        cron.hourly|cron.daily|cron.weekly|cron.monthly) color="$MAGENTA" ;;
        systemd-timer)  color="$YELLOW" ;;
        anacron)        color="$DIM" ;;
        *)              color="" ;;
    esac

    printf "  %b%-18s%b %-14s %-22s %s\n" "$color" "$source" "$RESET" "$user" "$schedule" "$command"
}

# ══════════════════════════════════════════════════════════════════════
#                       USER CRONTABS
# ══════════════════════════════════════════════════════════════════════

scan_user_crontabs() {
    section_header "User Crontabs"

    local crontab_dir="/var/spool/cron/crontabs"
    local found=false

    if [[ -d "$crontab_dir" ]] && [[ -r "$crontab_dir" ]]; then
        while IFS= read -r crontab_file; do
            [[ -z "$crontab_file" ]] && continue
            found=true
            local username
            username=$(basename "$crontab_file")
            verbose "Reading crontab for user: $username"

            while IFS= read -r line; do
                # Skip comments and empty lines
                [[ -z "$line" || "$line" == "#"* || "$line" == "SHELL="* || "$line" == "PATH="* || "$line" == "MAILTO="* ]] && continue

                local schedule cmd
                schedule=$(echo "$line" | awk '{print $1, $2, $3, $4, $5}')
                cmd=$(echo "$line" | awk '{for(i=6;i<=NF;i++) printf "%s ", $i; print ""}' | sed 's/ *$//')

                print_job "user-crontab" "$username" "$schedule" "$cmd"
                COUNT_USER_CRONTAB=$((COUNT_USER_CRONTAB + 1))
            done < "$crontab_file"
        done < <(find "$crontab_dir" -type f 2>/dev/null)
    fi

    if [[ "$found" == "false" ]]; then
        verbose "No user crontabs found in $crontab_dir"
    fi
}

# ══════════════════════════════════════════════════════════════════════
#                       SYSTEM CRONTAB
# ══════════════════════════════════════════════════════════════════════

scan_system_crontab() {
    section_header "/etc/crontab"

    if [[ ! -f /etc/crontab ]]; then
        verbose "/etc/crontab not found"
        return
    fi

    while IFS= read -r line; do
        [[ -z "$line" || "$line" == "#"* || "$line" == "SHELL="* || "$line" == "PATH="* || "$line" == "MAILTO="* || "$line" == "HOME="* ]] && continue

        local schedule user cmd
        schedule=$(echo "$line" | awk '{print $1, $2, $3, $4, $5}')
        user=$(echo "$line" | awk '{print $6}')
        cmd=$(echo "$line" | awk '{for(i=7;i<=NF;i++) printf "%s ", $i; print ""}' | sed 's/ *$//')

        if [[ -n "$cmd" ]]; then
            print_job "/etc/crontab" "$user" "$schedule" "$cmd"
            COUNT_SYSTEM_CRONTAB=$((COUNT_SYSTEM_CRONTAB + 1))
        fi
    done < /etc/crontab
}

# ══════════════════════════════════════════════════════════════════════
#                       /etc/cron.d
# ══════════════════════════════════════════════════════════════════════

scan_cron_d() {
    section_header "/etc/cron.d"

    if [[ ! -d /etc/cron.d ]]; then
        verbose "/etc/cron.d not found"
        return
    fi

    while IFS= read -r cron_file; do
        [[ -z "$cron_file" ]] && continue
        local filename
        filename=$(basename "$cron_file")

        # Skip dpkg and package manager files
        [[ "$filename" == *.dpkg-* || "$filename" == *.ucf-* || "$filename" == "." || "$filename" == ".." ]] && continue

        verbose "Reading /etc/cron.d/$filename"

        while IFS= read -r line; do
            [[ -z "$line" || "$line" == "#"* || "$line" == "SHELL="* || "$line" == "PATH="* || "$line" == "MAILTO="* || "$line" == "HOME="* ]] && continue

            local schedule user cmd
            schedule=$(echo "$line" | awk '{print $1, $2, $3, $4, $5}')
            user=$(echo "$line" | awk '{print $6}')
            cmd=$(echo "$line" | awk '{for(i=7;i<=NF;i++) printf "%s ", $i; print ""}' | sed 's/ *$//')

            if [[ -n "$cmd" ]]; then
                print_job "/etc/cron.d/$filename" "$user" "$schedule" "$cmd"
                COUNT_CRON_D=$((COUNT_CRON_D + 1))
            fi
        done < "$cron_file"
    done < <(find /etc/cron.d -maxdepth 1 -type f 2>/dev/null)
}

# ══════════════════════════════════════════════════════════════════════
#                       CRON DIRECTORIES
# ══════════════════════════════════════════════════════════════════════

scan_cron_dirs() {
    section_header "Cron Directories"

    local period
    for period in hourly daily weekly monthly; do
        local dir="/etc/cron.${period}"
        if [[ ! -d "$dir" ]]; then
            continue
        fi

        while IFS= read -r script; do
            [[ -z "$script" ]] && continue
            local script_name
            script_name=$(basename "$script")

            # Skip non-executable and package manager leftovers
            [[ "$script_name" == *.dpkg-* || "$script_name" == *.ucf-* || "$script_name" == "." || "$script_name" == ".." ]] && continue

            if [[ -x "$script" ]]; then
                print_job "cron.${period}" "root" "$period" "$script_name"
                COUNT_CRON_DIRS=$((COUNT_CRON_DIRS + 1))
            else
                verbose "Skipping non-executable: $script"
            fi
        done < <(find "$dir" -maxdepth 1 -type f 2>/dev/null)
    done
}

# ══════════════════════════════════════════════════════════════════════
#                       SYSTEMD TIMERS
# ══════════════════════════════════════════════════════════════════════

scan_systemd_timers() {
    section_header "Systemd Timers"

    if ! command -v systemctl &>/dev/null; then
        verbose "systemd not available"
        return
    fi

    systemctl list-timers --all --no-legend --no-pager 2>/dev/null | while IFS= read -r line; do
        [[ -z "$line" ]] && continue

        local unit_name schedule_info
        # Timer unit is the second-to-last field, schedule is NEXT + LEFT
        unit_name=$(echo "$line" | awk '{print $(NF-1)}')
        schedule_info=$(echo "$line" | awk '{print $1, $2, $3}')

        if [[ -n "$unit_name" && "$unit_name" != "UNIT" ]]; then
            # Get the trigger schedule from the timer unit
            local on_calendar
            on_calendar=$(systemctl show "$unit_name" --property=TimersCalendar 2>/dev/null | sed 's/TimersCalendar=//' | head -1)

            if [[ -z "$on_calendar" || "$on_calendar" == "" ]]; then
                on_calendar=$(systemctl show "$unit_name" --property=TimersMonotonic 2>/dev/null | sed 's/TimersMonotonic=//' | head -1)
            fi

            on_calendar="${on_calendar:-$schedule_info}"

            # Truncate schedule if too long
            if [[ ${#on_calendar} -gt 20 ]]; then
                on_calendar="${on_calendar:0:17}..."
            fi

            print_job "systemd-timer" "$unit_name" "$on_calendar" "$(echo "$line" | awk '{print $NF}')"
            COUNT_SYSTEMD_TIMER=$((COUNT_SYSTEMD_TIMER + 1))
        fi
    done
}

# ══════════════════════════════════════════════════════════════════════
#                       ANACRON
# ══════════════════════════════════════════════════════════════════════

scan_anacron() {
    section_header "Anacron"

    if [[ ! -f /etc/anacrontab ]]; then
        verbose "/etc/anacrontab not found"
        return
    fi

    while IFS= read -r line; do
        [[ -z "$line" || "$line" == "#"* || "$line" == "SHELL="* || "$line" == "PATH="* || "$line" == "MAILTO="* || "$line" == "HOME="* || "$line" == "START_HOURS_RANGE="* || "$line" == "RANDOM_DELAY="* ]] && continue

        local period delay ident cmd
        period=$(echo "$line" | awk '{print $1}')
        delay=$(echo "$line" | awk '{print $2}')
        ident=$(echo "$line" | awk '{print $3}')
        cmd=$(echo "$line" | awk '{for(i=4;i<=NF;i++) printf "%s ", $i; print ""}' | sed 's/ *$//')

        if [[ -n "$cmd" ]]; then
            print_job "anacron" "$ident" "every ${period}d +${delay}m" "$cmd"
            COUNT_ANACRON=$((COUNT_ANACRON + 1))
        fi
    done < /etc/anacrontab
}

# ══════════════════════════════════════════════════════════════════════
#                       SUMMARY
# ══════════════════════════════════════════════════════════════════════

print_summary() {
    local total=$((COUNT_USER_CRONTAB + COUNT_SYSTEM_CRONTAB + COUNT_CRON_D + COUNT_CRON_DIRS + COUNT_SYSTEMD_TIMER + COUNT_ANACRON))

    echo ""
    echo -e "  ${BOLD}══════════════════════════════════════════${RESET}"
    echo -e "  ${BOLD}Summary${RESET}"
    echo -e "  ${BOLD}══════════════════════════════════════════${RESET}"

    printf "  %-22s %d\n" "User crontabs:" "$COUNT_USER_CRONTAB"
    printf "  %-22s %d\n" "/etc/crontab:" "$COUNT_SYSTEM_CRONTAB"
    printf "  %-22s %d\n" "/etc/cron.d:" "$COUNT_CRON_D"
    printf "  %-22s %d\n" "cron.{h,d,w,m}:" "$COUNT_CRON_DIRS"
    printf "  %-22s %d\n" "Systemd timers:" "$COUNT_SYSTEMD_TIMER"
    printf "  %-22s %d\n" "Anacron:" "$COUNT_ANACRON"
    printf "  %s\n" "$(printf '%.0s─' {1..30})"
    printf "  ${BOLD}%-22s %d${RESET}\n" "Total:" "$total"

    echo ""
}

# ══════════════════════════════════════════════════════════════════════
#                       USAGE
# ══════════════════════════════════════════════════════════════════════

usage() {
    cat <<EOF
${SCRIPT_NAME} — List all cron jobs across users, system cron, and systemd timers

USAGE:
  ${SCRIPT_NAME} [OPTIONS]

OPTIONS:
  --format FORMAT       Output format: table (default) or raw (tab-separated)
  --verbose             Enable debug output
  --no-color            Disable colored output
  --help                Show this help

SOURCES SCANNED:
  /var/spool/cron/crontabs/   Per-user crontabs
  /etc/crontab                System crontab
  /etc/cron.d/*               Drop-in cron files
  /etc/cron.{hourly,daily,weekly,monthly}/  Periodic scripts
  systemd timers              Active systemd timer units
  /etc/anacrontab             Anacron jobs

ENVIRONMENT VARIABLES:
  FORMAT                Output format (default: table)
  COLOR                 Color mode: auto, always, never (default: auto)

EXAMPLES:
  # List all cron jobs
  sudo ./cron-lister.sh

  # Raw output for parsing
  sudo ./cron-lister.sh --format raw

  # Pipe-friendly
  sudo ./cron-lister.sh --no-color | tee /tmp/cron-audit.txt
EOF
}

# ══════════════════════════════════════════════════════════════════════
#                       ARGUMENT PARSING
# ══════════════════════════════════════════════════════════════════════

parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --format)
                FORMAT="$2"; shift 2 ;;
            --verbose)
                VERBOSE="true"; shift ;;
            --no-color)
                COLOR="never"; shift ;;
            --help|-h)
                setup_colors
                usage
                exit 0 ;;
            *)
                echo "Unknown option: $1" >&2
                echo "Run ${SCRIPT_NAME} --help for usage" >&2
                exit 1 ;;
        esac
    done

    if [[ "$FORMAT" != "table" && "$FORMAT" != "raw" ]]; then
        echo "Invalid format: $FORMAT (must be 'table' or 'raw')" >&2
        exit 1
    fi
}

# ══════════════════════════════════════════════════════════════════════
#                       MAIN
# ══════════════════════════════════════════════════════════════════════

main() {
    parse_args "$@"
    setup_colors

    if [[ "$FORMAT" != "raw" ]]; then
        echo ""
        echo -e "${BOLD}Cron Job Lister — $(hostname -f 2>/dev/null || hostname)${RESET}"
        echo -e "${DIM}$(date '+%Y-%m-%d %H:%M:%S %Z')${RESET}"
    fi

    if [[ "$FORMAT" == "table" ]]; then
        echo ""
        print_table_header
    fi

    scan_user_crontabs
    scan_system_crontab
    scan_cron_d
    scan_cron_dirs
    scan_systemd_timers
    scan_anacron

    if [[ "$FORMAT" != "raw" ]]; then
        print_summary
    fi
}

main "$@"
