#!/usr/bin/env bash
#
# credentials.sh — Shared credential loader for Mosaic tool suites
#
# Usage: source ~/.config/mosaic/tools/_lib/credentials.sh
#        load_credentials <service-name>
#
# credentials.json is the single source of truth.
# For Woodpecker, credentials are also synced to ~/.woodpecker/<instance>.env.
#
# Supported services:
#   portainer, coolify, authentik, glpi, github,
#   gitea-mosaicstack, gitea-usc, woodpecker, cloudflare,
#   turbo-cache, openbrain
#
# After loading, service-specific env vars are exported.
# Run `load_credentials --help` for details.
#
# Resolution order (first match wins):
#   1. $MOSAIC_CREDENTIALS_FILE (explicit override — never second-guessed)
#   2. $HOME/.config/mosaic/credentials.json
#   3. /etc/mosaic/credentials.json (host-level fallback)
# The /etc fallback exists for HOME-redirected profile environments, where
# $HOME points at a per-profile directory that has no credentials file.
# Operators symlink /etc/mosaic/credentials.json to the host's canonical
# file once, instead of exporting MOSAIC_CREDENTIALS_FILE per invocation.

if [[ -z "${MOSAIC_CREDENTIALS_FILE:-}" ]]; then
  for _cand in "$HOME/.config/mosaic/credentials.json" "/etc/mosaic/credentials.json"; do
    if [[ -f "$_cand" ]]; then MOSAIC_CREDENTIALS_FILE="$_cand"; break; fi
  done
  : "${MOSAIC_CREDENTIALS_FILE:=$HOME/.config/mosaic/credentials.json}"
fi
export MOSAIC_CREDENTIALS_FILE

_mosaic_require_jq() {
  if ! command -v jq &>/dev/null; then
    echo "Error: jq is required but not installed" >&2
    return 1
  fi
}

_mosaic_read_cred() {
  local jq_path="$1"
  if [[ ! -f "$MOSAIC_CREDENTIALS_FILE" ]]; then
    echo "Error: Credentials file not found: $MOSAIC_CREDENTIALS_FILE" >&2
    return 1
  fi
  jq -r "$jq_path // empty" "$MOSAIC_CREDENTIALS_FILE"
}

# Decide curl TLS flag for a target URL: validate public hosts (MITM matters on
# WAN); allow self-signed only for private-network IP literals (trusted LAN) or an
# explicit $MOSAIC_INSECURE_TLS opt-in. Echoes "-k" or "" (empty).
_mosaic_tls_opt() {
  local url="$1" host
  [[ -n "${MOSAIC_INSECURE_TLS:-}" ]] && { echo "-k"; return; }
  host=$(printf '%s' "$url" | sed -E 's#^[a-zA-Z]+://([^/:]+).*#\1#')
  if [[ "$host" =~ ^(10\.|127\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.) ]]; then
    echo "-k"; return
  fi
  echo ""
}

# Sync Woodpecker credentials to ~/.woodpecker/<instance>.env
# Only writes when values differ to avoid unnecessary disk writes.
_mosaic_sync_woodpecker_env() {
  local instance="$1" url="$2" token="$3"
  local env_file="$HOME/.woodpecker/${instance}.env"
  [[ -d "$HOME/.woodpecker" ]] || return 0
  local expected
  expected=$(printf '# %s Woodpecker CI\nexport WOODPECKER_SERVER="%s"\nexport WOODPECKER_TOKEN="%s"\n' \
    "$instance" "$url" "$token")
  if [[ -f "$env_file" ]]; then
    local current_url current_token
    current_url=$(grep -oP '(?<=WOODPECKER_SERVER=").*(?=")' "$env_file" 2>/dev/null || true)
    current_token=$(grep -oP '(?<=WOODPECKER_TOKEN=").*(?=")' "$env_file" 2>/dev/null || true)
    [[ "$current_url" == "$url" && "$current_token" == "$token" ]] && return 0
  fi
  printf '%s\n' "$expected" > "$env_file"
}

# Load legacy flat Woodpecker credentials (.woodpecker.url / .woodpecker.token).
# Some environments export WOODPECKER_INSTANCE=mosaic, but the current
# credentials.json may still use the legacy flat schema. Treat "mosaic" as the
# default flat instance when a nested .woodpecker.mosaic object is absent.
_mosaic_load_woodpecker_legacy() {
  export WOODPECKER_URL="$(_mosaic_read_cred '.woodpecker.url')"
  export WOODPECKER_TOKEN="$(_mosaic_read_cred '.woodpecker.token')"
  export WOODPECKER_INSTANCE="${WOODPECKER_INSTANCE:-mosaic}"
  WOODPECKER_URL="${WOODPECKER_URL%/}"
  [[ -n "$WOODPECKER_URL" ]] || { echo "Error: woodpecker.url not found" >&2; return 1; }
  [[ -n "$WOODPECKER_TOKEN" ]] || { echo "Error: woodpecker.token not found" >&2; return 1; }
  _mosaic_sync_woodpecker_env "$WOODPECKER_INSTANCE" "$WOODPECKER_URL" "$WOODPECKER_TOKEN"
}

load_credentials() {
  local service="$1"

  if [[ -z "$service" || "$service" == "--help" ]]; then
    cat <<'EOF'
Usage: load_credentials <service>

Services and exported variables:
  portainer         → PORTAINER_URL, PORTAINER_API_KEY
  coolify           → COOLIFY_URL, COOLIFY_TOKEN
  authentik         → AUTHENTIK_URL, AUTHENTIK_TOKEN, AUTHENTIK_TEST_USER, AUTHENTIK_TEST_PASSWORD (uses default instance)
  authentik-<name>  → AUTHENTIK_URL, AUTHENTIK_TOKEN, AUTHENTIK_TEST_USER, AUTHENTIK_TEST_PASSWORD (specific instance, e.g. authentik-usc)
  glpi              → GLPI_URL, GLPI_APP_TOKEN, GLPI_USER_TOKEN
  github            → GITHUB_TOKEN
  gitea-mosaicstack → GITEA_URL, GITEA_TOKEN
  gitea-usc         → GITEA_URL, GITEA_TOKEN
  woodpecker        → WOODPECKER_URL, WOODPECKER_TOKEN (uses default instance)
  woodpecker-<name> → WOODPECKER_URL, WOODPECKER_TOKEN (specific instance, e.g. woodpecker-usc)
  cloudflare        → CLOUDFLARE_API_TOKEN (uses default instance)
  cloudflare-<name> → CLOUDFLARE_API_TOKEN (specific instance, e.g. cloudflare-personal)
  turbo-cache       → TURBO_API, TURBO_TOKEN, TURBO_TEAM
  openbrain         → OPENBRAIN_URL, OPENBRAIN_TOKEN
EOF
    return 0
  fi

  _mosaic_require_jq || return 1

  case "$service" in
    portainer)
      export PORTAINER_URL="${PORTAINER_URL:-$(_mosaic_read_cred '.portainer.url')}"
      export PORTAINER_API_KEY="${PORTAINER_API_KEY:-$(_mosaic_read_cred '.portainer.api_key')}"
      PORTAINER_URL="${PORTAINER_URL%/}"
      [[ -n "$PORTAINER_URL" ]] || { echo "Error: portainer.url not found" >&2; return 1; }
      [[ -n "$PORTAINER_API_KEY" ]] || { echo "Error: portainer.api_key not found" >&2; return 1; }
      ;;
    coolify)
      export COOLIFY_URL="${COOLIFY_URL:-$(_mosaic_read_cred '.coolify.url')}"
      export COOLIFY_TOKEN="${COOLIFY_TOKEN:-$(_mosaic_read_cred '.coolify.app_token')}"
      COOLIFY_URL="${COOLIFY_URL%/}"
      [[ -n "$COOLIFY_URL" ]] || { echo "Error: coolify.url not found" >&2; return 1; }
      [[ -n "$COOLIFY_TOKEN" ]] || { echo "Error: coolify.app_token not found" >&2; return 1; }
      ;;
    authentik-*)
      local ak_instance="${service#authentik-}"
      export AUTHENTIK_URL="$(_mosaic_read_cred ".authentik.${ak_instance}.url")"
      export AUTHENTIK_TOKEN="$(_mosaic_read_cred ".authentik.${ak_instance}.token")"
      export AUTHENTIK_TEST_USER="$(_mosaic_read_cred ".authentik.${ak_instance}.test_user.username")"
      export AUTHENTIK_TEST_PASSWORD="$(_mosaic_read_cred ".authentik.${ak_instance}.test_user.password")"
      export AUTHENTIK_INSTANCE="$ak_instance"
      AUTHENTIK_URL="${AUTHENTIK_URL%/}"
      [[ -n "$AUTHENTIK_URL" ]] || { echo "Error: authentik.${ak_instance}.url not found" >&2; return 1; }
      ;;
    authentik)
      local ak_default
      ak_default="${AUTHENTIK_INSTANCE:-$(_mosaic_read_cred '.authentik.default')}"
      if [[ -z "$ak_default" ]]; then
        # Fallback: try legacy flat structure (.authentik.url)
        local legacy_url
        legacy_url="$(_mosaic_read_cred '.authentik.url')"
        if [[ -n "$legacy_url" ]]; then
          export AUTHENTIK_URL="${AUTHENTIK_URL:-$legacy_url}"
          export AUTHENTIK_TOKEN="${AUTHENTIK_TOKEN:-$(_mosaic_read_cred '.authentik.token')}"
          export AUTHENTIK_TEST_USER="${AUTHENTIK_TEST_USER:-$(_mosaic_read_cred '.authentik.test_user.username')}"
          export AUTHENTIK_TEST_PASSWORD="${AUTHENTIK_TEST_PASSWORD:-$(_mosaic_read_cred '.authentik.test_user.password')}"
          AUTHENTIK_URL="${AUTHENTIK_URL%/}"
          [[ -n "$AUTHENTIK_URL" ]] || { echo "Error: authentik.url not found" >&2; return 1; }
        else
          echo "Error: authentik.default not set and no AUTHENTIK_INSTANCE env var" >&2
          echo "Available instances: $(jq -r '.authentik | keys | join(", ")' "$MOSAIC_CREDENTIALS_FILE" 2>/dev/null)" >&2
          return 1
        fi
      else
        load_credentials "authentik-${ak_default}"
      fi
      ;;
    glpi)
      export GLPI_URL="${GLPI_URL:-$(_mosaic_read_cred '.glpi.url')}"
      export GLPI_APP_TOKEN="${GLPI_APP_TOKEN:-$(_mosaic_read_cred '.glpi.app_token')}"
      export GLPI_USER_TOKEN="${GLPI_USER_TOKEN:-$(_mosaic_read_cred '.glpi.user_token')}"
      GLPI_URL="${GLPI_URL%/}"
      [[ -n "$GLPI_URL" ]] || { echo "Error: glpi.url not found" >&2; return 1; }
      ;;
    github)
      export GITHUB_TOKEN="${GITHUB_TOKEN:-$(_mosaic_read_cred '.github.token')}"
      [[ -n "$GITHUB_TOKEN" ]] || { echo "Error: github.token not found" >&2; return 1; }
      ;;
    gitea-mosaicstack)
      export GITEA_URL="${GITEA_URL:-$(_mosaic_read_cred '.gitea.mosaicstack.url')}"
      export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.mosaicstack.token')}"
      GITEA_URL="${GITEA_URL%/}"
      [[ -n "$GITEA_URL" ]] || { echo "Error: gitea.mosaicstack.url not found" >&2; return 1; }
      [[ -n "$GITEA_TOKEN" ]] || { echo "Error: gitea.mosaicstack.token not found" >&2; return 1; }
      ;;
    gitea-usc)
      export GITEA_URL="${GITEA_URL:-$(_mosaic_read_cred '.gitea.usc.url')}"
      export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.usc.token')}"
      GITEA_URL="${GITEA_URL%/}"
      [[ -n "$GITEA_URL" ]] || { echo "Error: gitea.usc.url not found" >&2; return 1; }
      [[ -n "$GITEA_TOKEN" ]] || { echo "Error: gitea.usc.token not found" >&2; return 1; }
      ;;
    woodpecker-*)
      local wp_instance="${service#woodpecker-}"
      # credentials.json is authoritative — always read from it, ignore env.
      # Backward compatibility: the default Mosaic Woodpecker instance may be
      # stored in the legacy flat schema (.woodpecker.url/.token) instead of
      # .woodpecker.mosaic.url/.token.
      if [[ "$wp_instance" == "mosaic" ]] && [[ -z "$(_mosaic_read_cred '.woodpecker.mosaic.url')" ]] && [[ -n "$(_mosaic_read_cred '.woodpecker.url')" ]]; then
        WOODPECKER_INSTANCE="mosaic" _mosaic_load_woodpecker_legacy
        return $?
      fi
      export WOODPECKER_URL="$(_mosaic_read_cred ".woodpecker.${wp_instance}.url")"
      export WOODPECKER_TOKEN="$(_mosaic_read_cred ".woodpecker.${wp_instance}.token")"
      export WOODPECKER_INSTANCE="$wp_instance"
      WOODPECKER_URL="${WOODPECKER_URL%/}"
      [[ -n "$WOODPECKER_URL" ]] || { echo "Error: woodpecker.${wp_instance}.url not found" >&2; return 1; }
      [[ -n "$WOODPECKER_TOKEN" ]] || { echo "Error: woodpecker.${wp_instance}.token not found" >&2; return 1; }
      # Sync to ~/.woodpecker/<instance>.env so the wp CLI wrapper stays current
      _mosaic_sync_woodpecker_env "$wp_instance" "$WOODPECKER_URL" "$WOODPECKER_TOKEN"
      ;;
    woodpecker)
      # Resolve default instance, then load it. If WOODPECKER_INSTANCE is set to
      # "mosaic" by a shell/profile but credentials.json still uses the legacy
      # flat .woodpecker.url/.token schema, load the flat credentials instead of
      # failing with "woodpecker.mosaic.url not found".
      local wp_default
      wp_default="${WOODPECKER_INSTANCE:-$(_mosaic_read_cred '.woodpecker.default')}"
      if [[ -z "$wp_default" ]]; then
        # Fallback: try legacy flat structure (.woodpecker.url / .woodpecker.token)
        local legacy_url
        legacy_url="$(_mosaic_read_cred '.woodpecker.url')"
        if [[ -n "$legacy_url" ]]; then
          _mosaic_load_woodpecker_legacy
        else
          echo "Error: woodpecker.default not set and no WOODPECKER_INSTANCE env var" >&2
          echo "Available instances: $(jq -r '.woodpecker | keys | join(", ")' "$MOSAIC_CREDENTIALS_FILE" 2>/dev/null)" >&2
          return 1
        fi
      else
        if [[ "$wp_default" == "mosaic" ]] && [[ -z "$(_mosaic_read_cred '.woodpecker.mosaic.url')" ]] && [[ -n "$(_mosaic_read_cred '.woodpecker.url')" ]]; then
          WOODPECKER_INSTANCE="mosaic" _mosaic_load_woodpecker_legacy
        else
          load_credentials "woodpecker-${wp_default}"
        fi
      fi
      ;;
    cloudflare-*)
      local cf_instance="${service#cloudflare-}"
      export CLOUDFLARE_API_TOKEN="${CLOUDFLARE_API_TOKEN:-$(_mosaic_read_cred ".cloudflare.${cf_instance}.api_token")}"
      export CLOUDFLARE_INSTANCE="$cf_instance"
      [[ -n "$CLOUDFLARE_API_TOKEN" ]] || { echo "Error: cloudflare.${cf_instance}.api_token not found" >&2; return 1; }
      ;;
    cloudflare)
      # Resolve default instance, then load it
      local cf_default
      cf_default="${CLOUDFLARE_INSTANCE:-$(_mosaic_read_cred '.cloudflare.default')}"
      if [[ -z "$cf_default" ]]; then
        echo "Error: cloudflare.default not set and no CLOUDFLARE_INSTANCE env var" >&2
        return 1
      fi
      load_credentials "cloudflare-${cf_default}"
      ;;
    turbo-cache)
      export TURBO_API="${TURBO_API:-$(_mosaic_read_cred '.turbo_cache.api_url')}"
      export TURBO_TOKEN="${TURBO_TOKEN:-$(_mosaic_read_cred '.turbo_cache.token')}"
      export TURBO_TEAM="${TURBO_TEAM:-$(_mosaic_read_cred '.turbo_cache.team')}"
      [[ -n "$TURBO_API" ]] || { echo "Error: turbo_cache.api_url not found" >&2; return 1; }
      [[ -n "$TURBO_TOKEN" ]] || { echo "Error: turbo_cache.token not found" >&2; return 1; }
      [[ -n "$TURBO_TEAM" ]] || { echo "Error: turbo_cache.team not found" >&2; return 1; }
      ;;
    openbrain)
      export OPENBRAIN_URL="${OPENBRAIN_URL:-$(_mosaic_read_cred '.openbrain.url')}"
      export OPENBRAIN_TOKEN="${OPENBRAIN_TOKEN:-$(_mosaic_read_cred '.openbrain.api_key')}"
      OPENBRAIN_URL="${OPENBRAIN_URL%/}"
      [[ -n "$OPENBRAIN_URL" ]] || { echo "Error: openbrain.url not found" >&2; return 1; }
      [[ -n "$OPENBRAIN_TOKEN" ]] || { echo "Error: openbrain.api_key not found" >&2; return 1; }
      ;;
    *)
      echo "Error: Unknown service '$service'" >&2
      echo "Supported: portainer, coolify, authentik[-<name>], glpi, github, gitea-mosaicstack, gitea-usc, woodpecker[-<name>], cloudflare[-<name>], turbo-cache, openbrain" >&2
      return 1
      ;;
  esac
}

# Common HTTP helper — makes a curl request and separates body from status code
# Usage: mosaic_http GET "/api/v1/endpoint" "Authorization: Bearer $TOKEN" [base_url]
# Returns: body on stdout, sets MOSAIC_HTTP_CODE
mosaic_http() {
  local method="$1"
  local endpoint="$2"
  local auth_header="$3"
  local base_url="${4:-}"

  local response
  local _tls; _tls=$(_mosaic_tls_opt "${base_url}${endpoint}")
  response=$(curl -sS $_tls -w "\n%{http_code}" -X "$method" \
    -H "$auth_header" \
    -H "Content-Type: application/json" \
    "${base_url}${endpoint}")

  MOSAIC_HTTP_CODE=$(echo "$response" | tail -n1)
  echo "$response" | sed '$d'
}

# POST variant with body
# Usage: mosaic_http_post "/api/v1/endpoint" "Authorization: Bearer $TOKEN" '{"key":"val"}' [base_url]
mosaic_http_post() {
  local endpoint="$1"
  local auth_header="$2"
  local data="$3"
  local base_url="${4:-}"

  local response
  local _tls; _tls=$(_mosaic_tls_opt "${base_url}${endpoint}")
  response=$(curl -sS $_tls -w "\n%{http_code}" -X POST \
    -H "$auth_header" \
    -H "Content-Type: application/json" \
    -d "$data" \
    "${base_url}${endpoint}")

  MOSAIC_HTTP_CODE=$(echo "$response" | tail -n1)
  echo "$response" | sed '$d'
}

# PATCH variant with body
mosaic_http_patch() {
  local endpoint="$1"
  local auth_header="$2"
  local data="$3"
  local base_url="${4:-}"

  local response
  local _tls; _tls=$(_mosaic_tls_opt "${base_url}${endpoint}")
  response=$(curl -sS $_tls -w "\n%{http_code}" -X PATCH \
    -H "$auth_header" \
    -H "Content-Type: application/json" \
    -d "$data" \
    "${base_url}${endpoint}")

  MOSAIC_HTTP_CODE=$(echo "$response" | tail -n1)
  echo "$response" | sed '$d'
}
