#!/usr/bin/env bash
# fake `devcontainer` CLI — see docs/internal/FAKE_DEVCONTAINER_CLI.md
set -u

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/fake-state.sh
. "$SCRIPT_DIR/../lib/fake-state.sh"

usage() {
  cat <<'EOF' >&2
fake devcontainer CLI
usage:
  devcontainer up --workspace-folder <path> [--remove-existing-container] [--id-label k=v ...]
  devcontainer read-configuration --workspace-folder <path>
  devcontainer reset                # fake-only: wipe all container state
EOF
}

cmd_up() {
  workspace=""
  remove_existing=0
  while [ $# -gt 0 ]; do
    case "$1" in
      --workspace-folder)
        workspace="${2:-}"; shift 2 ;;
      --workspace-folder=*)
        workspace="${1#*=}"; shift ;;
      --remove-existing-container)
        remove_existing=1; shift ;;
      --id-label|--mount-workspace-git-root|--config|--override-config)
        # value-taking flags we don't act on
        shift 2 ;;
      --skip-non-blocking-commands|--update-remote-user-uid-default=*|--user-data-folder=*)
        shift ;;
      *)
        # Unknown flag/value — ignore so we stay forgiving.
        shift ;;
    esac
  done

  if [ -z "$workspace" ]; then
    echo "fake-devcontainer: --workspace-folder is required" >&2
    exit 64
  fi

  state="$(fake_state_init)"

  delay_ms="${FAKE_DC_UP_DELAY_MS:-150}"
  lines="${FAKE_DC_UP_LINES:-6}"

  # If asked to remove existing, mark any running container for this
  # workspace stopped before allocating a new id.
  if [ "$remove_existing" = "1" ]; then
    existing="$(fake_find_container_for_workspace "$workspace" || true)"
    if [ -n "$existing" ]; then
      echo "stopped" > "$state/containers/$existing/status"
      printf '[%s] stopping existing container %s\n' "$(date +%T)" "$existing" >&2
      fake_sleep_ms "$delay_ms"
    fi
  fi

  # Reuse a running container for this workspace unless we just removed.
  reuse=""
  if [ "$remove_existing" != "1" ]; then
    reuse="$(fake_find_container_for_workspace "$workspace" || true)"
  fi

  if [ -n "$reuse" ]; then
    container_id="$reuse"
    printf '[%s] reusing container %s\n' "$(date +%T)" "$container_id" >&2
    fake_sleep_ms "$delay_ms"
  else
    container_id="$(fake_alloc_id "$workspace")"
  fi

  cdir="$state/containers/$container_id"
  mkdir -p "$cdir"
  printf '%s\n' "$workspace" > "$cdir/workspace"
  date +%s > "$cdir/created_at"
  printf 'running\n' > "$cdir/status"

  # Locate the devcontainer.json — used by the user-fallback parsing
  # below and by the containerEnv extractor further down.
  dc_cfg=""
  for cfg in \
    "$workspace/.devcontainer/devcontainer.json" \
    "$workspace/.devcontainer.json"; do
    if [ -f "$cfg" ]; then dc_cfg="$cfg"; break; fi
  done

  # Resolve `remoteUser` per spec: explicit `remoteUser` wins, else
  # fall back to `containerUser`, else leave the field unset. Tests
  # can override either via env. Real `@devcontainers/cli` does the
  # same precedence; the plugin's authority builder reads back
  # whatever JSON we emit and uses `null` when the key is missing.
  config_remote_user=""
  config_container_user=""
  if [ -n "$dc_cfg" ]; then
    config_remote_user="$(extract_string_field "remoteUser" "$dc_cfg")"
    config_container_user="$(extract_string_field "containerUser" "$dc_cfg")"
  fi
  if [ -n "${FAKE_DC_REMOTE_USER+set}" ]; then
    # Explicit env override (allow empty string to mean "unset").
    resolved_remote_user="$FAKE_DC_REMOTE_USER"
  elif [ -n "$config_remote_user" ]; then
    resolved_remote_user="$config_remote_user"
  elif [ -n "$config_container_user" ]; then
    resolved_remote_user="$config_container_user"
  else
    resolved_remote_user=""
  fi
  printf '%s\n' "$resolved_remote_user" > "$cdir/remote_user"

  remote_workspace="${FAKE_DC_REMOTE_WORKSPACE:-/workspaces/$(basename "$workspace")}"
  printf '%s\n' "$remote_workspace" > "$cdir/remote_workspace"

  # Record `containerEnv` from devcontainer.json (or the .devcontainer/
  # subfolder variants) into a flat KEY=VALUE file so `docker exec`
  # can replay it. Real devcontainers bake containerEnv into the
  # container's runtime env at create time; this is the cheapest
  # faithful simulation. The parser is deliberately small — single-
  # line `"key": "value"` entries only — to avoid pulling in `jq` or
  # python.
  : > "$cdir/container_env"
  if [ -n "$dc_cfg" ]; then
    cfg="$dc_cfg"
    # Slice out the line range starting at the `"containerEnv": {`
    # opener through the next `}` (inclusive). Both the multi-line
    # block form and the inline `{ "K": "V" }` form are covered:
    # for inline, the opener and closer are on the same line, which
    # `sed`'s `/A/,/B/p` collapses to just that line. Strip JSONC
    # `//` comments, then pluck every `"K": "V"` pair on the
    # selected lines except the outer `"containerEnv": ` itself.
    sed -n '/"containerEnv"[[:space:]]*:[[:space:]]*{/,/}/p' "$cfg" \
      | sed 's|//.*$||' \
      | grep -oE '"[^"]+"[[:space:]]*:[[:space:]]*"[^"]*"' \
      | grep -v '^"containerEnv"' \
      | sed -E 's/^"([^"]+)"[[:space:]]*:[[:space:]]*"([^"]*)"$/\1=\2/' \
      >> "$cdir/container_env"
  fi
  # Allow tests to override / add via env. Format: `K=V;K=V;...`.
  if [ -n "${FAKE_DC_CONTAINER_ENV:-}" ]; then
    IFS=';' read -r -a _envs <<<"$FAKE_DC_CONTAINER_ENV"
    for kv in "${_envs[@]}"; do
      [ -n "$kv" ] && printf '%s\n' "$kv" >> "$cdir/container_env"
    done
  fi

  # Default port mappings if a forwardPorts mock isn't supplied.
  : > "$cdir/ports"
  if [ -n "${FAKE_DC_PORTS:-}" ]; then
    # Comma-separated list like "8080,5432"
    next=32768
    IFS=',' read -r -a _p <<<"$FAKE_DC_PORTS"
    for p in "${_p[@]}"; do
      printf '%s/tcp -> 0.0.0.0:%s\n' "$p" "$next" >> "$cdir/ports"
      next=$((next+1))
    done
  fi

  # Stream phase lines on stderr with sleeps so the build-log split has
  # something to scroll. The leading `[+] Building` mirrors the look of
  # the real devcontainer/docker buildx output.
  : > "$cdir/logs"
  emit_progress() {
    line="$1"
    printf '%s\n' "$line" >&2
    printf '%s\n' "$line" >> "$cdir/logs"
  }

  emit_progress "[+] Building 0.0s ..."
  fake_sleep_ms "$delay_ms"
  emit_progress "[+] Building 0.5s ...   resolving image"
  fake_sleep_ms "$delay_ms"

  if [ "${FAKE_DC_UP_HANG:-0}" = "1" ]; then
    emit_progress "[+] Hanging on purpose for cancel test (FAKE_DC_UP_HANG=1)"
    # Sleep forever; cancel-attach should kill us.
    while true; do sleep 30; done
  fi

  i=1
  while [ "$i" -le "$lines" ]; do
    emit_progress "[+] Building $(awk -v i="$i" 'BEGIN { printf "%.1f", i*0.7 + 1 }')s ...   pulling layers ($i/$lines)"
    fake_sleep_ms "$delay_ms"
    i=$((i+1))
  done

  if [ "${FAKE_DC_UP_FAIL:-0}" = "1" ]; then
    reason="${FAKE_DC_UP_FAIL_REASON:-image not found: ubuntu:bogus}"
    emit_progress "error: $reason"
    printf 'stopped\n' > "$cdir/status"
    exit 1
  fi

  emit_progress "[+] Starting container $container_id"
  fake_sleep_ms "$delay_ms"

  # Run the post-attach lifecycle hooks in spec order, mirroring
  # what `@devcontainers/cli up` does inside the container. The
  # plugin only runs `initializeCommand` itself (host-side, before
  # invoking us); everything from `onCreateCommand` through
  # `postAttachCommand` is the CLI's job. Each hook runs as
  # `sh -c "<value>"` from the workspace cwd, with the recorded
  # containerEnv exported. Output is folded into the build log
  # via `emit_progress`.
  #
  # Limitations of the fake parser: only the string form is
  # supported here. Object form (parallel) and array form (exec)
  # are out of scope — the plugin's `devcontainer_on_lifecycle_confirmed`
  # is what tests like R1/R2 cover for those, not this codepath.
  if [ -n "$dc_cfg" ] && [ "${FAKE_DC_UP_FAIL:-0}" != "1" ]; then
    # Re-export containerEnv so hooks see it (matches real
    # docker exec semantics; the plugin's `containerEnv` test
    # depends on this in the post-attach lifecycle picker too).
    if [ -f "$cdir/container_env" ]; then
      while IFS= read -r line; do
        [ -z "$line" ] && continue
        export "$line"
      done < "$cdir/container_env"
    fi
    # Honor `waitFor` per spec: hooks up to and including the named
    # one block `up`'s return; hooks AFTER it run in the background
    # so the editor can become "ready" before slow post-create work
    # finishes. Default is `updateContentCommand`. The named hook
    # itself runs synchronously (i.e. waitFor is inclusive).
    wait_for="$(extract_string_field "waitFor" "$dc_cfg")"
    [ -n "$wait_for" ] || wait_for="updateContentCommand"

    in_pre=1
    for hook in onCreateCommand updateContentCommand postCreateCommand postStartCommand postAttachCommand; do
      raw="$(extract_string_field "$hook" "$dc_cfg")"
      if [ -n "$raw" ]; then
        if [ "$in_pre" = "1" ]; then
          emit_progress "[+] Running $hook"
          # Run from the workspace dir so $PWD matches the host
          # workspace path the user expects.
          if ! ( cd "$workspace" && sh -c "$raw" ) >&2; then
            emit_progress "error: $hook exited non-zero"
            printf 'stopped\n' > "$cdir/status"
            exit 1
          fi
        else
          emit_progress "[+] Backgrounding $hook (waitFor=$wait_for)"
          # Background hook so `up` returns to the editor while
          # slow setup keeps running. We avoid `setsid` because
          # macOS doesn't ship it by default. The double-fork
          # below detaches the inner shell from this script's
          # process group: the outer subshell exits immediately
          # after spawning the inner one (with `&`), which makes
          # the inner shell's parent become PID 1 (init) — same
          # effect as setsid for our purposes (survival across
          # `up`'s exit + own session). Output goes to /dev/null
          # because tests assert via the hook's own side effects
          # (sentinel files, env probes), and the build log is
          # closed by the time `up` returns anyway.
          (
            ( cd "$workspace" && sh -c "$raw" ) </dev/null >/dev/null 2>&1 &
          ) </dev/null >/dev/null 2>&1
          disown 2>/dev/null || true
        fi
        fake_sleep_ms "$delay_ms"
      fi
      if [ "$hook" = "$wait_for" ]; then
        in_pre=0
      fi
    done
  fi

  # Final JSON outcome on stdout. Plugin scans stdout backwards for the
  # last line that parses as JSON, so trailing newline placement is fine.
  printf '%s\n' "$container_id" > "$state/last_id"

  # Build the optional `remoteUser` JSON fragment so an unset user
  # yields `{...,"remoteWorkspaceFolder":"..."}` (no remoteUser key).
  # The plugin's authority builder treats `null`/missing the same —
  # both result in no `-u` flag passed to `docker exec`.
  resolved_user_value="$(cat "$cdir/remote_user")"
  if [ -n "$resolved_user_value" ]; then
    remote_user_frag="\"remoteUser\":\"$resolved_user_value\","
  else
    remote_user_frag=""
  fi

  if [ "${FAKE_DC_UP_BAD_JSON:-0}" = "1" ]; then
    echo "{ this is not valid json"
  elif [ "${FAKE_DC_UP_NO_CONTAINER_ID:-0}" = "1" ]; then
    printf '{"outcome":"success",%s"remoteWorkspaceFolder":"%s"}\n' \
      "$remote_user_frag" \
      "$(cat "$cdir/remote_workspace")"
  else
    printf '{"outcome":"success","containerId":"%s",%s"remoteWorkspaceFolder":"%s"}\n' \
      "$container_id" \
      "$remote_user_frag" \
      "$(cat "$cdir/remote_workspace")"
  fi
}

cmd_read_configuration() {
  # Stub — Fresh doesn't currently call this, but we accept it so we don't
  # explode if someone wires it up.
  printf '{"configuration":{},"workspace":{}}\n'
}

cmd_reset() {
  state="$(fake_state_init)"
  rm -rf "$state/containers" "$state/last_id"
  mkdir -p "$state/containers"
  echo "fake-devcontainer: reset $state" >&2
}

main() {
  if [ $# -eq 0 ]; then usage; exit 64; fi
  sub="$1"; shift
  case "$sub" in
    up)                 cmd_up "$@" ;;
    read-configuration) cmd_read_configuration "$@" ;;
    reset)              cmd_reset "$@" ;;
    # Fresh probes CLI presence by running `devcontainer --version`
    # (the real CLI prints its version and exits 0).
    --version)          printf '0.0.0-fake\n'; exit 0 ;;
    --help|-h|help)     usage; exit 0 ;;
    *)
      echo "fake-devcontainer: unsupported subcommand: $sub" >&2
      exit 64
      ;;
  esac
}

main "$@"
