#!/bin/bash

set -e

export REPOSITORY_ROOT=$(realpath "$(dirname "$0")/..")
export LOCK_FILE="$REPOSITORY_ROOT/bin/start.lock"

# All worktrees share one infra stack under the "posthog" compose project. Pin it
# here so it never silently falls back to the worktree folder name when direnv
# hasn't exported it (.envrc) — that mismatch makes wait-for-docker query the wrong
# project and report db/redis/clickhouse/kafka as "missing" while docker ps shows
# them running. An explicit override still wins.
export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-posthog}"

# 1Password: auto-resolve op:// references in .env.local.
# Must run before lock acquisition: `exec op run ...` keeps the bash fds open
# in the op-run process, so if we held fd 9's flock here the re-exec'd child
# bin/start would fail to acquire it.
if [ -f "$REPOSITORY_ROOT/.env.local" ] && grep -q "op://" "$REPOSITORY_ROOT/.env.local" 2>/dev/null; then
    if [ -z "$_POSTHOG_OP_RESOLVED" ]; then
        if command -v op &>/dev/null; then
            echo "🔐 Resolving secrets from .env.local via 1Password"
            export _POSTHOG_OP_RESOLVED=1
            exec op run --env-file="$REPOSITORY_ROOT/.env.local" -- "$0" "$@"
        else
            echo "⚠️  .env.local contains 1Password refs (op://) but 'op' CLI is not installed."
            echo "   These refs will be skipped — services that depend on them will fail with their own"
            echo "   'missing key' errors. To resolve them automatically, install: brew install 1password-cli"
            echo "   Or replace op:// refs with literal values in .env.local."
        fi
    fi
fi

cleanup_lock() {
    rm -f "$LOCK_FILE"
}

# Acquire exclusive lock using flock (atomic, no race condition)
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
    echo "⚠️  Another instance of bin/start is already running (lock file: $LOCK_FILE)"
    echo "   To skip this check, delete the lock file: rm $LOCK_FILE"
    exit 1
fi

trap cleanup_lock EXIT INT TERM

# Environment variables are defined in:
#   .env.services     - service connection defaults (shared with containers
#                       and hobby via docker-compose env_file)
#   .env.development  - dev-mode runtime knobs (DEBUG, OTEL, etc.)
#   .env.local        - your overrides + 1Password secrets (gitignored,
#                       copy .env.local.example to get started)
#
# Source env files respecting precedence:
#   shell env > .env.local > .env.development > .env.services
# Only set vars that aren't already in the environment.
# op:// refs are always skipped here — they only get resolved by `op run` in
# the re-exec block above. Sourcing them as literals would set env vars to
# garbage strings that break downstream services with cryptic errors.
source_env_defaults() {
    if [[ ! -f "$1" ]]; then
        echo "⚠️  Expected env file missing: $1 — skipping. Did your checkout get truncated?" >&2
        return 0
    fi
    # Use explicit if/fi (not `[[ ... ]] && cmd`) so the loop body always
    # returns 0. With `&&`, the last iteration's `[[ -z … ]]` returning false
    # (variable already set) makes the function return non-zero, and `set -e`
    # silently kills the entire script — no error, no output, just exit. This
    # was the root cause of "hogli start" failing in Cursor and other terminals
    # where some of the trailing env vars happened to already be in the shell.
    while IFS='=' read -r name value; do
        [[ -z "$name" || "$name" == \#* ]] && continue
        # Substring match so quoted ("op://...") and space-padded values are
        # also caught — mirrors what op run itself accepts.
        [[ "$value" == *op://* ]] && continue
        if [[ -z "${!name:-}" ]]; then
            export "$name=$value"
        fi
    done < "$1"
    return 0
}
# .env.local: already in env if processed by op run, otherwise source directly
if [[ -f "$REPOSITORY_ROOT/.env.local" ]] && [[ -z "$_POSTHOG_OP_RESOLVED" ]]; then
    source_env_defaults "$REPOSITORY_ROOT/.env.local"
elif [[ ! -f "$REPOSITORY_ROOT/.env.local" ]] && [[ -f "$REPOSITORY_ROOT/.env.local.example" ]]; then
    echo "💡 No .env.local found — running with committed defaults only."
    echo "   For personal overrides or secrets: cp .env.local.example .env.local"
fi
source_env_defaults "$REPOSITORY_ROOT/.env.development"
source_env_defaults "$REPOSITORY_ROOT/.env.services"

# Limit Rust parallel compilation jobs to reduce CPU contention when multiple
# services start simultaneously via mprocs (they share a build lock anyway)
export CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-1}"

# Use sccache for Rust compilation caching (if available via flox or homebrew)
if [[ -z "${RUSTC_WRAPPER:-}" ]] && command -v sccache &>/dev/null; then
    export RUSTC_WRAPPER="sccache"
fi

# Use lld linker on macOS for faster Rust linking (if available via flox or homebrew)
if [[ "$(uname -s)" == "Darwin" ]] && [[ -z "${CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS:-}" ]] && command -v lld &>/dev/null; then
    export CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS="-C link-arg=-fuse-ld=lld"
fi

# Computed vars (need REPOSITORY_ROOT)
export DAGSTER_HOME=$REPOSITORY_ROOT/.dagster_home

# Tracing is disabled by default - requires 'tracing' intent for Jaeger/otel-collector
# Note: otel-collector uses significant CPU/memory resources
# Use --tracing flag to enable if you have the intent configured
if [[ "$*" == *"--tracing"* ]]; then
    export OTEL_SDK_DISABLED="false"
    export OTEL_TRACES_SAMPLER="parentbased_traceidratio"
    export OTEL_TRACES_SAMPLER_ARG="1"
    echo "👉 Tracing enabled, see http://localhost:16686 for Jaeger UI"
else
    export OTEL_SDK_DISABLED="true"
fi

# Vars below need bash variable expansion in their defaults (${PGHOST:-db},
# $PERSONS_DATABASE_URL, etc.) — must stay in this script because
# .env.development is sourced via a loader that doesn't re-expand $-refs.

# Persons DB — Node.js and Rust use different env var names for the same connection
export PERSONS_DATABASE_URL=${PERSONS_DATABASE_URL:-postgres://posthog:posthog@${PGHOST:-db}:${PGPORT:-5432}/posthog_persons}
export PERSONS_READONLY_DATABASE_URL=${PERSONS_READONLY_DATABASE_URL:-postgres://posthog:posthog@${PGHOST:-db}:${PGPORT:-5432}/posthog_persons}
# nosemgrep: env-default-belongs-in-env-development -- bash variable ref in default
export PERSONS_WRITE_DATABASE_URL=${PERSONS_WRITE_DATABASE_URL:-$PERSONS_DATABASE_URL}
# nosemgrep: env-default-belongs-in-env-development -- bash variable ref in default
export PERSONS_READ_DATABASE_URL=${PERSONS_READ_DATABASE_URL:-$PERSONS_READONLY_DATABASE_URL}
# nosemgrep: env-default-belongs-in-env-development -- bash variable ref in default
export PERSONS_DB_WRITER_URL=${PERSONS_DB_WRITER_URL:-$PERSONS_DATABASE_URL}
# nosemgrep: env-default-belongs-in-env-development -- bash variable ref in default
export PERSONS_DB_READER_URL=${PERSONS_DB_READER_URL:-$PERSONS_READONLY_DATABASE_URL}
export BEHAVIORAL_COHORTS_DATABASE_URL=${BEHAVIORAL_COHORTS_DATABASE_URL:-postgres://posthog:posthog@${PGHOST:-db}:${PGPORT:-5432}/behavioral_cohorts}
export FLAGS_READ_STORE_DATABASE_URL=${FLAGS_READ_STORE_DATABASE_URL:-postgres://posthog:posthog@${PGHOST:-db}:${PGPORT:-5432}/flags_read_store}
export CYCLOTRON_DATABASE_URL=${CYCLOTRON_DATABASE_URL:-postgres://posthog:posthog@${PGHOST:-db}:${PGPORT:-5432}/cyclotron}
export CYCLOTRON_NODE_DATABASE_URL=${CYCLOTRON_NODE_DATABASE_URL:-postgres://posthog:posthog@${PGHOST:-db}:${PGPORT:-5432}/cyclotron_node}

# Clean up orphaned dev processes left behind by a previous unclean shutdown.
# These "zombies" hold ports and memory and cause most local dev issues. Only
# orphans are killed — processes under an active process manager are left alone.
# Set HOGLI_SKIP_ZOMBIE_CHECK=1 to skip. Never let it fail the script (set -e).
if [[ -z "${HOGLI_SKIP_ZOMBIE_CHECK:-}" ]] && command -v hogli &>/dev/null; then
    hogli doctor:zombies --yes || true
fi

# Pre-flight: detect who already holds the host ports the dev stack needs. A stale
# stack under a *different* compose project binds the same ports, so one of our
# containers fails to bind — and that aborts the entire `docker compose up`,
# sometimes leaving a container detached from the compose network. In a TTY, offer
# to tear foreign stacks down; otherwise stay advisory. Delegated to hogli (tested
# there, see doctor_ports in hogli_commands/doctor.py).
#
# Also runs the one-time migration for the July 2026 named-volume switch: the named
# volume is absent until the first `up` after the switch, which would otherwise
# reset a clickhouse container's data that already exists under this project.
# Auto-salvages the old anonymous volumes when the mapping is unambiguous, otherwise
# prints the same manual-salvage warning as before. Must run before `docker compose
# up` below (see doctor_migrate_volumes in hogli_commands/doctor.py).
#
# Both skipped if hogli isn't on PATH, same as doctor:zombies above. Without hogli
# there's no auto-migration either, so warn plainly instead of silently losing data.
if command -v hogli &>/dev/null; then
    hogli doctor:ports || true
    hogli doctor:migrate-volumes || true
else
    named_volume_reset_notice() {
        command -v docker &>/dev/null || return 0
        docker volume inspect "${COMPOSE_PROJECT_NAME}_clickhouse-data" &>/dev/null && return 0
        if ! docker ps -a --filter 'label=com.docker.compose.service=clickhouse' \
            --format '{{.Label "com.docker.compose.project"}}' 2>/dev/null \
            | grep -qx "$COMPOSE_PROJECT_NAME"; then
            # No clickhouse container left — either a fresh clone (nothing to lose) or
            # `docker compose down` (removes containers but keeps volumes). Postgres has
            # no profile gate, so its volume surviving is a reliable "not a fresh clone"
            # signal, same as _has_prior_install in hogli_commands/doctor.py.
            docker volume inspect "${COMPOSE_PROJECT_NAME}_postgres-15-data" &>/dev/null || return 0
        fi
        echo "⚠️  Switching ClickHouse/ZooKeeper to named docker volumes. This first start"
        echo "   recreates them empty, so local ClickHouse data resets once (happens once"
        echo "   per machine). Install hogli to auto-migrate your existing data instead. See"
        echo "   'Local ClickHouse suddenly empty' in"
        echo "   docs/published/handbook/engineering/developing-locally.md to salvage old data."
    }
    named_volume_reset_notice || true
fi

# Geo files
./bin/download-mmdb

PROCESS_MANAGER="phrocs"
# PROCESS_MANAGER is the bare name (kept for telemetry); PROCESS_MANAGER_BIN is
# the executable to run, resolved per-manager below. For phrocs that's the
# in-repo source build when present, so it wins over anything on PATH.

# Detect detached mode early (`-d` is the shorthand, `--detach` the long
# form). Incompatible with --mprocs since mprocs has no detached mode; only
# phrocs supports it.
DETACHED=0
if [[ " $* " == *" --detach "* ]] || [[ " $* " == *" -d "* ]]; then
    DETACHED=1
    if [[ " $* " == *" --mprocs "* ]]; then
        echo "Error: -d / --detach requires phrocs (not compatible with --mprocs)"
        exit 1
    fi
fi

# Only use mprocs when --mprocs is explicitly passed
if [[ " $* " == *" --mprocs "* ]]; then
    # Ensure mprocs is available
    if ! command -v mprocs &>/dev/null; then
        if command -v brew &>/dev/null; then
            echo "🔁 Installing mprocs via Homebrew..."
            brew install mprocs
        else
            echo "👉 To use --mprocs, install mprocs: https://github.com/pvolok/mprocs#installation"
            exit 1
        fi
    fi
    PROCESS_MANAGER="mprocs"
    PROCESS_MANAGER_BIN="mprocs"
fi

if [[ "$PROCESS_MANAGER" == "phrocs" ]]; then
    # Resolve phrocs deterministically. The in-repo source build (produced by
    # flox activation via `make -C tools/phrocs build`) is canonical: it always
    # matches this checkout, so prefer it over anything on PATH. This stops a
    # stray brew/curl install from silently shadowing the version your branch
    # built. Non-flox setups have no dist binary and fall back to PATH.
    DIST_PHROCS="$REPOSITORY_ROOT/tools/phrocs/dist/phrocs"
    if [[ -x "$DIST_PHROCS" ]]; then
        PROCESS_MANAGER_BIN="$DIST_PHROCS"
    elif [[ -n "${FLOX_ENV_PROJECT:-}" ]]; then
        # Inside flox the source build is the only accepted binary — do NOT fall
        # back to PATH, or a stray brew/curl phrocs would shadow the checkout
        # build in exactly the failure mode this resolution is meant to prevent.
        echo "👉 phrocs isn't built yet. Re-activate your flox environment to build it"
        echo "   (leave and re-enter the repo directory, or run: flox activate),"
        echo "   or build it directly: hogli phrocs:build"
        exit 1
    elif PATH_PHROCS="$(command -v phrocs)"; then
        PROCESS_MANAGER_BIN="$PATH_PHROCS"
    else
        echo "👉 To run bin/start, install phrocs:"
        echo "     brew tap posthog/tap && brew install phrocs"
        echo "   or, without Homebrew:"
        echo "     curl -fsSL https://raw.githubusercontent.com/PostHog/posthog/master/tools/phrocs/install.sh | bash"
        exit 1
    fi
fi

# Use custom config, if provided (e.g. bin/start --custom bin/mprocs-custom.yaml)
# Ensure to provide config path after --custom flag
if [[ "$*" == *"--custom"* ]]; then
    # Extract the path after --custom
    config_path=""
    found_custom=false
    for i in "$@"; do
        if [[ $found_custom == true ]]; then
            config_path="$i"
            break
        fi
        if [[ "$i" == "--custom" ]]; then
            found_custom=true
        fi
    done
    if [[ -z "$config_path" ]]; then
        echo "Error: --custom requires a config path"
        exit 1
    fi
    if [[ "$DETACHED" == "1" ]]; then
        exec 9<&-
        "$PROCESS_MANAGER_BIN" --detach --config "$config_path"
    else
        "$PROCESS_MANAGER_BIN" --config "$config_path"
    fi
else
    GENERATED_CONFIG="$REPOSITORY_ROOT/.posthog/.generated/mprocs.yaml"

    # Generate/regenerate config using hogli (defaults to product_analytics if no config exists)
    # HOGLI_PROCESS_MANAGER is picked up by dev:generate for telemetry
    if command -v hogli &>/dev/null; then
        POSTHOG_TELEMETRY_OPT_OUT=1 HOGLI_PROCESS_MANAGER="$PROCESS_MANAGER" hogli dev:generate 2>/dev/null || true
    fi

    if [[ -f "$GENERATED_CONFIG" ]]; then
        CONFIG_TO_USE="$GENERATED_CONFIG"
    else
        # Fallback if hogli not available
        CONFIG_TO_USE="$REPOSITORY_ROOT/bin/mprocs.yaml"
    fi

    if [[ "$DETACHED" == "1" ]]; then
        # Close fd 9 before spawning the detached child. Go's exec.Command
        # inherits every open fd by default, and fd 9 holds this script's
        # flock on $LOCK_FILE. If the child inherits it, the lock stays held
        # for the child's whole lifetime even after bin/start exits —
        # blocking the next bin/start invocation.
        exec 9<&-
        "$PROCESS_MANAGER_BIN" --detach --config "$CONFIG_TO_USE"
    else
        "$PROCESS_MANAGER_BIN" --config "$CONFIG_TO_USE"
    fi
fi
