#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REGISTRY_TOOL="${SCRIPT_DIR}/instance-registry.py"

IMAGE_NAME="${IMAGE_NAME:-beaver/app-instance:latest}"
INSTANCES_ROOT_DEFAULT="${SCRIPT_DIR}/runtime/instances"
REGISTRY_PATH_DEFAULT="${SCRIPT_DIR}/runtime/registry/instances.json"
KNOWN_PROVIDERS=" custom anthropic openai openrouter deepseek groq zhipu dashscope vllm gemini moonshot minimax aihubmix siliconflow volcengine "

INSTANCE_ID=""
INSTANCE_SLUG=""
CONTAINER_NAME=""
HOST_PORT=""
PUBLIC_URL=""
AUTHZ_BASE_URL=""
AUTHZ_INTERNAL_TOKEN=""
AUTHZ_OUTLOOK_MCP_URL=""
OUTLOOK_MCP_SERVER_ID="${OUTLOOK_MCP_SERVER_ID:-outlook_mcp}"
OUTLOOK_MCP_CALL_TIMEOUT_SECONDS="${OUTLOOK_MCP_CALL_TIMEOUT_SECONDS:-60}"
USER_FILES_MAX_UPLOAD_BYTES="${USER_FILES_MAX_UPLOAD_BYTES:-}"
EXTERNAL_CONNECTOR_BASE_URL="${EXTERNAL_CONNECTOR_BASE_URL:-http://external-connector:8787}"
EXTERNAL_CONNECTOR_TOKEN="${EXTERNAL_CONNECTOR_TOKEN:-}"
BEAVER_BRIDGE_TOKEN="${BEAVER_BRIDGE_TOKEN:-}"
EXTERNAL_CONNECTOR_CALLBACK_BASE_URL="${EXTERNAL_CONNECTOR_CALLBACK_BASE_URL:-}"
BACKEND_ID=""
CLIENT_ID=""
CLIENT_SECRET=""
BACKEND_NAME=""
MODEL="openai/gpt-5"
PROVIDER="openai"
API_KEY="${API_KEY:-}"
API_BASE="${API_BASE:-}"
SKIP_PROVIDER_CONFIG=0
AUTH_USERNAME=""
AUTH_PASSWORD=""
USERNAME=""
EMAIL=""
INSTANCE_HOST=""
AUTH_PORTAL_URL="${AUTH_PORTAL_URL:-}"
AUTH_PORTAL_PORT="${AUTH_PORTAL_PORT:-3081}"
INSTANCES_ROOT="${INSTANCES_ROOT:-$INSTANCES_ROOT_DEFAULT}"
REGISTRY_PATH="${REGISTRY_PATH:-$REGISTRY_PATH_DEFAULT}"
NETWORK_NAME="${NETWORK_NAME:-}"
HOST_BIND_IP="${HOST_BIND_IP:-127.0.0.1}"
INITIAL_SKILLS_DIR="${INITIAL_SKILLS_DIR:-${SCRIPT_DIR}/../skills}"
INITIAL_SKILLS_EXCLUDE="${INITIAL_SKILLS_EXCLUDE:-officebench-mcp}"
SEED_INITIAL_SKILLS=1
FORCE_BUILD=0
REPLACE=0

usage() {
  cat <<'EOF'
Usage:
  ./create-instance.sh --instance-id demo --auth-username admin --auth-password 123456 [options]

Required:
  --instance-id <id>          Unique instance id.
  --auth-username <name>      Initial web login username.
  --auth-password <password>  Initial web login password.

Optional:
  --image <name>              Docker image tag. Default: beaver/app-instance:latest
  --container-name <name>     Docker container name. Default: app-instance-<slug>
  --host-port <port>          Host port to publish. Default: auto-pick from 20000-29999.
  --public-url <url>          Public URL exposed to users. Default: http://127.0.0.1:<host-port>
  --provider <name>           Provider key in config.json. Default: openai
  --api-base <url>            Optional custom provider base URL.
  --api-key <key>             Provider API key for Boardware Genius.
  --model <name>              Model name. Default: openai/gpt-5
  --skip-provider-config      Create the instance without model/provider/API key settings.
  --authz-base-url <url>      AuthZ service base URL.
  --authz-internal-token <token>
                              AuthZ internal token for backend-only user file storage settings lookup.
  --authz-outlook-mcp-url <url>
                              Managed Outlook MCP URL for AuthZ mode.
  --outlook-mcp-server-id <id>
                              Default Outlook MCP server id. Default: outlook_mcp
  --outlook-mcp-call-timeout-seconds <seconds>
                              Backend wait timeout for Outlook MCP calls. Default: 60
  --user-files-max-upload-bytes <bytes>
                              Optional max upload size for the user file system.
  --external-connector-base-url <url>
                              External connector sidecar URL. Default: http://external-connector:8787
  --external-connector-token <token>
                              Service token used for Beaver-to-sidecar requests.
  --external-connector-callback-base-url <url>
                              Internal URL the sidecar should call back for inbound events.
                              Default: http://<container-name>:8080 when a Docker network is used.
  --bridge-token <token>      Service token accepted from the connector bridge.
  --backend-id <id>           Pre-assigned backend id.
  --client-id <id>            Pre-assigned AuthZ client id.
  --client-secret <secret>    Pre-assigned AuthZ client secret.
  --backend-name <name>       Display name in backend identity.
  --auth-portal-url <url>     Shared auth portal URL used when building the image.
  --auth-portal-port <port>   Fallback auth portal port. Default: 3081
  --username <name>           Registry username owner. Default: auth username
  --email <value>             Registry email owner.
  --instance-host <host>      Public host used by reverse proxy, for registry only.
  --instances-root <path>     Instance data root. Default: ./runtime/instances
  --registry <path>           Registry JSON path. Default: ./runtime/registry/instances.json
  --network <name>            Optional docker network name.
  --host-bind-ip <ip>         Host bind IP for published port. Default: 127.0.0.1
  --initial-skills-dir <path> Directory copied into workspace/skills on first create.
                              Default: ../skills
  --skip-initial-skills       Do not seed initial workspace skills.
  --build                     Force rebuild image before running.
  --replace                   Remove existing container with same name before running.
  --help                      Show this help.
EOF
}

log() {
  printf '[create-instance] %s\n' "$*"
}

die() {
  printf '[create-instance] %s\n' "$*" >&2
  exit 1
}

slugify() {
  local input="$1"
  local output
  output="$(printf '%s' "$input" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9._-]+/-/g; s/^-+//; s/-+$//')"
  if [[ -z "$output" ]]; then
    die "instance id produced an empty slug"
  fi
  printf '%s' "$output"
}

pick_free_port() {
  local args=(
    --registry "$REGISTRY_PATH"
    next-port
    --start 20000
    --end 29999
  )
  if [[ -n "$INSTANCE_ID" ]]; then
    args+=(--exclude-instance-id "$INSTANCE_ID")
  fi
  "$REGISTRY_TOOL" "${args[@]}"
}

extract_url_host() {
  local input_url="$1"
  INPUT_URL="$input_url" python3 - <<'PY'
import os
from urllib.parse import urlsplit

value = os.environ["INPUT_URL"].strip()
parts = urlsplit(value)
print(parts.hostname or "")
PY
}

render_config_json() {
  local target_path="$1"

  TARGET_PATH="$target_path" \
  MODEL="$MODEL" \
  PROVIDER="$PROVIDER" \
  API_KEY="$API_KEY" \
  API_BASE="$API_BASE" \
  SKIP_PROVIDER_CONFIG="$SKIP_PROVIDER_CONFIG" \
  AUTHZ_BASE_URL="$AUTHZ_BASE_URL" \
  AUTHZ_INTERNAL_TOKEN="$AUTHZ_INTERNAL_TOKEN" \
  AUTHZ_OUTLOOK_MCP_URL="$AUTHZ_OUTLOOK_MCP_URL" \
  OUTLOOK_MCP_SERVER_ID="$OUTLOOK_MCP_SERVER_ID" \
  BACKEND_ID="$BACKEND_ID" \
  CLIENT_ID="$CLIENT_ID" \
  CLIENT_SECRET="$CLIENT_SECRET" \
  BACKEND_NAME="$BACKEND_NAME" \
  PUBLIC_URL="$PUBLIC_URL" \
  python3 - <<'PY'
import json
import os
from pathlib import Path

target = Path(os.environ["TARGET_PATH"])
provider = os.environ["PROVIDER"]
outlook_mcp_url = os.environ["AUTHZ_OUTLOOK_MCP_URL"].strip()
outlook_server_id = os.environ["OUTLOOK_MCP_SERVER_ID"].strip() or "outlook_mcp"
skip_provider_config = os.environ["SKIP_PROVIDER_CONFIG"].strip() == "1"

providers = {}
agent_defaults = {
    "workspace": "/root/.beaver/workspace",
}
if not skip_provider_config:
    provider_cfg = {"apiKey": os.environ["API_KEY"]}
    api_base = os.environ["API_BASE"].strip()
    if api_base:
        provider_cfg["apiBase"] = api_base
    providers[provider] = provider_cfg
    agent_defaults["provider"] = provider
    agent_defaults["model"] = os.environ["MODEL"]

outlook_tool_names = [
    "auth_status",
    "mail_list_folders",
    "mail_list_messages",
    "mail_search_messages",
    "mail_get_message",
    "mail_send_email",
    "mail_reply_to_message",
    "mail_forward_message",
    "mail_move_message",
    "mail_delta_sync",
    "calendar_list_events",
    "calendar_create_event",
    "calendar_update_event",
    "calendar_get_schedule",
    "calendar_find_meeting_times",
    "calendar_delta_sync",
]

default_mcp_servers = {}
if outlook_mcp_url:
    default_mcp_servers[outlook_server_id] = {
        "command": "",
        "args": [],
        "env": {},
        "url": outlook_mcp_url,
        "headers": {},
        "authMode": "oauth_backend_token",
        "authAudience": f"mcp:{outlook_server_id}",
        "authScopes": ["list_tools", *[f"tool:{name}" for name in outlook_tool_names]],
        "toolTimeout": 60,
        "sensitive": True,
    }

data = {
    "agents": {
        "defaults": agent_defaults
    },
    "providers": providers,
    "tools": {
        "restrictToWorkspace": True,
        "mcpServers": default_mcp_servers,
    },
    "authz": {
        "enabled": bool(os.environ["AUTHZ_BASE_URL"].strip()),
        "baseUrl": os.environ["AUTHZ_BASE_URL"].strip() or "http://127.0.0.1:19090",
        "requestTimeoutSeconds": 10,
        "outlookMcpUrl": outlook_mcp_url,
    },
    "backend_identity": {
        "backendId": os.environ["BACKEND_ID"].strip(),
        "clientId": os.environ["CLIENT_ID"].strip(),
        "clientSecret": os.environ["CLIENT_SECRET"].strip(),
        "name": os.environ["BACKEND_NAME"].strip(),
        "publicBaseUrl": os.environ["PUBLIC_URL"].strip(),
    },
    "channels": {
        "telegram-main": {
            "enabled": False,
            "kind": "telegram",
            "mode": "polling",
            "accountId": "bot-main",
            "displayName": "Telegram Main",
            "secrets": {
                "botToken": "",
            },
            "config": {
                "requireMentionInGroups": True,
                "maxMessageChars": 4096,
            },
        },
        "feishu-main": {
            "enabled": False,
            "kind": "feishu",
            "mode": "websocket",
            "accountId": "tenant-main",
            "displayName": "Feishu Main",
            "secrets": {
                "appId": "",
                "appSecret": "",
            },
            "config": {
                "domain": "feishu",
                "connectionMode": "websocket",
                "requireMentionInGroups": True,
            },
        },
        "qqbot-main": {
            "enabled": False,
            "kind": "qqbot",
            "mode": "websocket",
            "accountId": "qqbot-main",
            "displayName": "QQ Bot Main",
            "secrets": {
                "appId": "",
                "clientSecret": "",
            },
            "config": {
                "dmPolicy": "open",
                "groupPolicy": "allowlist",
                "markdownSupport": False,
            },
        },
        "weixin-main": {
            "enabled": False,
            "kind": "weixin",
            "mode": "polling",
            "accountId": "wx-main",
            "displayName": "Weixin Main",
            "secrets": {
                "token": "",
            },
            "config": {
                "dmPolicy": "open",
                "groupPolicy": "disabled",
                "textBatchDelaySeconds": 0.5,
            },
        },
        "terminal-dev": {
            "enabled": True,
            "kind": "terminal",
            "mode": "websocket",
            "accountId": "local",
            "displayName": "Terminal Dev",
            "config": {},
            "secrets": {},
        },
    },
}

target.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
PY
}

render_auth_users_json() {
  local target_path="$1"

  TARGET_PATH="$target_path" \
  AUTH_USERNAME="$AUTH_USERNAME" \
  AUTH_PASSWORD="$AUTH_PASSWORD" \
  python3 - <<'PY'
import json
import os
from pathlib import Path

target = Path(os.environ["TARGET_PATH"])
data = {
    "users": [
        {
            "username": os.environ["AUTH_USERNAME"],
            "password": os.environ["AUTH_PASSWORD"],
        }
    ]
}
target.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
PY
}

seed_initial_skills() {
  local workspace_path="$1"
  local initial_skills_dir="$2"
  local target_dir="${workspace_path}/skills"

  if [[ "$SEED_INITIAL_SKILLS" -ne 1 ]]; then
    return
  fi
  if [[ ! -d "$initial_skills_dir" ]]; then
    log "initial skills directory not found, skipping: ${initial_skills_dir}"
    return
  fi

  mkdir -p "$target_dir"
  INITIAL_SKILLS_DIR="$initial_skills_dir" TARGET_DIR="$target_dir" INITIAL_SKILLS_EXCLUDE="$INITIAL_SKILLS_EXCLUDE" python3 - <<'PY'
import json
import shutil
import os
from pathlib import Path

initial = Path(os.environ["INITIAL_SKILLS_DIR"]).resolve()
target = Path(os.environ["TARGET_DIR"]).resolve()
excluded = {item.strip() for item in os.environ.get("INITIAL_SKILLS_EXCLUDE", "").split(",") if item.strip()}

for child in sorted(initial.iterdir()):
    if child.name.startswith("."):
        continue
    if child.name in excluded:
        continue
    destination = target / child.name
    if destination.exists():
        continue
    if child.is_dir():
        shutil.copytree(child, destination)
    elif child.is_file():
        shutil.copy2(child, destination)

for index_name in ("published", "disabled"):
    initial_index = initial / "_index" / f"{index_name}.json"
    target_index = target / "_index" / f"{index_name}.json"
    if not initial_index.exists():
        continue
    try:
        initial_items = json.loads(initial_index.read_text(encoding="utf-8")).get("items", [])
    except json.JSONDecodeError:
        initial_items = []
    if target_index.exists():
        try:
            target_items = json.loads(target_index.read_text(encoding="utf-8")).get("items", [])
        except json.JSONDecodeError:
            target_items = []
    else:
        target_items = []
    merged = []
    for item in [*target_items, *initial_items]:
        text = str(item).strip()
        if text in excluded:
            continue
        if text and text not in merged:
            merged.append(text)
    target_index.parent.mkdir(parents=True, exist_ok=True)
    target_index.write_text(json.dumps({"items": merged}, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
PY
}

render_runtime_env_file() {
  local target_path="$1"

  TARGET_PATH="$target_path" \
  AUTHZ_BASE_URL="$AUTHZ_BASE_URL" \
  AUTHZ_INTERNAL_TOKEN="$AUTHZ_INTERNAL_TOKEN" \
  AUTHZ_OUTLOOK_MCP_URL="$AUTHZ_OUTLOOK_MCP_URL" \
  BACKEND_ID="$BACKEND_ID" \
  CLIENT_ID="$CLIENT_ID" \
  CLIENT_SECRET="$CLIENT_SECRET" \
  BACKEND_NAME="$BACKEND_NAME" \
  PUBLIC_URL="$PUBLIC_URL" \
  python3 - <<'PY'
import os
import shlex
from pathlib import Path

target = Path(os.environ["TARGET_PATH"])
values = {
    "BEAVER_AUTHZ__ENABLED": "1" if os.environ["AUTHZ_BASE_URL"].strip() else "0",
    "BEAVER_AUTHZ__BASE_URL": os.environ["AUTHZ_BASE_URL"].strip(),
    "BEAVER_AUTHZ_INTERNAL_TOKEN": os.environ["AUTHZ_INTERNAL_TOKEN"].strip(),
    "BEAVER_AUTHZ__OUTLOOK_MCP_URL": os.environ["AUTHZ_OUTLOOK_MCP_URL"].strip(),
    "BEAVER_BACKEND_IDENTITY__BACKEND_ID": os.environ["BACKEND_ID"].strip(),
    "BEAVER_BACKEND_IDENTITY__CLIENT_ID": os.environ["CLIENT_ID"].strip(),
    "BEAVER_BACKEND_IDENTITY__CLIENT_SECRET": os.environ["CLIENT_SECRET"].strip(),
    "BEAVER_BACKEND_IDENTITY__NAME": os.environ["BACKEND_NAME"].strip(),
    "BEAVER_BACKEND_IDENTITY__PUBLIC_BASE_URL": os.environ["PUBLIC_URL"].strip(),
}
ordered_keys = [
    "BEAVER_AUTHZ__ENABLED",
    "BEAVER_AUTHZ__BASE_URL",
    "BEAVER_AUTHZ_INTERNAL_TOKEN",
    "BEAVER_AUTHZ__OUTLOOK_MCP_URL",
    "BEAVER_BACKEND_IDENTITY__BACKEND_ID",
    "BEAVER_BACKEND_IDENTITY__CLIENT_ID",
    "BEAVER_BACKEND_IDENTITY__CLIENT_SECRET",
    "BEAVER_BACKEND_IDENTITY__NAME",
    "BEAVER_BACKEND_IDENTITY__PUBLIC_BASE_URL",
]
lines: list[str] = []
for key in ordered_keys:
    value = values.get(key, "")
    if value:
        lines.append(f"export {key}={shlex.quote(value)}")
        continue
    if key == "BEAVER_AUTHZ__ENABLED":
        lines.append("export BEAVER_AUTHZ__ENABLED=0")
    else:
        lines.append(f"unset {key}")
target.write_text("\n".join(lines) + "\n", encoding="utf-8")
PY
}

image_exists() {
  docker image inspect "$IMAGE_NAME" >/dev/null 2>&1
}

container_exists() {
  docker container inspect "$CONTAINER_NAME" >/dev/null 2>&1
}

while [[ $# -gt 0 ]]; do
  case "$1" in
    --instance-id)
      INSTANCE_ID="${2:-}"
      shift 2
      ;;
    --image)
      IMAGE_NAME="${2:-}"
      shift 2
      ;;
    --container-name)
      CONTAINER_NAME="${2:-}"
      shift 2
      ;;
    --host-port)
      HOST_PORT="${2:-}"
      shift 2
      ;;
    --public-url)
      PUBLIC_URL="${2:-}"
      shift 2
      ;;
    --provider)
      PROVIDER="${2:-}"
      shift 2
      ;;
    --api-key)
      API_KEY="${2:-}"
      shift 2
      ;;
    --api-base)
      API_BASE="${2:-}"
      shift 2
      ;;
    --model)
      MODEL="${2:-}"
      shift 2
      ;;
    --skip-provider-config)
      SKIP_PROVIDER_CONFIG=1
      shift
      ;;
    --auth-username)
      AUTH_USERNAME="${2:-}"
      shift 2
      ;;
    --auth-password)
      AUTH_PASSWORD="${2:-}"
      shift 2
      ;;
    --username)
      USERNAME="${2:-}"
      shift 2
      ;;
    --email)
      EMAIL="${2:-}"
      shift 2
      ;;
    --instance-host)
      INSTANCE_HOST="${2:-}"
      shift 2
      ;;
    --authz-base-url)
      AUTHZ_BASE_URL="${2:-}"
      shift 2
      ;;
    --authz-internal-token)
      AUTHZ_INTERNAL_TOKEN="${2:-}"
      shift 2
      ;;
    --authz-outlook-mcp-url)
      AUTHZ_OUTLOOK_MCP_URL="${2:-}"
      shift 2
      ;;
    --outlook-mcp-server-id)
      OUTLOOK_MCP_SERVER_ID="${2:-}"
      shift 2
      ;;
    --outlook-mcp-call-timeout-seconds)
      OUTLOOK_MCP_CALL_TIMEOUT_SECONDS="${2:-}"
      shift 2
      ;;
    --user-files-max-upload-bytes)
      USER_FILES_MAX_UPLOAD_BYTES="${2:-}"
      shift 2
      ;;
    --external-connector-base-url)
      EXTERNAL_CONNECTOR_BASE_URL="${2:-}"
      shift 2
      ;;
    --external-connector-token)
      EXTERNAL_CONNECTOR_TOKEN="${2:-}"
      shift 2
      ;;
    --external-connector-callback-base-url)
      EXTERNAL_CONNECTOR_CALLBACK_BASE_URL="${2:-}"
      shift 2
      ;;
    --bridge-token)
      BEAVER_BRIDGE_TOKEN="${2:-}"
      shift 2
      ;;
    --backend-id)
      BACKEND_ID="${2:-}"
      shift 2
      ;;
    --client-id)
      CLIENT_ID="${2:-}"
      shift 2
      ;;
    --client-secret)
      CLIENT_SECRET="${2:-}"
      shift 2
      ;;
    --backend-name)
      BACKEND_NAME="${2:-}"
      shift 2
      ;;
    --auth-portal-url)
      AUTH_PORTAL_URL="${2:-}"
      shift 2
      ;;
    --auth-portal-port)
      AUTH_PORTAL_PORT="${2:-}"
      shift 2
      ;;
    --instances-root)
      INSTANCES_ROOT="${2:-}"
      shift 2
      ;;
    --registry)
      REGISTRY_PATH="${2:-}"
      shift 2
      ;;
    --network)
      NETWORK_NAME="${2:-}"
      shift 2
      ;;
    --host-bind-ip)
      HOST_BIND_IP="${2:-}"
      shift 2
      ;;
    --initial-skills-dir)
      INITIAL_SKILLS_DIR="${2:-}"
      shift 2
      ;;
    --skip-initial-skills)
      SEED_INITIAL_SKILLS=0
      shift
      ;;
    --build)
      FORCE_BUILD=1
      shift
      ;;
    --replace)
      REPLACE=1
      shift
      ;;
    --help|-h)
      usage
      exit 0
      ;;
    *)
      die "unknown argument: $1"
      ;;
  esac
done

[[ -n "$INSTANCE_ID" ]] || die "--instance-id is required"
[[ -n "$AUTH_USERNAME" ]] || die "--auth-username is required"
[[ -n "$AUTH_PASSWORD" ]] || die "--auth-password is required"
if [[ "$SKIP_PROVIDER_CONFIG" -ne 1 ]]; then
  [[ -n "$API_KEY" ]] || die "--api-key is required unless --skip-provider-config is set"
fi

INSTANCE_SLUG="$(slugify "$INSTANCE_ID")"
USERNAME="${USERNAME:-$AUTH_USERNAME}"
EXISTING_RECORD_JSON=""
EXISTING_CONTAINER_NAME=""
EXISTING_HOST_PORT=""

if EXISTING_RECORD_JSON="$("$REGISTRY_TOOL" --registry "$REGISTRY_PATH" get --instance-id "$INSTANCE_ID" 2>/dev/null)"; then
  EXISTING_CONTAINER_NAME="$(printf '%s' "$EXISTING_RECORD_JSON" | python3 -c 'import json,sys; print(json.load(sys.stdin)["container_name"])')"
  EXISTING_HOST_PORT="$(printf '%s' "$EXISTING_RECORD_JSON" | python3 -c 'import json,sys; print(json.load(sys.stdin)["host_port"])')"
  if [[ "$REPLACE" -ne 1 ]]; then
    die "instance already exists in registry: ${INSTANCE_ID} (use --replace to recreate)"
  fi
fi

CONTAINER_NAME="${CONTAINER_NAME:-${EXISTING_CONTAINER_NAME:-app-instance-${INSTANCE_SLUG}}}"
BACKEND_NAME="${BACKEND_NAME:-${INSTANCE_ID}}"

if [[ -z "$HOST_PORT" ]]; then
  HOST_PORT="${EXISTING_HOST_PORT:-$(pick_free_port)}"
fi

if [[ -z "$PUBLIC_URL" ]]; then
  PUBLIC_URL="http://127.0.0.1:${HOST_PORT}"
fi

if [[ -z "$INSTANCE_HOST" ]]; then
  INSTANCE_HOST="$(extract_url_host "$PUBLIC_URL")"
fi

if [[ "$SKIP_PROVIDER_CONFIG" -ne 1 ]]; then
  case "$KNOWN_PROVIDERS" in
    *" ${PROVIDER} "*) ;;
    *) die "unsupported provider '${PROVIDER}'" ;;
  esac
fi

if [[ -n "$INITIAL_SKILLS_DIR" ]]; then
  INITIAL_SKILLS_DIR="$(INITIAL_SKILLS_DIR="$INITIAL_SKILLS_DIR" python3 - <<'PY'
import os
from pathlib import Path

print(Path(os.environ["INITIAL_SKILLS_DIR"]).expanduser().resolve())
PY
)"
fi

if [[ -n "$BACKEND_ID$CLIENT_ID$CLIENT_SECRET" ]]; then
  [[ -n "$BACKEND_ID" && -n "$CLIENT_ID" && -n "$CLIENT_SECRET" ]] || die "backend identity requires --backend-id, --client-id and --client-secret together"
fi

if PORT_HOLDER_JSON="$("$REGISTRY_TOOL" --registry "$REGISTRY_PATH" get --container-name "$CONTAINER_NAME" 2>/dev/null)"; then
  REGISTERED_ID="$(printf '%s' "$PORT_HOLDER_JSON" | python3 -c 'import json,sys; print(json.load(sys.stdin)["instance_id"])')"
  if [[ "$REGISTERED_ID" != "$INSTANCE_ID" && "$REPLACE" -ne 1 ]]; then
    die "container name already registered for another instance: ${CONTAINER_NAME}"
  fi
fi

if PORT_HOLDER_JSON="$("$REGISTRY_TOOL" --registry "$REGISTRY_PATH" list --json)"; then
  PORT_CONFLICT_ID="$(PORT_HOLDER_JSON="$PORT_HOLDER_JSON" python3 - "$HOST_PORT" "$INSTANCE_ID" <<'PY'
import json
import os
import sys

port = int(sys.argv[1])
instance_id = sys.argv[2]
for item in json.loads(os.environ["PORT_HOLDER_JSON"]).get("instances", []):
    if int(item.get("host_port", 0) or 0) == port and item.get("instance_id") != instance_id:
        print(item.get("instance_id", ""))
        break
PY
)"
  if [[ -n "$PORT_CONFLICT_ID" ]]; then
    die "host port already registered by another instance: ${HOST_PORT} (${PORT_CONFLICT_ID})"
  fi
fi

INSTANCE_ROOT="${INSTANCES_ROOT}/${INSTANCE_SLUG}"
BEAVER_HOME="${INSTANCE_ROOT}/beaver-home"
CONFIG_PATH="${BEAVER_HOME}/config.json"
AUTH_USERS_PATH="${BEAVER_HOME}/web_auth_users.json"
RUNTIME_ENV_PATH="${BEAVER_HOME}/runtime.env"
WORKSPACE_PATH="${BEAVER_HOME}/workspace"

mkdir -p "$BEAVER_HOME" "$WORKSPACE_PATH"

render_config_json "$CONFIG_PATH"
render_auth_users_json "$AUTH_USERS_PATH"
render_runtime_env_file "$RUNTIME_ENV_PATH"
seed_initial_skills "$WORKSPACE_PATH" "$INITIAL_SKILLS_DIR"

if [[ "$FORCE_BUILD" -eq 1 ]] || ! image_exists; then
  log "building image ${IMAGE_NAME}"
  docker build \
    --build-arg "NEXT_PUBLIC_AUTH_PORTAL_URL=${AUTH_PORTAL_URL}" \
    --build-arg "NEXT_PUBLIC_AUTH_PORTAL_PORT=${AUTH_PORTAL_PORT}" \
    -t "$IMAGE_NAME" \
    "$SCRIPT_DIR"
fi

if container_exists; then
  if [[ "$REPLACE" -eq 1 ]]; then
    log "removing existing container ${CONTAINER_NAME}"
    docker rm -f "$CONTAINER_NAME" >/dev/null
  else
    die "container already exists: ${CONTAINER_NAME} (use --replace to recreate)"
  fi
fi

RUN_ARGS=(
  -d
  --name "$CONTAINER_NAME"
  --restart unless-stopped
  -p "${HOST_BIND_IP}:${HOST_PORT}:8080"
  -v "${BEAVER_HOME}:/root/.beaver"
  -e "BEAVER_HOME=/root/.beaver"
  -e "BEAVER_CONFIG_PATH=/root/.beaver/config.json"
  -e "BEAVER_WORKSPACE=/root/.beaver/workspace"
  -e "BEAVER_AUTH_FILE=/root/.beaver/web_auth_users.json"
  -e "BEAVER_FRONTEND_PUBLIC_BASE_URL=${PUBLIC_URL}"
  -e "APP_PUBLIC_PORT=8080"
  -e "APP_FRONTEND_PORT=3000"
  -e "APP_BACKEND_PORT=18080"
  -e "BEAVER_ENABLE_SELF_RESTART=1"
  -e "BEAVER_OUTLOOK_MCP_SERVER_ID=${OUTLOOK_MCP_SERVER_ID}"
  -e "BEAVER_OUTLOOK_MCP_CALL_TIMEOUT_SECONDS=${OUTLOOK_MCP_CALL_TIMEOUT_SECONDS}"
  -e "EXTERNAL_CONNECTOR_BASE_URL=${EXTERNAL_CONNECTOR_BASE_URL}"
  --label "beaver.instance.id=${INSTANCE_ID}"
  --label "beaver.instance.slug=${INSTANCE_SLUG}"
  --label "beaver.instance.public_url=${PUBLIC_URL}"
)

if [[ -z "$EXTERNAL_CONNECTOR_CALLBACK_BASE_URL" && -n "$NETWORK_NAME" ]]; then
  EXTERNAL_CONNECTOR_CALLBACK_BASE_URL="http://${CONTAINER_NAME}:8080"
fi

if [[ "$SEED_INITIAL_SKILLS" -eq 1 && -n "$INITIAL_SKILLS_DIR" ]]; then
  RUN_ARGS+=(
    -v "${INITIAL_SKILLS_DIR}:/opt/app/initial-skills:ro"
    -e "BEAVER_INITIAL_SKILLS_DIR=/opt/app/initial-skills"
    -e "BEAVER_INITIAL_SKILLS_EXCLUDE=${INITIAL_SKILLS_EXCLUDE}"
  )
fi

if [[ -n "$USER_FILES_MAX_UPLOAD_BYTES" ]]; then
  RUN_ARGS+=(-e "BEAVER_USER_FILES_MAX_UPLOAD_BYTES=${USER_FILES_MAX_UPLOAD_BYTES}")
fi
if [[ -n "$EXTERNAL_CONNECTOR_TOKEN" ]]; then
  RUN_ARGS+=(-e "EXTERNAL_CONNECTOR_TOKEN=${EXTERNAL_CONNECTOR_TOKEN}")
fi
if [[ -n "$EXTERNAL_CONNECTOR_CALLBACK_BASE_URL" ]]; then
  RUN_ARGS+=(-e "EXTERNAL_CONNECTOR_CALLBACK_BASE_URL=${EXTERNAL_CONNECTOR_CALLBACK_BASE_URL}")
fi
if [[ -n "$BEAVER_BRIDGE_TOKEN" ]]; then
  RUN_ARGS+=(-e "BEAVER_BRIDGE_TOKEN=${BEAVER_BRIDGE_TOKEN}")
fi

if [[ -n "$NETWORK_NAME" ]]; then
  RUN_ARGS+=(--network "$NETWORK_NAME")
fi

log "starting container ${CONTAINER_NAME}"
docker run "${RUN_ARGS[@]}" "$IMAGE_NAME" >/dev/null

"$REGISTRY_TOOL" --registry "$REGISTRY_PATH" upsert \
  --instance-id "$INSTANCE_ID" \
  --instance-slug "$INSTANCE_SLUG" \
  --container-name "$CONTAINER_NAME" \
  --image-name "$IMAGE_NAME" \
  --host-port "$HOST_PORT" \
  --public-url "$PUBLIC_URL" \
  --instance-root "$INSTANCE_ROOT" \
  --beaver-home "$BEAVER_HOME" \
  --config-path "$CONFIG_PATH" \
  --auth-users-path "$AUTH_USERS_PATH" \
  --network-name "$NETWORK_NAME" \
  --backend-id "$BACKEND_ID" \
  --backend-name "$BACKEND_NAME" \
  --authz-base-url "$AUTHZ_BASE_URL" \
  --username "$USERNAME" \
  --email "$EMAIL" \
  --instance-host "$INSTANCE_HOST" \
  --frontend-base-url "$PUBLIC_URL" \
  --api-base-url "$PUBLIC_URL" \
  --created-at "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >/dev/null

cat <<EOF
instance_id=${INSTANCE_ID}
instance_slug=${INSTANCE_SLUG}
container_name=${CONTAINER_NAME}
image_name=${IMAGE_NAME}
host_port=${HOST_PORT}
public_url=${PUBLIC_URL}
instance_root=${INSTANCE_ROOT}
beaver_home=${BEAVER_HOME}
config_path=${CONFIG_PATH}
auth_users_path=${AUTH_USERS_PATH}
runtime_env_path=${RUNTIME_ENV_PATH}
username=${USERNAME}
email=${EMAIL}
instance_host=${INSTANCE_HOST}
host_bind_ip=${HOST_BIND_IP}
EOF
