#!/usr/bin/env bash

#########################################################################################
#### terraform-lint-reporter.sh — Lint and plan Terraform configs with formatted     ####
#### reports. Wraps tflint and terraform plan with severity levels and CI exit codes  ####
#### Requires: bash 4+, terraform, tflint                                            ####
####                                                                                 ####
#### Author: Phil Connor                                                             ####
#### Contact: contact@mylinux.work                                                   ####
#### License: MIT                                                                    ####
#### Version 1.01                                                                    ####
####                                                                                 ####
#### Usage:                                                                          ####
####   ./terraform-lint-reporter.sh --full --dir ./infra                             ####
####                                                                                 ####
#### See --help for all options.                                                     ####
#########################################################################################
# v1.01 changes:
#   - Fixed: ((0++)) returns 1 under set -e; added || true guards
#   - Fixed: grep in pipeline crashes under set -euo pipefail when no matches found. Added || true guard
#########################################################################################

set -euo pipefail

# ── Defaults ──────────────────────────────────────────────────────────
RUN_MODE=""
TF_DIR="${TF_DIR:-.}"
TFLINT_PATH="${TFLINT_PATH:-tflint}"
TF_PATH="${TF_PATH:-terraform}"
OUTPUT_FORMAT="${TF_REPORT_FORMAT:-text}"
OUTPUT_FILE=""
FAIL_ON="${TF_FAIL_ON:-error}"
AUTO_INIT="${TF_AUTO_INIT:-true}"
PLAN_FILE=""
VAR_FILE=""
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"

# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
START_TIME=""
LINT_WARNINGS=0
LINT_ERRORS=0
PLAN_ADDS=0
PLAN_CHANGES=0
PLAN_DESTROYS=0
TMPDIR_WORK=""
RESULTS_JSON="[]"

# ── Colors ────────────────────────────────────────────────────────────
RED="" GREEN="" YELLOW="" BLUE="" CYAN="" BOLD="" DIM="" RESET=""

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

# ── Logging ───────────────────────────────────────────────────────────
log()     { echo -e "${BLUE}[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; }
die()     { err "$*"; exit 1; }

# ── 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"
}

elapsed() {
    local end_time
    end_time=$(date +%s)
    echo "$(( end_time - START_TIME ))s"
}

cleanup() {
    if [[ -n "$TMPDIR_WORK" && -d "$TMPDIR_WORK" ]]; then
        rm -rf "$TMPDIR_WORK"
    fi
}
trap cleanup EXIT

make_tmpdir() {
    if [[ -z "$TMPDIR_WORK" ]]; then
        TMPDIR_WORK=$(mktemp -d "${TMPDIR:-/tmp}/tf-lint-reporter.XXXXXX")
    fi
}

# ── Dependency checks ────────────────────────────────────────────────
require_terraform() {
    if ! command -v "$TF_PATH" &>/dev/null; then
        die "Terraform binary not found: ${TF_PATH}"
    fi
    verbose "Terraform found: $(command -v "$TF_PATH") ($("$TF_PATH" version -json 2>/dev/null | jq -r '.terraform_version' 2>/dev/null || "$TF_PATH" version | head -1))"
}

require_tflint() {
    if ! command -v "$TFLINT_PATH" &>/dev/null; then
        die "tflint binary not found: ${TFLINT_PATH} — install from https://github.com/terraform-linters/tflint"
    fi
    verbose "tflint found: $(command -v "$TFLINT_PATH") ($("$TFLINT_PATH" --version 2>/dev/null || echo 'unknown'))"
}

# ── Lint ─────────────────────────────────────────────────────────────
do_lint() {
    require_tflint

    START_TIME=$(date +%s)
    section_header "Terraform Lint (tflint)"
    field "Directory:" "$TF_DIR"
    echo ""

    local lint_output exit_code=0
    verbose "Running: ${TFLINT_PATH} --format json --chdir ${TF_DIR}"
    lint_output=$("$TFLINT_PATH" --format json --chdir "$TF_DIR" 2>&1) || exit_code=$?

    local issues_count=0

    if command -v jq &>/dev/null && echo "$lint_output" | jq empty 2>/dev/null; then
        issues_count=$(echo "$lint_output" | jq '[.issues // [] | .[] ] | length' 2>/dev/null || echo 0)

        if [[ "$issues_count" -gt 0 ]]; then
            printf "  ${BOLD}%-10s %-30s %-8s %s${RESET}\n" "SEVERITY" "RULE" "LINE" "MESSAGE"
            printf "  %-10s %-30s %-8s %s\n" "----------" "------------------------------" "--------" "$(printf '%0.s-' {1..30})"

            echo "$lint_output" | jq -r '.issues[] | [.rule.severity, .rule.name, (.range.filename + ":" + (.range.start.line | tostring)), .message] | @tsv' 2>/dev/null | \
            while IFS=$'\t' read -r sev rule loc msg; do
                local color="$RESET"
                case "$sev" in
                    error)   color="$RED"; ((LINT_ERRORS++)) || true ;;
                    warning) color="$YELLOW"; ((LINT_WARNINGS++)) || true ;;
                    notice)  color="$DIM" ;;
                esac
                printf "  ${color}%-10s${RESET} %-30s %-8s %s\n" "${sev^^}" "$rule" "$loc" "$msg"

                RESULTS_JSON=$(echo "$RESULTS_JSON" | jq --arg s "$sev" --arg r "$rule" \
                    --arg l "$loc" --arg m "$msg" \
                    '. + [{"type":"lint","severity":$s,"rule":$r,"location":$l,"message":$m}]')
            done
        fi
    else
        while IFS= read -r line; do
            if [[ "$line" =~ (Error|error) ]]; then
                ((LINT_ERRORS++)) || true
                echo -e "  ${RED}${line}${RESET}"
            elif [[ "$line" =~ (Warning|warning) ]]; then
                ((LINT_WARNINGS++)) || true
                echo -e "  ${YELLOW}${line}${RESET}"
            elif [[ -n "$line" ]]; then
                echo "  $line"
            fi
        done <<< "$lint_output"
    fi

    local tf_files=0
    if [[ -d "$TF_DIR" ]]; then
        tf_files=$(find "$TF_DIR" -maxdepth 1 -name '*.tf' | wc -l | tr -d ' ')
    fi

    section_header "Lint Summary"
    field "Files checked:" "$tf_files"
    field_color "Errors:" "${RED}${LINT_ERRORS}${RESET}"
    field_color "Warnings:" "${YELLOW}${LINT_WARNINGS}${RESET}"
    field "Duration:" "$(elapsed)"
}

# ── Plan ─────────────────────────────────────────────────────────────
do_plan() {
    require_terraform

    START_TIME=$(date +%s)
    section_header "Terraform Plan"
    field "Directory:" "$TF_DIR"
    field "Auto-init:" "$AUTO_INIT"
    [[ -n "$VAR_FILE" ]] && field "Var file:" "$VAR_FILE"
    echo ""

    if [[ "$AUTO_INIT" == "true" ]]; then
        log "Running terraform init..."
        verbose "Running: ${TF_PATH} -chdir=${TF_DIR} init -input=false -no-color"
        if ! "$TF_PATH" -chdir="$TF_DIR" init -input=false -no-color >/dev/null 2>&1; then
            die "terraform init failed in ${TF_DIR}"
        fi
        log "Init complete"
    fi

    make_tmpdir
    local plan_out="${TMPDIR_WORK}/tfplan"
    local plan_args=(-detailed-exitcode -no-color -out="$plan_out")
    [[ -n "$VAR_FILE" ]] && plan_args+=(-var-file="$VAR_FILE")

    local plan_output plan_exit=0
    verbose "Running: ${TF_PATH} -chdir=${TF_DIR} plan ${plan_args[*]}"
    plan_output=$("$TF_PATH" -chdir="$TF_DIR" plan "${plan_args[@]}" 2>&1) || plan_exit=$?

    verbose "terraform plan exit code: ${plan_exit}"

    if [[ $plan_exit -eq 1 ]]; then
        err "terraform plan failed:"
        echo "$plan_output" | tail -20
        exit 1
    fi

    if echo "$plan_output" | grep -qE 'Plan: [0-9]+ to add'; then
        local plan_line
        plan_line=$(echo "$plan_output" | { grep -oE 'Plan: [0-9]+ to add, [0-9]+ to change, [0-9]+ to destroy' || true; })
        if [[ -n "$plan_line" ]]; then
            PLAN_ADDS=$(echo "$plan_line" | { grep -oE '[0-9]+ to add' || true; } | { grep -oE '[0-9]+' || true; })
            PLAN_CHANGES=$(echo "$plan_line" | { grep -oE '[0-9]+ to change' || true; } | { grep -oE '[0-9]+' || true; })
            PLAN_DESTROYS=$(echo "$plan_line" | { grep -oE '[0-9]+ to destroy' || true; } | { grep -oE '[0-9]+' || true; })
        fi
    fi

    if [[ -n "$PLAN_FILE" ]]; then
        cp "$plan_out" "$PLAN_FILE"
        log "Plan file saved to ${PLAN_FILE}"
    fi

    RESULTS_JSON=$(echo "$RESULTS_JSON" | jq \
        --argjson a "$PLAN_ADDS" --argjson c "$PLAN_CHANGES" --argjson d "$PLAN_DESTROYS" \
        '. + [{"type":"plan","adds":$a,"changes":$c,"destroys":$d}]')

    section_header "Plan Summary"
    if [[ $plan_exit -eq 0 ]]; then
        echo -e "  ${GREEN}No changes.${RESET} Infrastructure is up-to-date."
    else
        field_color "Resources to add:" "${GREEN}${PLAN_ADDS}${RESET}"
        field_color "Resources to change:" "${YELLOW}${PLAN_CHANGES}${RESET}"
        field_color "Resources to destroy:" "${RED}${PLAN_DESTROYS}${RESET}"
    fi
    field "Duration:" "$(elapsed)"
}

# ── Full ─────────────────────────────────────────────────────────────
do_full() {
    START_TIME=$(date +%s)
    local full_start
    full_start=$(date +%s)

    do_lint
    echo ""
    do_plan

    section_header "Combined Summary"
    field_color "Lint errors:" "${RED}${LINT_ERRORS}${RESET}"
    field_color "Lint warnings:" "${YELLOW}${LINT_WARNINGS}${RESET}"
    field_color "Plan adds:" "${GREEN}${PLAN_ADDS}${RESET}"
    field_color "Plan changes:" "${YELLOW}${PLAN_CHANGES}${RESET}"
    field_color "Plan destroys:" "${RED}${PLAN_DESTROYS}${RESET}"
    local total_elapsed=$(( $(date +%s) - full_start ))
    field "Total duration:" "${total_elapsed}s"
}

# ── Fmt ──────────────────────────────────────────────────────────────
do_fmt() {
    require_terraform

    START_TIME=$(date +%s)
    section_header "Terraform Format Check"
    field "Directory:" "$TF_DIR"
    echo ""

    local fmt_output exit_code=0
    verbose "Running: ${TF_PATH} fmt -check -diff -recursive ${TF_DIR}"
    fmt_output=$("$TF_PATH" fmt -check -diff -recursive "$TF_DIR" 2>&1) || exit_code=$?

    local unformatted=0
    local unformatted_files=()

    if [[ $exit_code -ne 0 && -n "$fmt_output" ]]; then
        while IFS= read -r line; do
            if [[ "$line" =~ \.tf$ ]]; then
                ((unformatted++)) || true
                unformatted_files+=("$line")
                echo -e "  ${YELLOW}needs formatting:${RESET} ${line}"
            fi
        done <<< "$fmt_output"
    fi

    section_header "Format Summary"
    if [[ $unformatted -eq 0 ]]; then
        echo -e "  ${GREEN}✓${RESET} All files are properly formatted"
    else
        field_color "Files needing format:" "${YELLOW}${unformatted}${RESET}"
        echo ""
        echo -e "  Run ${BOLD}${TF_PATH} fmt -recursive ${TF_DIR}${RESET} to fix"
    fi
    field "Duration:" "$(elapsed)"

    for f in "${unformatted_files[@]+"${unformatted_files[@]}"}"; do
        RESULTS_JSON=$(echo "$RESULTS_JSON" | jq --arg f "$f" \
            '. + [{"type":"fmt","file":$f,"message":"needs formatting"}]')
    done
}

# ── Validate ─────────────────────────────────────────────────────────
do_validate() {
    require_terraform

    START_TIME=$(date +%s)
    section_header "Terraform Validate"
    field "Directory:" "$TF_DIR"
    echo ""

    if [[ "$AUTO_INIT" == "true" ]]; then
        log "Running terraform init -backend=false..."
        if ! "$TF_PATH" -chdir="$TF_DIR" init -backend=false -input=false -no-color >/dev/null 2>&1; then
            die "terraform init failed in ${TF_DIR}"
        fi
    fi

    local validate_output exit_code=0
    verbose "Running: ${TF_PATH} -chdir=${TF_DIR} validate -json"
    validate_output=$("$TF_PATH" -chdir="$TF_DIR" validate -json 2>&1) || exit_code=$?

    local valid diag_count
    valid=$(echo "$validate_output" | jq -r '.valid' 2>/dev/null || echo "false")
    diag_count=$(echo "$validate_output" | jq '[.diagnostics // [] | .[]] | length' 2>/dev/null || echo 0)

    if [[ "$valid" == "true" ]]; then
        echo -e "  ${GREEN}✓${RESET} Configuration is valid"
    else
        echo -e "  ${RED}✗${RESET} Configuration has errors"
        echo ""

        if [[ "$diag_count" -gt 0 ]]; then
            echo "$validate_output" | jq -r '.diagnostics[] | [.severity, .summary, .detail] | @tsv' 2>/dev/null | \
            while IFS=$'\t' read -r sev summary detail; do
                local color="$RESET"
                case "$sev" in
                    error)   color="$RED" ;;
                    warning) color="$YELLOW" ;;
                esac
                echo -e "  ${color}[${sev^^}]${RESET} ${summary}"
                [[ -n "$detail" ]] && echo "          $detail"

                RESULTS_JSON=$(echo "$RESULTS_JSON" | jq --arg s "$sev" \
                    --arg m "$summary" --arg d "$detail" \
                    '. + [{"type":"validate","severity":$s,"summary":$m,"detail":$d}]')
            done
        fi
    fi

    section_header "Validation Summary"
    field "Valid:" "$valid"
    field "Diagnostics:" "$diag_count"
    field "Duration:" "$(elapsed)"
}

# ── Output formats ───────────────────────────────────────────────────
write_output() {
    [[ -z "$OUTPUT_FILE" ]] && return
    case "$OUTPUT_FORMAT" in
        json)  write_json ;;
        junit) write_junit ;;
        text)  write_text ;;
        *)     die "Unknown output format: ${OUTPUT_FORMAT}" ;;
    esac
    log "Results written to ${OUTPUT_FILE}"
}

write_json() {
    jq -n --argjson results "$RESULTS_JSON" \
        --argjson lint_errors "$LINT_ERRORS" \
        --argjson lint_warnings "$LINT_WARNINGS" \
        --argjson plan_adds "$PLAN_ADDS" \
        --argjson plan_changes "$PLAN_CHANGES" \
        --argjson plan_destroys "$PLAN_DESTROYS" \
        --arg dir "$TF_DIR" \
        --arg mode "$RUN_MODE" \
        '{
            mode: $mode,
            directory: $dir,
            summary: {
                lint_errors: $lint_errors,
                lint_warnings: $lint_warnings,
                plan_adds: $plan_adds,
                plan_changes: $plan_changes,
                plan_destroys: $plan_destroys
            },
            results: $results
        }' > "$OUTPUT_FILE"
}

write_junit() {
    local total_tests failures
    total_tests=$(echo "$RESULTS_JSON" | jq '[.[] | select(.type == "lint")] | length' 2>/dev/null || echo 0)
    failures=$LINT_ERRORS

    {
        echo '<?xml version="1.0" encoding="UTF-8"?>'
        echo "<testsuites tests=\"${total_tests}\" failures=\"${failures}\">"
        echo "  <testsuite name=\"terraform-lint-reporter\" tests=\"${total_tests}\" failures=\"${failures}\">"

        echo "$RESULTS_JSON" | jq -r '.[] | select(.type == "lint") | @base64' 2>/dev/null | while read -r entry; do
            local sev rule loc msg
            sev=$(echo "$entry" | base64 -d | jq -r '.severity')
            rule=$(echo "$entry" | base64 -d | jq -r '.rule')
            loc=$(echo "$entry" | base64 -d | jq -r '.location')
            msg=$(echo "$entry" | base64 -d | jq -r '.message')
            if [[ "$sev" == "error" ]]; then
                echo "    <testcase name=\"${rule}\" classname=\"${loc}\">"
                echo "      <failure message=\"${msg}\" type=\"${sev}\"/>"
                echo "    </testcase>"
            else
                echo "    <testcase name=\"${rule}\" classname=\"${loc}\"/>"
            fi
        done

        echo "  </testsuite>"
        echo "</testsuites>"
    } > "$OUTPUT_FILE"
}

write_text() {
    {
        echo "Terraform Lint Reporter Results"
        echo "==============================="
        echo ""
        echo "Mode:      ${RUN_MODE}"
        echo "Directory: ${TF_DIR}"
        echo ""
        echo "$RESULTS_JSON" | jq -r '.[] | if .type == "lint" then
            "[\(.severity | ascii_upcase)] \(.rule) at \(.location): \(.message)"
        elif .type == "plan" then
            "Plan: \(.adds) to add, \(.changes) to change, \(.destroys) to destroy"
        elif .type == "fmt" then
            "[FMT] \(.file): \(.message)"
        elif .type == "validate" then
            "[\(.severity | ascii_upcase)] \(.summary)"
        else empty end' 2>/dev/null || true
        echo ""
        echo "Lint Errors: ${LINT_ERRORS}  Warnings: ${LINT_WARNINGS}"
        echo "Plan: +${PLAN_ADDS} ~${PLAN_CHANGES} -${PLAN_DESTROYS}"
    } > "$OUTPUT_FILE"
}

# ── Compute exit code ────────────────────────────────────────────────
compute_exit() {
    case "$FAIL_ON" in
        warning)
            if [[ $LINT_ERRORS -gt 0 || $LINT_WARNINGS -gt 0 ]]; then
                exit 2
            fi
            if [[ $PLAN_DESTROYS -gt 0 || $PLAN_CHANGES -gt 0 ]]; then
                exit 2
            fi
            ;;
        error)
            if [[ $LINT_ERRORS -gt 0 ]]; then
                exit 2
            fi
            if [[ $PLAN_DESTROYS -gt 0 ]]; then
                exit 2
            fi
            ;;
    esac
}

# ── Usage ─────────────────────────────────────────────────────────────
show_help() {
    cat <<EOF
${BOLD}${SCRIPT_NAME}${RESET} — Terraform Lint & Plan Reporter

Lint and plan Terraform configurations with formatted reports,
severity levels, and CI-friendly exit codes.

${BOLD}MODES${RESET}
  --lint              Run tflint on Terraform directory
  --plan              Run terraform init + plan, report resource changes
  --full              Run lint then plan, combined report
  --fmt               Check formatting with terraform fmt
  --validate          Run terraform validate, report diagnostics

${BOLD}OPTIONS${RESET}
  --dir DIR           Terraform directory (default: .)
  --var-file FILE     Terraform var file for plan
  --plan-out FILE     Save plan binary to file
  --format FORMAT     Output format: text, json, junit (default: text)
  --output-file FILE  Write results to file
  --fail-on LEVEL     Fail threshold: warning or error (default: error)
  --no-init           Skip automatic terraform init
  --verbose           Show debug output
  --no-color          Disable colored output
  --help              Show this help message

${BOLD}ENVIRONMENT VARIABLES${RESET}
  TF_DIR              Default Terraform directory
  TF_PATH             Path to terraform binary (default: terraform)
  TFLINT_PATH         Path to tflint binary (default: tflint)
  TF_REPORT_FORMAT    Default output format
  TF_FAIL_ON          Default fail threshold
  TF_AUTO_INIT        Auto-run terraform init (default: true)
  VERBOSE             Enable verbose output (true/false)
  COLOR               Color mode: auto, always, never

${BOLD}EXAMPLES${RESET}
  # Lint only
  ${SCRIPT_NAME} --lint --dir ./infra

  # Plan with var file
  ${SCRIPT_NAME} --plan --dir ./infra --var-file prod.tfvars

  # Full lint + plan, save JSON report
  ${SCRIPT_NAME} --full --dir ./infra --format json --output-file report.json

  # Format check in CI
  ${SCRIPT_NAME} --fmt --dir ./modules

  # Validate configuration
  ${SCRIPT_NAME} --validate --dir ./infra

  # Strict CI pipeline (fail on warnings)
  ${SCRIPT_NAME} --full --dir ./infra --fail-on warning --format junit --output-file results.xml

${BOLD}EXIT CODES${RESET}
  0   All checks passed
  1   Runtime error
  2   Lint errors or plan changes exceed threshold
EOF
}

# ── Parse arguments ──────────────────────────────────────────────────
parse_args() {
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --lint)        RUN_MODE="lint"; shift ;;
            --plan)        RUN_MODE="plan"; shift ;;
            --full)        RUN_MODE="full"; shift ;;
            --fmt)         RUN_MODE="fmt"; shift ;;
            --validate)    RUN_MODE="validate"; shift ;;
            --dir)         TF_DIR="${2:?--dir requires a path}"; shift 2 ;;
            --var-file)    VAR_FILE="${2:?--var-file requires a path}"; shift 2 ;;
            --plan-out)    PLAN_FILE="${2:?--plan-out requires a path}"; shift 2 ;;
            --format)      OUTPUT_FORMAT="${2:?--format requires a value}"; shift 2 ;;
            --output-file) OUTPUT_FILE="${2:?--output-file requires a path}"; shift 2 ;;
            --fail-on)     FAIL_ON="${2:?--fail-on requires a value}"; shift 2 ;;
            --no-init)     AUTO_INIT="false"; shift ;;
            --verbose)     VERBOSE="true"; shift ;;
            --no-color)    COLOR="never"; shift ;;
            --help|-h)     setup_colors; show_help; exit 0 ;;
            *)             die "Unknown option: $1 (see --help)" ;;
        esac
    done
}

# ── Main ─────────────────────────────────────────────────────────────
main() {
    parse_args "$@"
    setup_colors

    case "$RUN_MODE" in
        lint)     do_lint; write_output; compute_exit ;;
        plan)     do_plan; write_output; compute_exit ;;
        full)     do_full; write_output; compute_exit ;;
        fmt)      do_fmt; write_output; compute_exit ;;
        validate) do_validate; write_output; compute_exit ;;
        "")       err "No mode specified"; echo ""; show_help; exit 1 ;;
        *)        die "Unknown mode: ${RUN_MODE}" ;;
    esac
}

main "$@"
