{
	admin off

	# Everything reaching this gateway comes from loopback or the docker
	# bridge, so trust it as a proxy hop. This makes Caddy pass through
	# X-Forwarded-Proto from https tunnel agents (tailscale funnel / ngrok)
	# instead of overwriting it with the plain-http scheme it sees on :80.
	servers {
		trusted_proxies static private_ranges
	}
}

:80 {
	# Compact log format for development
	log {
		output stdout
		format transform "{common_log}"
	}

	# Vite dev server fans out hundreds of per-module fetches on every admin
	# page load — skip those (and the internal auth-frame) so the access log
	# stays useful during development.
	log_skip /__admin-dev__*
	log_skip /ghost/auth-frame/*

	# Ember live reload (runs on separate port 4201)
	# This handles both the script injection and WebSocket connections
	handle /ember-cli-live-reload.js {
		reverse_proxy {env.ADMIN_LIVE_RELOAD_SERVER} {
			header_up Host {http.reverse_proxy.upstream.hostport}
			# Enable WebSocket support for live reload
			header_up Connection {>Connection}
			header_up Upgrade {>Upgrade}
		}
	}

	# Ghost API - must go to Ghost backend, not admin dev server
	handle /ghost/api/* {
		reverse_proxy {env.GHOST_BACKEND} {
			header_up Host {host}
			header_up X-Real-IP {remote_host}

			# Always tell Ghost requests are HTTPS to prevent redirects
			header_up X-Forwarded-Proto https
		}
	}

	# Analytics API - proxy analytics requests to analytics service
	# Handles paths like /.ghost/analytics/* or /blog/.ghost/analytics/*
	@analytics_paths path_regexp analytics_match ^(.*)/\.ghost/analytics(.*)$
	handle @analytics_paths {
		rewrite * {re.analytics_match.2}
		reverse_proxy {env.ANALYTICS_PROXY_TARGET} {
			header_up Host {host}
			header_up X-Real-IP {remote_host}
		}
	}

	# ActivityPub API - proxy activityPub requests to activityPub service (running in separate project)
	# Requires activitypub containers to be running via the ActivityPub project's docker-compose
	#
	# Unlike the Ghost blocks below, the ActivityPub blocks must NOT force
	# X-Forwarded-Proto: the AP service derives its own URLs (and whether to
	# fetch Ghost's JWKS over http or https) from the real scheme. Direct
	# localhost access needs http; tunneled access gets https passed through
	# from the tunnel agent via trusted_proxies (see global options).
	handle /.ghost/activitypub/* {
		reverse_proxy {env.ACTIVITYPUB_PROXY_TARGET} {
			header_up Host {host}
			header_up X-Real-IP {remote_host}
		}
	}

	# WebFinger - required for ActivityPub federation
	handle /.well-known/webfinger {
		reverse_proxy {env.ACTIVITYPUB_PROXY_TARGET} {
			header_up Host {host}
			header_up X-Real-IP {remote_host}
		}
	}

	# NodeInfo - required for ActivityPub federation
	handle /.well-known/nodeinfo {
		reverse_proxy {env.ACTIVITYPUB_PROXY_TARGET} {
			header_up Host {host}
			header_up X-Real-IP {remote_host}
		}
	}

	# Public app dev server assets - must come BEFORE general /ghost/* handler
	# Ghost is configured to load these from /ghost/assets/* via compose.dev.yaml
	handle /ghost/assets/* {
		# Strip /ghost/assets/ prefix
		uri strip_prefix /ghost/assets

		# Koenig Lexical Editor (optional - started by "pnpm dev:lexical" at the
		# repo root, which runs the koenig/koenig-lexical dev:integrated target
		# and sets EDITOR_URL so admin loads the editor from here)
		# Falls back to Ghost backend (built package) via handle_errors if dev server isn't running
		@lexical path /koenig-lexical/*
		handle @lexical {
			uri strip_prefix /koenig-lexical
			reverse_proxy {env.LEXICAL_DEV_SERVER} {
				header_up Host {http.reverse_proxy.upstream.hostport}
				# Fail quickly if dev server is down
				fail_duration 1s
				unhealthy_request_count 1
			}
		}

		# Public app UMD bundles — served directly from each app's umd/
		# build output. Vite's build:watch writes to disk on every source
		# change, Caddy picks up the new file on next request. Replaces the
		# per-app `vite preview` server (sirv) layer that used to sit in front.
		#
		# The named allowlist is load-bearing: a generic [a-z0-9-]+ pattern
		# would swallow admin asset paths (e.g. /ghost/assets/built/foo.css)
		# before they could fall through to the admin dev server below.
		# Adding a new public app = add its name to the alternation.
		@public_app path_regexp public_app ^/(portal|comments-ui|signup-form|sodo-search|announcement-bar|admin-toolbar)/(.*)$
		handle @public_app {
			rewrite * /{re.public_app.2}
			root * /srv/apps/{re.public_app.1}/umd
			file_server
		}

		# Everything else under /ghost/assets/* goes to admin dev server.
		# Vite serves admin/Ember assets under its dev base (/__admin-dev__/assets/),
		# so we rewrite the request before proxying.
		handle {
			rewrite * /__admin-dev__/assets{path}
			reverse_proxy {env.ADMIN_DEV_SERVER} {
				header_up Host {http.reverse_proxy.upstream.hostport}
			}
		}
	}

	# Auth frame - must go to Ghost backend for comment admin authentication
	handle /ghost/auth-frame/* {
		reverse_proxy {env.GHOST_BACKEND} {
			header_up Host {host}
			header_up X-Real-IP {remote_host}
			header_up X-Forwarded-Proto https
		}
	}

	# JWKS endpoint - must go to Ghost backend for JWT verification
	handle /ghost/.well-known/* {
		reverse_proxy {env.GHOST_BACKEND} {
			header_up Host {host}
			header_up X-Real-IP {remote_host}
			header_up X-Forwarded-Proto https
		}
	}

	# Vite dev server lives under its own prefix so its internals
	# (HMR client, module graph, refresh runtime) don't collide with the
	# /ghost/* paths Ghost serves in production. Handles HMR WebSocket upgrade.
	handle /__admin-dev__* {
		reverse_proxy {env.ADMIN_DEV_SERVER} {
			header_up Connection {>Connection}
			header_up Upgrade {>Upgrade}
		}
	}

	# Admin HTML entry — proxy to Vite, which serves the dev index.html.
	# Matches `/ghost` and `/ghost/` exactly (deep links fall through to Ghost
	# below so its redirect middleware turns them into hash URLs).
	@admin_html path /ghost /ghost/
	handle @admin_html {
		rewrite * /__admin-dev__/
		reverse_proxy {env.ADMIN_DEV_SERVER}
	}

	# Everything else under /ghost/* (deep links, redirects, etc.) goes to
	# Ghost so the same Express middleware runs in dev as in production.
	handle /ghost/* {
		reverse_proxy {env.GHOST_BACKEND} {
			header_up Host {host}
			header_up X-Real-IP {remote_host}
			header_up X-Forwarded-Proto https
		}
	}

	# Everything else goes to Ghost backend
	handle {
		reverse_proxy {env.GHOST_BACKEND} {
			header_up Host {host}
			header_up X-Real-IP {remote_host}

			# Always tell Ghost requests are HTTPS to prevent redirects
			header_up X-Forwarded-Proto https
		}
	}

	# Handle errors
	handle_errors {
		# Fallback for Lexical when dev server is unavailable (502/503/504)
		# Forwards to Ghost backend which serves the built koenig-lexical package
		@lexical_fallback `{http.request.orig_uri.path}.startsWith("/ghost/assets/koenig-lexical/")`
		handle @lexical_fallback {
			rewrite * {http.request.orig_uri.path}
			reverse_proxy {env.GHOST_BACKEND} {
				header_up Host {host}
				header_up X-Forwarded-Proto https
			}
		}

		# Default error response
		respond "{err.status_code} {err.status_text}"
	}
}
