#!/usr/bin/env bash
# new-service: one command from nothing to a running, observed service.
#   new-service orders --kind backend --with postgres,redis --wait
#   new-service orders-web --kind frontend --api orders --wait
# Idempotent: every step checks before acting, so re-running after a failure
# continues where it stopped. See README.md.
set -euo pipefail
# shellcheck source=../lib/common.sh
source "$(dirname "$(readlink -f "$0")")/../lib/common.sh"

usage() {
  cat <<'EOF'
usage: new-service NAME --kind backend|frontend [options]

  --kind backend|frontend   template to generate from (required)
  --with LIST               backend only: comma list of postgres,redis
                            (runtime-config is always included)
  --api BACKEND             frontend only: route /api to BACKEND and deploy
                            into its namespace
  --public                  also serve https://NAME.$PUBLIC_DOMAIN through the
                            Cloudflare Tunnel (adds the DNS CNAME); default is
                            LAN-only. No Cloudflare Access on generated services.
  --wait                    after deploy, wait until Healthy and the health
                            endpoint answers; print elapsed time
  --timeout SECONDS         per-wait timeout (default 900)
  --dry-run                 print every API call, git push and kubectl
                            mutation instead of executing it
  -h, --help                this text
EOF
}

NAME='' KIND='' WITH='' API_APP='' WAIT=0 TIMEOUT=900 PUBLIC=0
while (($#)); do
  case $1 in
    --kind) KIND=${2:?--kind needs a value}; shift 2 ;;
    --with) WITH=${2:?--with needs a value}; shift 2 ;;
    --api) API_APP=${2:?--api needs a value}; shift 2 ;;
    --public) PUBLIC=1; shift ;;
    --wait) WAIT=1; shift ;;
    --timeout) TIMEOUT=${2:?--timeout needs a value}; shift 2 ;;
    --dry-run) DRY_RUN=1; shift ;;
    -h | --help) usage; exit 0 ;;
    -*) usage >&2; die "unknown option: $1" ;;
    *) [[ -z $NAME ]] || die "unexpected argument: $1"; NAME=$1; shift ;;
  esac
done

# ------------------------------------------------------------ 1. validate
[[ -n $NAME ]] || { usage >&2; die "NAME is required"; }
validate_name "$NAME"
[[ $TIMEOUT =~ ^[0-9]+$ ]] || die "--timeout must be a number of seconds"
WITH_PG=0 WITH_REDIS=0
case $KIND in
  backend)
    [[ -z $API_APP ]] || die "--api is for --kind frontend"
    IFS=, read -ra _with <<<"$WITH"
    for w in "${_with[@]}"; do
      case $w in
        postgres) WITH_PG=1 ;;
        redis) WITH_REDIS=1 ;;
        runtime-config | '') ;; # always on
        *) die "--with: unknown component '$w' (postgres, redis)" ;;
      esac
    done
    TEMPLATE_REPO=service-template-express
    NS=$NAME
    HEALTH_PATH=/readyz
    ;;
  frontend)
    [[ -z $WITH ]] || die "--with is for --kind backend"
    TEMPLATE_REPO=service-template-react
    if [[ -n $API_APP ]]; then
      validate_name "$API_APP"
      [[ $API_APP != "$NAME" ]] || die "--api must name a different service"
      NS=$API_APP
    else
      NS=$NAME
    fi
    HEALTH_PATH=/healthz
    ;;
  '') usage >&2; die "--kind is required" ;;
  *) die "--kind must be backend or frontend" ;;
esac

is_dry && log "DRY RUN: nothing below is executed except read-only checks"
load_config
gitops_sync
((!PUBLIC)) || cf_require

if [[ -n $API_APP ]]; then
  [[ -f $GITOPS_DIR/$APPS_DIR/$API_APP.yaml && -d $GITOPS_DIR/manifests/$API_APP ]] ||
    die "--api $API_APP: no such service in gitops (create it first)"
  [[ $(app_namespace "$API_APP") == "$API_APP" ]] ||
    die "--api $API_APP: not a backend (its namespace is '$(app_namespace "$API_APP")')"
fi
if [[ -f $GITOPS_DIR/$APPS_DIR/$NAME.yaml && $(app_namespace "$NAME") != "$NS" ]]; then
  die "gitops $APPS_DIR/$NAME.yaml exists with namespace '$(app_namespace "$NAME")', expected '$NS'"
fi
ok "validated $NAME (kind=$KIND ns=$NS postgres=$WITH_PG redis=$WITH_REDIS api=${API_APP:--})"

# ------------------------------------------------------------ 2. repo
if repo_exists "$NAME"; then
  ok "repo $FORGEJO_OWNER/$NAME exists"
  [[ $(jq -r .private <<<"$API_BODY") == false ]] || die "repo $FORGEJO_OWNER/$NAME is private; the cluster pulls images anonymously"
else
  log "generating $FORGEJO_OWNER/$NAME from $TEMPLATE_REPO"
  body=$(jq -nc --arg o "$FORGEJO_OWNER" --arg n "$NAME" '{owner:$o, name:$n, git_content:true, private:false}')
  api_write POST "/repos/$FORGEJO_OWNER/$TEMPLATE_REPO/generate" "$body"
  [[ $API_CODE == 201 || $API_CODE == 200 ]] || die "generate failed ($API_CODE): $API_BODY"
  ok "repo created: $FORGEJO_URL/$FORGEJO_OWNER/$NAME"
fi

# ------------------------------------------------------------ 3. Actions secrets
set_secret() {
  local body
  body=$(jq -nc --arg d "$2" '{data:$d}')
  api_write PUT "/repos/$FORGEJO_OWNER/$NAME/actions/secrets/$1" "$body" admin 1
  [[ $API_CODE =~ ^20[014]$ ]] || die "set secret $1 failed ($API_CODE): $API_BODY"
}
set_secret REGISTRY_USER "$FORGEJO_OWNER"
set_secret REGISTRY_TOKEN "$REGISTRY_TOKEN"
set_secret GITOPS_TOKEN "$GITOPS_TOKEN"
ok "Actions secrets REGISTRY_USER, REGISTRY_TOKEN, GITOPS_TOKEN set"

# ------------------------------------------------------------ 4. namespace + unleash token
if kubectl get namespace "$NS" >/dev/null 2>&1; then
  ok "namespace $NS exists"
else
  run kubectl create namespace "$NS" >/dev/null
  ok "namespace $NS created"
fi
if kubectl get secret -n "$NS" unleash-client-token >/dev/null 2>&1; then
  ok "secret $NS/unleash-client-token exists"
else
  # Copy the base64 values straight across; tokens are never decoded or put in argv.
  manifest=$(kubectl get secret -n unleash unleash-init-tokens -o json | jq --arg ns "$NS" '{
      apiVersion: "v1", kind: "Secret", type: "Opaque",
      metadata: {name: "unleash-client-token", namespace: $ns,
                 labels: {"app.kubernetes.io/managed-by": "platform-new-service"}},
      data: {token: .data.INIT_CLIENT_API_TOKENS, frontendToken: .data.INIT_FRONTEND_API_TOKENS}}')
  [[ $(jq -r '.data.token // empty' <<<"$manifest") ]] || die "could not read unleash/unleash-init-tokens"
  if is_dry; then
    dry "kubectl create -f - (Secret $NS/unleash-client-token from unleash/unleash-init-tokens)"
  else
    kubectl create -f - <<<"$manifest" >/dev/null
  fi
  ok "secret $NS/unleash-client-token created"
fi

# ------------------------------------------------------------ 5. gitops manifests
# $APPS_DIR/<name>.yaml is written only after CI's first image exists (step 7), so
# ArgoCD never syncs `newTag: latest` before there is an image to pull (the
# migrate-Job ImagePullBackOff caveat from Plan 04).
# public_block DIR: keep (markers only) or strip the `# __PUBLIC_BEGIN__..END__` host rule.
public_block() {
  if ((PUBLIC)); then
    sed -i '/# __PUBLIC_\(BEGIN\|END\)__/d' "$1"/*.yaml
  else
    sed -i '/# __PUBLIC_BEGIN__/,/# __PUBLIC_END__/d' "$1"/*.yaml
  fi
}

render_manifests() {
  local out=$1 t=$GITOPS_DIR/templates/$KIND
  cp -r "$t/." "$out/"
  rm -f "$out/README.md" "$out/application.yaml.tmpl"
  if [[ $KIND == backend ]]; then
    local extra=()
    if ((WITH_PG)); then
      cp "$GITOPS_DIR/templates/postgres/cluster.yaml" "$out/postgres.yaml"
      extra+=(postgres.yaml)
    else
      rm "$out/migrate-job.yaml"
      sed -i '/^  - migrate-job.yaml$/d' "$out/kustomization.yaml"
      sed -i '/- name: DATABASE_URL$/,/secretKeyRef:/d' "$out/deployment.yaml"
    fi
    if ((WITH_REDIS)); then
      cp "$GITOPS_DIR/templates/redis/redis.yaml" "$out/redis.yaml"
      extra+=(redis.yaml)
    else
      sed -i '/- name: REDIS_URL$/,+1d' "$out/deployment.yaml"
    fi
    # Replace the "Added by the render recipe" comment with the actual list.
    local add='' f
    for f in "${extra[@]}"; do add+="  - $f\n"; done
    sed -i "s|^  # Added by the render recipe.*$|${add%\\n}|" "$out/kustomization.yaml"
    sed -i '/^$/d' "$out/kustomization.yaml"
    public_block "$out"
    sed -i "s/__APP__/$NAME/g; s/__DOMAIN__/$INGRESS_DOMAIN/g; s/__PUBLIC_DOMAIN__/$PUBLIC_DOMAIN/g" "$out"/*.yaml
  else
    if [[ -z $API_APP ]]; then
      rm "$out/ingress-api.yaml" "$out/middleware.yaml"
      sed -i '/ingress-api.yaml$/d; /middleware.yaml$/d; /# \/api -> backend/d' "$out/kustomization.yaml"
    fi
    public_block "$out"
    # One Unleash URL serves both hosts; public services point browsers at the public name.
    local ud=$INGRESS_DOMAIN
    ((PUBLIC)) && ud=$PUBLIC_DOMAIN
    sed -i "s/__APP__/$NAME/g; s/__API__/$API_APP/g; s/__DOMAIN__/$INGRESS_DOMAIN/g; s/__PUBLIC_DOMAIN__/$PUBLIC_DOMAIN/g; s/__UNLEASH_DOMAIN__/$ud/g" "$out"/*.yaml
  fi
  ! grep -rnE '__(APP|API|NS|DOMAIN|PUBLIC_DOMAIN|UNLEASH_DOMAIN|PUBLIC_BEGIN|PUBLIC_END)__' "$out" || die "unrendered placeholder left in manifests"
  kustomize_build "$out" >/dev/null || die "kustomize build of rendered manifests failed"
}

if [[ -d $GITOPS_DIR/manifests/$NAME ]]; then
  ok "gitops manifests/$NAME exists"
else
  stage=$(tmpdir)
  render_manifests "$stage"
  ok "rendered + linted manifests ($(cd "$stage" && echo *.yaml))"
  if is_dry; then
    dry "write $GITOPS_DIR/manifests/$NAME/{$(cd "$stage" && printf "%s," *.yaml | sed "s/,$//")}"
    sed 's/^/    /' "$stage/kustomization.yaml" >&2
  else
    cp -r "$stage" "$GITOPS_DIR/manifests/$NAME"
    chmod 755 "$GITOPS_DIR/manifests/$NAME"
  fi
  gitops_commit_push "feat($NAME): scaffold $KIND manifests" "manifests/$NAME"
fi

# ------------------------------------------------------------ 6. clone + kick CI
APP_DIR=$CODE_DIR/$NAME
REPO_URL=$FORGEJO_URL/$FORGEJO_OWNER/$NAME.git
deployed_tag() { # tag CI has written to gitops origin/main ('' if none yet)
  git_auth -C "$GITOPS_DIR" fetch -q origin main 2>/dev/null || return 0
  git -C "$GITOPS_DIR" show "origin/main:manifests/$NAME/kustomization.yaml" 2>/dev/null |
    awk '/newTag:/{print $2}' | grep -vx latest || true
}

if [[ -d $APP_DIR/.git ]]; then
  ok "local clone $APP_DIR exists"
elif is_dry; then
  dry "git clone $REPO_URL $APP_DIR"
else
  git_auth clone -q "$REPO_URL" "$APP_DIR"
  ok "cloned to $APP_DIR"
fi

# A CI task is live/finished for the kick sha?
ci_state() { # prints: none | running | success | failure
  api GET "/repos/$FORGEJO_OWNER/$NAME/actions/tasks?limit=20"
  [[ $API_CODE == 200 ]] || { echo none; return; }
  jq -r --arg sha "$1" '[.workflow_runs[] | select(.head_sha == $sha) | .status] as $s
    | if ($s | length) == 0 then "none"
      elif any($s[]; . == "failure" or . == "cancelled") then "failure"
      elif any($s[]; . == "running" or . == "waiting" or . == "blocked") then "running"
      elif any($s[]; . == "success") and ($s | length) >= 2 then "success"
      else "running" end' <<<"$API_BODY"
}

TAG=$(deployed_tag)
if [[ -n $TAG ]]; then
  ok "CI already deployed $NAME:$TAG"
elif is_dry; then
  dry "git -C $APP_DIR commit --allow-empty -m 'chore: kick ci' && git push origin main"
  dry "poll gitops origin/main manifests/$NAME/kustomization.yaml until newTag != latest"
else
  git_auth -C "$APP_DIR" pull -q --ff-only origin main
  KICK_SHA=$(git -C "$APP_DIR" log -1 --format=%H --grep='^chore: kick ci$' origin/main || true)
  if [[ -n $KICK_SHA && $(ci_state "$KICK_SHA") != failure ]]; then
    ok "CI already kicked (${KICK_SHA:0:8})"
  else
    [[ -n $KICK_SHA ]] && warn "previous CI run for ${KICK_SHA:0:8} failed; kicking again"
    git -C "$APP_DIR" -c user.name="$FORGEJO_OWNER" -c user.email="$FORGEJO_OWNER@forgejo.local" \
      commit -q --allow-empty -m 'chore: kick ci'
    git_auth -C "$APP_DIR" push -q origin main
    KICK_SHA=$(git -C "$APP_DIR" rev-parse HEAD)
    ok "pushed 'chore: kick ci' (${KICK_SHA:0:8}); CI: $FORGEJO_URL/$FORGEJO_OWNER/$NAME/actions"
  fi

  # ---------------------------------------------------------- 7. wait for first image
  ci_done() {
    TAG=$(deployed_tag)
    [[ -n $TAG ]] && return 0
    [[ $(ci_state "$KICK_SHA") != failure ]] ||
      die "CI failed for ${KICK_SHA:0:8}: $FORGEJO_URL/$FORGEJO_OWNER/$NAME/actions (re-run new-service to kick again)"
    return 1
  }
  wait_until "$TIMEOUT" 10 "CI to build $NAME and bump its tag in gitops" ci_done
  ok "CI deployed $NAME:$TAG at $(elapsed)s"
fi

# ------------------------------------------------------------ 8. register with ArgoCD
if [[ -f $GITOPS_DIR/$APPS_DIR/$NAME.yaml ]]; then
  ok "gitops $APPS_DIR/$NAME.yaml exists"
else
  is_dry || git_auth -C "$GITOPS_DIR" pull -q --ff-only origin main
  if is_dry; then
    dry "write $GITOPS_DIR/$APPS_DIR/$NAME.yaml (namespace $NS)"
  else
    sed "s/__APP__/$NAME/g; s/__NS__/$NS/g" "$GITOPS_DIR/templates/$KIND/application.yaml.tmpl" \
      >"$GITOPS_DIR/$APPS_DIR/$NAME.yaml"
  fi
  gitops_commit_push "feat($NAME): register ArgoCD application" "$APPS_DIR/$NAME.yaml"
fi
# Don't wait for ArgoCD's 3-min poll.
argocd_refresh root

# ------------------------------------------------------------ 8b. public DNS
if ((PUBLIC)); then
  cf_cname_ensure "$NAME"
fi

# ------------------------------------------------------------ 9. wait for healthy
HOST=$NAME.$INGRESS_DOMAIN
if ((WAIT)) && ! is_dry; then
  app_ok() {
    local s
    s=$(kubectl get application -n argocd "$NAME" -o json 2>/dev/null |
      jq -r '"\(.status.sync.status)/\(.status.health.status)"') || return 1
    [[ $s == Synced/Healthy ]]
  }
  wait_until "$TIMEOUT" 10 "ArgoCD app $NAME Synced/Healthy" app_ok
  http_ok() {
    local p
    for p in "$@"; do
      [[ $(curl -s -o /dev/null -w '%{http_code}' --max-time 5 \
        --resolve "$HOST:443:$INGRESS_IP" "https://$HOST$p") == 200 ]] || return 1
    done
  }
  paths=("$HEALTH_PATH")
  [[ -n $API_APP ]] && paths+=(/api/readyz)
  wait_until 180 5 "https://$HOST${paths[*]} -> 200" http_ok "${paths[@]}"
  for p in "${paths[@]}"; do
    printf '  GET %s -> %s\n' "$p" "$(curl -s --max-time 5 --resolve "$HOST:443:$INGRESS_IP" "https://$HOST$p")" >&2
  done
fi

print_urls "$NAME" "$KIND" "$PUBLIC"
if is_dry; then
  log "dry run complete"
elif ((WAIT)); then
  ok "$NAME is live: $(elapsed)s from invocation"
else
  ok "$NAME registered; ArgoCD deploys it shortly (re-run with --wait to block until healthy)"
fi
