#!/usr/bin/env bash
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SCORECARD_MAP="${HOMELAB_SCORECARD_EXPLAIN_MAP:-${REPO_ROOT}/infra/jeannie-explain/scorecard.tsv}"
FINDINGS_MAP="${HOMELAB_FINDINGS_EXPLAIN_MAP:-${REPO_ROOT}/infra/jeannie-explain/findings.tsv}"
EXPLAIN_INPUT_FILE=""

cleanup() {
    if [ -n "$EXPLAIN_INPUT_FILE" ] && [ -f "$EXPLAIN_INPUT_FILE" ]; then
        rm -f "$EXPLAIN_INPUT_FILE"
    fi
}
trap cleanup EXIT

usage() {
    cat <<'EOF'
Usage:
  ./jeannie explain scorecard [--all]
  ./jeannie explain <read-only-command> [args...]
  ./jeannie explain output < file

Explain warnings, failures, and corrective findings from Jeannie output. This is
read-only and refuses mutating commands.

Examples:
  ./jeannie explain scorecard
  ./jeannie explain status
  ./jeannie explain capacity
  ./jeannie explain resource-budget --details
  ./jeannie status | ./jeannie explain output
EOF
}

is_read_only_command() {
    case "$1" in
        status|capacity|capacity-advisor|capacity-limits|recover-plan|incident|safety-case|agent-sandbox|scorecard|gitops-status|promote|cert-check|backup-status|synthetic-checks|resource-budget|route-inventory|golden-ledger|workers|change-journal|map|validate|access-audit|doctor-versions|doctor-edge|doctor-gitea|doctor-rpi|doctor-cluster|preflight|doctor-preapply|inventory-check|secrets-check|tailnet-policy-check|ai-check|ask|ai-evals|ai-scheduler|ai-memory|impact|review-last-change|security-scan|security-host|security-trivy|security-secrets|security-nuclei|security-web|security-logs|security-attack-path|prompt-injection-lab|red-blue|control-plane)
            return 0
            ;;
        *)
            return 1
            ;;
    esac
}

is_read_only_subcommand() {
    local command_name="$1"
    local subcommand="${2:-}"

    case "$command_name" in
        workers)
            [ -z "$subcommand" ] || [ "$subcommand" = "list" ]
            ;;
        golden-ledger)
            [ -z "$subcommand" ] || [ "$subcommand" = "show" ] || [ "$subcommand" = "check" ]
            ;;
        change-journal)
            [ -z "$subcommand" ] || [ "$subcommand" = "list" ] || [ "$subcommand" = "path" ]
            ;;
        control-plane)
            [ -z "$subcommand" ] || [ "$subcommand" = "status" ] || [ "$subcommand" = "pods" ]
            ;;
        *)
            return 0
            ;;
    esac
}

run_target() {
    local command_name="$1"
    shift || true

    if ! is_read_only_command "$command_name" || ! is_read_only_subcommand "$command_name" "${1:-}"; then
        echo "Refusing to explain by running mutating or unknown command: ./jeannie ${command_name} $*" >&2
        echo "Use './jeannie explain output < saved-output.txt' for arbitrary pasted output." >&2
        return 2
    fi

    "${REPO_ROOT}/jeannie" "$command_name" "$@" 2>&1 || true
}

explain_output() {
    local topic="$1"
    local show_all="$2"
    local input_file="$3"

    if [ ! -s "$FINDINGS_MAP" ]; then
        echo "Missing findings explain map: $FINDINGS_MAP" >&2
        return 1
    fi
    if [ ! -s "$SCORECARD_MAP" ]; then
        echo "Missing scorecard explain map: $SCORECARD_MAP" >&2
        return 1
    fi

    python3 - "$topic" "$show_all" "$input_file" "$SCORECARD_MAP" "$FINDINGS_MAP" <<'PY'
import csv
import re
import sys

topic, show_all_arg, input_file, scorecard_map, findings_map = sys.argv[1:6]
show_all = show_all_arg.lower() == "true"

def read_tsv(path, key):
    with open(path, encoding="utf-8", newline="") as handle:
        return {row[key].lower(): row for row in csv.DictReader(handle, delimiter="\t")}

def read_tsv_rows(path):
    with open(path, encoding="utf-8", newline="") as handle:
        return list(csv.DictReader(handle, delimiter="\t"))

scorecard_info = read_tsv(scorecard_map, "label")
finding_rows = read_tsv_rows(findings_map)

with open(input_file, encoding="utf-8") as handle:
    text = handle.read()

lines = text.splitlines()

def split_commands(value):
    return [item.strip() for item in (value or "").split(";") if item.strip() and item.strip() != "none"]

def print_info(info, indent="  "):
    diagnose = split_commands(info.get("diagnose_commands"))
    jeannie_fix = split_commands(info.get("jeannie_fix_commands"))
    other_fix = split_commands(info.get("other_fix_commands"))

    print(f"{indent}Why: {info.get('meaning') or 'No explain entry exists yet.'}")
    if diagnose:
        print(f"{indent}Next command: {diagnose[0]}")
        if len(diagnose) > 1:
            print(f"{indent}More checks:")
            for command in diagnose[1:]:
                print(f"{indent}  {command}")
    if jeannie_fix:
        print(f"{indent}Jeannie fix command:")
        for command in jeannie_fix:
            print(f"{indent}  {command}")
    else:
        print(f"{indent}Jeannie fix command: none yet")
    if other_fix:
        print(f"{indent}Code/config fix:")
        for command in other_fix:
            print(f"{indent}  {command}")
    print(f"{indent}Risk: {info.get('risk') or 'unknown'}")
    print(f"{indent}Auto-fix safe: {info.get('auto_fix') or 'no'}")
    if info.get("notes"):
        print(f"{indent}Note: {info['notes']}")

def parse_scorecard():
    rows = []
    pattern = re.compile(r"^(?P<label>.{1,28}) (?P<status>pass|warn|fail)(?: - (?P<summary>.*))?$")
    for line in lines:
        match = pattern.match(line)
        if not match:
            continue
        label = match.group("label").strip()
        status = match.group("status").strip()
        summary = (match.group("summary") or "").strip()
        if not show_all and status == "pass":
            continue
        rows.append((label, status, summary))
    return rows

def interesting_lines():
    keywords = [
        "fail",
        "warn",
        "error",
        "missing",
        "unavailable",
        "unhealthy",
        "degraded",
        "outofsync",
        "notready",
        "pressure",
        "stale",
        "expired",
        "timeout",
        "timed out",
        "refused",
        "404",
        "502",
        "backoff",
        "crashloop",
        "imagepull",
        "no servers could be reached",
        "needs cleanup",
    ]
    found = []
    for line in lines:
        lowered = line.lower()
        if any(keyword in lowered for keyword in keywords):
            found.append(line.strip())
    return found

def match_findings(line):
    lowered = line.lower()
    matches = []
    for info in finding_rows:
        pattern = (info.get("pattern") or "").lower()
        if pattern and pattern in lowered:
            matches.append((pattern, info))
    return matches

def best_finding(line):
    matches = match_findings(line)
    if not matches:
        return None
    return sorted(matches, key=lambda item: (len(item[0]), item[0]), reverse=True)[0]

def parse_report_line(line):
    report_match = re.match(r"^(?P<status>ok|warn|fail|skip)\s+(?P<check>.+?)(?:\s{2,}(?P<summary>.+))?$", line)
    if report_match:
        return (
            report_match.group("check").strip(),
            report_match.group("status").strip(),
            (report_match.group("summary") or "").strip(),
        )
    scorecard_match = re.match(r"^(?P<label>.{1,28}) (?P<status>pass|warn|fail)(?: - (?P<summary>.*))?$", line)
    if scorecard_match:
        return (
            scorecard_match.group("label").strip(),
            scorecard_match.group("status").strip(),
            (scorecard_match.group("summary") or "").strip(),
        )
    return (line[:72], "", "")

print(f"Jeannie Explain: {topic}")
print("=" * (17 + len(topic)))
print("mode: read-only")

if topic == "scorecard":
    rows = parse_scorecard()
    if not rows:
        print("\nNo scorecard warnings or failures found.")
    for index, (label, status, summary) in enumerate(rows, start=1):
        info = scorecard_info.get(label.lower(), {})
        print(f"\n{index}. {label}")
        print(f"   Status: {status}")
        if summary:
            print(f"   Observed: {summary}")
        print_info(info, "   ")
    raise SystemExit(0)

found_lines = interesting_lines()
seen_patterns = set()
matched_any = False

for line in found_lines:
    title, status, summary = parse_report_line(line)
    match = best_finding(summary) if summary else None
    if not match:
        match = best_finding(line)
    if not match:
        continue
    pattern, info = match
    if pattern in seen_patterns:
        continue
    seen_patterns.add(pattern)
    matched_any = True
    print(f"\n{len(seen_patterns)}. {title}")
    if status:
        print(f"   Status: {status}")
    if summary:
        print(f"   Observed: {summary}")
    else:
        print(f"   Observed: {line}")
    print(f"   Matched rule: {pattern}")
    print_info(info, "   ")

if not matched_any:
    if found_lines:
        print("\nPotential issues were found, but no specific explain rule matched yet.")
        print("Lines:")
        for line in found_lines[:20]:
            print(f"  {line}")
        if len(found_lines) > 20:
            print(f"  ... {len(found_lines) - 20} more")
    else:
        print("\nNo warning/error/corrective lines found.")
PY
}

main() {
    local topic="${1:-}"
    local show_all=false

    case "$topic" in
        ""|-h|--help|help)
            usage
            return 0
            ;;
    esac
    shift || true

    EXPLAIN_INPUT_FILE="$(mktemp "${TMPDIR:-/tmp}/jeannie-explain.XXXXXX")"

    if [ "$topic" = "output" ]; then
        cat >"$EXPLAIN_INPUT_FILE"
        explain_output "output" "$show_all" "$EXPLAIN_INPUT_FILE"
        return
    fi

    if [ "$topic" = "scorecard" ]; then
        while [ "$#" -gt 0 ]; do
            case "$1" in
                --all)
                    show_all=true
                    ;;
                -h|--help|help)
                    usage
                    return 0
                    ;;
                *)
                    usage >&2
                    return 2
                    ;;
            esac
            shift
        done
        run_target scorecard >"$EXPLAIN_INPUT_FILE"
        explain_output scorecard "$show_all" "$EXPLAIN_INPUT_FILE"
        return
    fi

    run_target "$topic" "$@" >"$EXPLAIN_INPUT_FILE"
    explain_output "$topic" "$show_all" "$EXPLAIN_INPUT_FILE"
}

main "$@"
