#!/usr/bin/env bash

#########################################################################################
#### ssl-cert-checker.sh — Check SSL certificate expiry across multiple endpoints    ####
#### Reads hosts from CLI args, a file, or stdin and reports days remaining           ####
####                                                                                 ####
#### Author: Phil Connor                                                             ####
#### Contact: contact@mylinux.work                                                   ####
#### License: MIT                                                                    ####
#### Version 1.00                                                                    ####
####                                                                                 ####
#### Usage:                                                                          ####
####   ./ssl-cert-checker.sh example.com google.com:443                              ####
####   ./ssl-cert-checker.sh --file hosts.txt                                        ####
####   echo "example.com" | ./ssl-cert-checker.sh                                    ####
####                                                                                 ####
#### See --help for all options.                                                     ####
#########################################################################################

set -euo pipefail

# ── Defaults ──────────────────────────────────────────────────────────
CERT_CHECK_TIMEOUT="${CERT_CHECK_TIMEOUT:-5}"
WARN_DAYS="${WARN_DAYS:-30}"
CRIT_DAYS="${CRIT_DAYS:-7}"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"
JSON_OUTPUT="${JSON_OUTPUT:-false}"
HOST_FILE=""

# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
HOSTS=()
COUNT_OK=0
COUNT_WARN=0
COUNT_CRIT=0
COUNT_EXPIRED=0
COUNT_ERROR=0
COUNT_TOTAL=0
JSON_RESULTS=()

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

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

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

field() {
    printf "  ${BOLD}%-22s${RESET} %s\n" "$1" "$2"
}

field_color() {
    printf "  ${BOLD}%-22s${RESET} %b\n" "$1" "$2"
}

# ══════════════════════════════════════════════════════════════════════
#                       CERTIFICATE CHECK
# ══════════════════════════════════════════════════════════════════════

check_cert() {
    local host="$1"
    local port="$2"

    verbose "Connecting to ${host}:${port} (timeout ${CERT_CHECK_TIMEOUT}s)"

    local cert_output
    if ! cert_output=$(openssl s_client -servername "$host" -connect "${host}:${port}" 2>/dev/null <<< "" | openssl x509 -noout -subject -issuer -dates 2>/dev/null); then
        err "Failed to retrieve certificate from ${host}:${port}"
        COUNT_ERROR=$((COUNT_ERROR + 1))
        if [[ "$JSON_OUTPUT" == "true" ]]; then
            JSON_RESULTS+=("{\"host\":\"${host}\",\"port\":${port},\"status\":\"ERROR\",\"error\":\"connection failed\"}")
        fi
        return
    fi

    local subject issuer not_after
    subject=$(echo "$cert_output" | grep "^subject=" | sed 's/^subject=//')
    issuer=$(echo "$cert_output" | grep "^issuer=" | sed 's/^issuer=//')
    not_after=$(echo "$cert_output" | grep "^notAfter=" | sed 's/^notAfter=//')

    if [[ -z "$not_after" ]]; then
        err "Could not parse certificate dates for ${host}:${port}"
        COUNT_ERROR=$((COUNT_ERROR + 1))
        return
    fi

    local expiry_epoch now_epoch days_remaining
    expiry_epoch=$(date -d "$not_after" +%s 2>/dev/null)
    now_epoch=$(date +%s)
    days_remaining=$(( (expiry_epoch - now_epoch) / 86400 ))

    local status color
    if [[ "$days_remaining" -lt 0 ]]; then
        status="EXPIRED"
        color="$RED"
        COUNT_EXPIRED=$((COUNT_EXPIRED + 1))
    elif [[ "$days_remaining" -le "$CRIT_DAYS" ]]; then
        status="CRITICAL"
        color="$RED"
        COUNT_CRIT=$((COUNT_CRIT + 1))
    elif [[ "$days_remaining" -le "$WARN_DAYS" ]]; then
        status="WARNING"
        color="$YELLOW"
        COUNT_WARN=$((COUNT_WARN + 1))
    else
        status="OK"
        color="$GREEN"
        COUNT_OK=$((COUNT_OK + 1))
    fi

    if [[ "$JSON_OUTPUT" == "true" ]]; then
        # Escape double quotes in subject/issuer for JSON
        local json_subject json_issuer
        json_subject="${subject//\"/\\\"}"
        json_issuer="${issuer//\"/\\\"}"
        JSON_RESULTS+=("{\"host\":\"${host}\",\"port\":${port},\"subject\":\"${json_subject}\",\"issuer\":\"${json_issuer}\",\"expiry\":\"${not_after}\",\"days_remaining\":${days_remaining},\"status\":\"${status}\"}")
    else
        echo ""
        field "Host:" "${host}:${port}"
        field "Subject:" "$subject"
        field "Issuer:" "$issuer"
        field "Expiry:" "$not_after"
        field_color "Days remaining:" "${color}${days_remaining}${RESET}"
        field_color "Status:" "${color}${status}${RESET}"
    fi
}

# ══════════════════════════════════════════════════════════════════════
#                       INPUT PARSING
# ══════════════════════════════════════════════════════════════════════

parse_host() {
    local entry="$1"
    local host port

    # Strip whitespace and skip empty/comment lines
    entry=$(echo "$entry" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
    [[ -z "$entry" || "$entry" == \#* ]] && return

    if [[ "$entry" == *:* ]]; then
        host="${entry%%:*}"
        port="${entry##*:}"
    else
        host="$entry"
        port="443"
    fi

    HOSTS+=("${host}:${port}")
}

load_hosts_from_file() {
    local file="$1"
    if [[ ! -f "$file" ]]; then
        err "File not found: $file"
        exit 1
    fi
    while IFS= read -r line; do
        parse_host "$line"
    done < "$file"
}

load_hosts_from_stdin() {
    while IFS= read -r line; do
        parse_host "$line"
    done
}

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

usage() {
    cat <<EOF
${SCRIPT_NAME} — Check SSL certificate expiry across multiple endpoints

USAGE:
  ${SCRIPT_NAME} [OPTIONS] [HOST[:PORT] ...]
  ${SCRIPT_NAME} --file hosts.txt
  echo "example.com" | ${SCRIPT_NAME}

OPTIONS:
  --file FILE           Read hosts from file (one host:port per line)
  --warn-days N         Warning threshold in days (default: ${WARN_DAYS})
  --crit-days N         Critical threshold in days (default: ${CRIT_DAYS})
  --json                Output results as JSON
  --verbose             Enable debug output
  --no-color            Disable colored output
  --help                Show this help

ENVIRONMENT VARIABLES:
  CERT_CHECK_TIMEOUT    Connection timeout in seconds (default: 5)
  WARN_DAYS             Warning threshold (default: 30)
  CRIT_DAYS             Critical threshold (default: 7)

EXAMPLES:
  # Check a single host
  ./ssl-cert-checker.sh example.com

  # Check multiple hosts with custom port
  ./ssl-cert-checker.sh example.com:443 mail.example.com:993

  # Check from a file
  ./ssl-cert-checker.sh --file hosts.txt

  # JSON output with custom thresholds
  ./ssl-cert-checker.sh --json --warn-days 60 --crit-days 14 example.com
EOF
}

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

parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --file)
                HOST_FILE="$2"; shift 2 ;;
            --warn-days)
                WARN_DAYS="$2"; shift 2 ;;
            --crit-days)
                CRIT_DAYS="$2"; shift 2 ;;
            --json)
                JSON_OUTPUT="true"; shift ;;
            --verbose)
                VERBOSE="true"; shift ;;
            --no-color)
                COLOR="never"; shift ;;
            --help|-h)
                setup_colors
                usage
                exit 0 ;;
            -*)
                err "Unknown option: $1"
                echo "Run ${SCRIPT_NAME} --help for usage" >&2
                exit 1 ;;
            *)
                parse_host "$1"; shift ;;
        esac
    done
}

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

main() {
    parse_args "$@"
    setup_colors

    # Require openssl
    if ! command -v openssl &>/dev/null; then
        err "openssl is required but not found"
        exit 1
    fi

    # Load hosts from file if specified
    if [[ -n "$HOST_FILE" ]]; then
        load_hosts_from_file "$HOST_FILE"
    fi

    # Load from stdin if no hosts yet and stdin is not a terminal
    if [[ ${#HOSTS[@]} -eq 0 ]] && ! [[ -t 0 ]]; then
        load_hosts_from_stdin
    fi

    if [[ ${#HOSTS[@]} -eq 0 ]]; then
        err "No hosts specified"
        echo "Run ${SCRIPT_NAME} --help for usage" >&2
        exit 1
    fi

    COUNT_TOTAL=${#HOSTS[@]}

    if [[ "$JSON_OUTPUT" != "true" ]]; then
        echo ""
        echo -e "${BOLD}SSL Certificate Check${RESET}"
        echo -e "${DIM}$(date '+%Y-%m-%d %H:%M:%S %Z')${RESET}"
        echo -e "${DIM}Thresholds: warn=${WARN_DAYS}d, crit=${CRIT_DAYS}d, timeout=${CERT_CHECK_TIMEOUT}s${RESET}"
        section_header "Certificates"
    fi

    for entry in "${HOSTS[@]}"; do
        local host port
        host="${entry%%:*}"
        port="${entry##*:}"
        check_cert "$host" "$port"
    done

    if [[ "$JSON_OUTPUT" == "true" ]]; then
        echo "{"
        echo "  \"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\","
        echo "  \"summary\": {"
        echo "    \"total\": ${COUNT_TOTAL},"
        echo "    \"ok\": ${COUNT_OK},"
        echo "    \"warning\": ${COUNT_WARN},"
        echo "    \"critical\": ${COUNT_CRIT},"
        echo "    \"expired\": ${COUNT_EXPIRED},"
        echo "    \"error\": ${COUNT_ERROR}"
        echo "  },"
        echo "  \"results\": ["
        local i=0
        for result in "${JSON_RESULTS[@]}"; do
            i=$((i + 1))
            if [[ $i -lt ${#JSON_RESULTS[@]} ]]; then
                echo "    ${result},"
            else
                echo "    ${result}"
            fi
        done
        echo "  ]"
        echo "}"
    else
        section_header "Summary"
        field "Total checked:" "$COUNT_TOTAL"
        field_color "OK:" "${GREEN}${COUNT_OK}${RESET}"
        if [[ "$COUNT_WARN" -gt 0 ]]; then
            field_color "Warning:" "${YELLOW}${COUNT_WARN}${RESET}"
        else
            field "Warning:" "$COUNT_WARN"
        fi
        if [[ "$COUNT_CRIT" -gt 0 ]]; then
            field_color "Critical:" "${RED}${COUNT_CRIT}${RESET}"
        else
            field "Critical:" "$COUNT_CRIT"
        fi
        if [[ "$COUNT_EXPIRED" -gt 0 ]]; then
            field_color "Expired:" "${RED}${COUNT_EXPIRED}${RESET}"
        else
            field "Expired:" "$COUNT_EXPIRED"
        fi
        if [[ "$COUNT_ERROR" -gt 0 ]]; then
            field_color "Errors:" "${RED}${COUNT_ERROR}${RESET}"
        else
            field "Errors:" "$COUNT_ERROR"
        fi
        echo ""
    fi
}

main "$@"
