#!/usr/bin/env bash
# sysjournal — shared system-knowledge journal for the Obsidian vault.
#
# One tool, used by every coding agent (Claude Code, Codex, Pi), so host/
# environment changes get recorded in ONE consistent, greppable place with
# the frontmatter schema that "Tech/Infrastructure/System Log MOC.md" indexes
# via Dataview (date, machine, type, status, tags, review-by).
#
# Journal HOST/ENVIRONMENT changes — system config, services, networking,
# VMs, drivers, build toolchains, host-wiring of an app. NOT ordinary work
# inside a code repo. See `sysjournal help`.
#
# Portable bash (invoked by agents in varied environments), not zsh.

set -euo pipefail

VAULT="${SYSJOURNAL_VAULT:-$HOME/Documents/Obsidian25}"
SUBDIR="${SYSJOURNAL_SUBDIR:-Tech/Infrastructure}"
INFRA="$VAULT/$SUBDIR"

die() { printf 'sysjournal: %s\n' "$*" >&2; exit 1; }

ensure_dir() {
  [ -d "$VAULT" ] || die "vault not found: $VAULT (set SYSJOURNAL_VAULT)"
  mkdir -p "$INFRA"
}

usage() {
  cat <<'EOF'
sysjournal — record & recall host/environment changes in the Obsidian vault.

USAGE
  sysjournal search <keywords...>        Recall: grep the journal first (case-insensitive)
  sysjournal new "<Title>" [options]     Scaffold a new note, print its path
  sysjournal list [N]                    List the N most-recent notes (default 20)
  sysjournal path                        Print the infrastructure folder path
  sysjournal help                        This help

`new` OPTIONS
  --type    <t>   change | setup | debug | fix | incident | note   (default: change)
  --status  <s>   deployed | resolved | unresolved | workaround | planned (default: deployed)
  --tags    a,b,c comma-separated tags
  --machine <m>   host the change was made on           (default: `hostname -s`)
  --review-by <YYYY-MM-DD>   optional follow-up/expiry date
  --summary "<one line>"     optional lead line

WHAT TO JOURNAL
  YES  system config, services/daemons (systemd/launchd/cron), networking/DNS/
       VPN/firewall, VMs & host containers, drivers/kernel/boot, disks/mounts,
       build toolchains & global package installs, wiring an app into the host.
  NO   feature work, bug fixes, refactors, tests INSIDE a project repo.
  Discriminator: does it change state outside the repo, on the host? If no, skip.
  Straddle (build an app AND install it as a service): journal only the
  host-wiring part (the unit/cron/port), not the app code.

EXAMPLES
  sysjournal search systemd relay port
  sysjournal new "WireGuard VPN to homelab" --type setup --tags wireguard,vpn,network
  sysjournal new "DNS resolution flaky after netplan change" --type debug --status unresolved
EOF
}

cmd_search() {
  ensure_dir
  [ "$#" -ge 1 ] || die "search needs at least one keyword"
  # OR-match the keywords so a few loosely-related terms still surface notes.
  local pattern
  pattern=$(printf '%s|' "$@"); pattern="${pattern%|}"
  echo "# Journal matches in $SUBDIR for: $*"
  echo
  if ! rg -i --no-heading -n -C1 --color never -e "$pattern" "$INFRA" 2>/dev/null; then
    echo "(no matches — nothing journaled on this yet)"
  fi
}

cmd_list() {
  ensure_dir
  local n="${1:-20}"
  # Newest first by mtime; strip the vault prefix for readability.
  find "$INFRA" -maxdepth 1 -name '*.md' -printf '%T@ %p\n' 2>/dev/null \
    | sort -rn | head -n "$n" | sed "s#[0-9.]* $INFRA/##"
}

cmd_path() { echo "$INFRA"; }

cmd_new() {
  ensure_dir
  local title="" type="change" status="deployed" tags="" machine review_by="" summary=""
  machine="$(hostname -s 2>/dev/null || hostname)"

  # First non-flag arg is the title.
  while [ "$#" -gt 0 ]; do
    case "$1" in
      --type)      type="${2:?--type needs a value}"; shift 2;;
      --status)    status="${2:?--status needs a value}"; shift 2;;
      --tags)      tags="${2:?--tags needs a value}"; shift 2;;
      --machine)   machine="${2:?--machine needs a value}"; shift 2;;
      --review-by) review_by="${2:?--review-by needs a value}"; shift 2;;
      --summary)   summary="${2:?--summary needs a value}"; shift 2;;
      --*)         die "unknown option: $1";;
      *)           [ -z "$title" ] && title="$1" || die "unexpected arg: $1"; shift;;
    esac
  done
  [ -n "$title" ] || die 'new needs a "<Title>"'

  # Filename: keep the human title (Obsidian-friendly), drop only path-illegal chars.
  local fname; fname=$(printf '%s' "$title" | tr '/\\' '--' | sed 's/[[:cntrl:]]//g; s/  */ /g; s/^ *//; s/ *$//')
  local file="$INFRA/$fname.md"
  if [ -e "$file" ]; then
    echo "$file"   # already exists — recall, don't clobber; edit/append this note.
    echo "sysjournal: note already exists — edit it instead of creating a duplicate." >&2
    return 0
  fi

  # YAML frontmatter — inline tag list, matching the MOC's own `tags: [moc]` style.
  local yaml_tags="[]"
  if [ -n "$tags" ]; then
    yaml_tags="[$(printf '%s' "$tags" | sed 's/ *, */, /g')]"
  fi
  local date_today; date_today="$(date +%F)"

  {
    echo "---"
    echo "date: $date_today"
    echo "machine: $machine"
    echo "type: $type"
    echo "status: $status"
    echo "tags: $yaml_tags"
    [ -n "$review_by" ] && echo "review-by: $review_by"
    echo "---"
    echo
    echo "# $title"
    echo
    [ -n "$summary" ] && { echo "$summary"; echo; }
    echo "## Why"
    echo
    echo "## What changed"
    echo
    echo "## Design decisions / gotchas"
    echo
    echo "## Verify"
    echo
    echo '```'
    echo '# command run + observed result'
    echo '```'
    echo
    echo "## Status / follow-ups"
    echo
    echo "Related: "
  } > "$file"

  echo "$file"
}

main() {
  local sub="${1:-help}"; shift || true
  case "$sub" in
    search|recall|grep) cmd_search "$@";;
    new|add)            cmd_new "$@";;
    list|ls)            cmd_list "$@";;
    path|dir)           cmd_path;;
    help|-h|--help)     usage;;
    *)                  usage; die "unknown command: $sub";;
  esac
}

main "$@"
