#!/bin/zsh

# Sed compatibility wrapper
function sediment() {
    if [[ "$OSTYPE" == "darwin"* ]]; then
        sed -i '' "$@"
    else
        sed -i "$@"
    fi
}


# Patch compatibility wrapper
function patchment() {
    # -f : Force. Do not ask questions. (Standard in GNU and BSD patch)
    # -N : Ignore patches that seem to be reversed or already applied (Forward)
    # But we control these flags in the caller.
    patch "$@"
}

function at-repos-env() {
	APP_PASSWORD=xxx
	host=syu.is
	handle=ai.syui.ai
	did=did:plc:6qyecktefllvenje24fcxnie
	repos=(
		"https://github.com/did-method-plc/did-method-plc"
		"https://github.com/bluesky-social/indigo"
		"https://github.com/bluesky-social/atproto"
		"https://github.com/bluesky-social/social-app"
		"https://github.com/bluesky-social/feed-generator"
		"https://github.com/bluesky-social/ozone"
		"https://github.com/bluesky-social/jetstream"
	)
	services=(
		"bsky"
		"plc"
		"pds"
		"jetstream"
		"bgs"
		"ozone"
		"social-app"
		"feed"
		"chat"
	)
	handles=(
		"syui.syui.ai"
		"ai.syui.ai"
		"apple.syu.is"
	)
	d=${0:a:h}
	dh=${0:a:h:h}
	name=${host%%.*}
	domain=${host##*.}
	dport=5000

	typeset -A PINNED_COMMITS
	PINNED_COMMITS=()
	#PINNED_COMMITS=( [indigo]="d49b454196351c988ceb5ce1f5e21b689487b5ab" [atproto]="104e6ed37b0589cc000109dc76316be35b2257e1")
}

# Arrays for patch management
typeset -a FAILED_PATCHES

# Patch file lists
typeset -a PATCH_FILES_CURL
PATCH_FILES_CURL=(
	"4367-atproto-services-bsky-api.diff|https://raw.githubusercontent.com/bluesky-social/atproto/refs/heads/main/services/bsky/api.js|services/bsky/api.js"
	"4367-atproto-services-pds-index.diff|https://raw.githubusercontent.com/bluesky-social/atproto/refs/heads/main/services/pds/index.js|services/pds/index.js"
)

typeset -a PATCH_FILES
PATCH_FILES=(
	"170-pds-oauth-same-site-fix.patch"
	"140-social-app-yarn-network-timeout.patch"
	"130-atproto-ozone-enable-daemon-v2.patch"
	"200-feed-generator-custom.patch"
	"210-bgs-since-empty-fix.patch"
	"220-atproto-chat-grapheme-limit.patch"
	"049-social-app-dockerfile-chat-grapheme.patch"
)

function at-repos-clone() {
	if [ ! -d $d/repos ];then
		mkdir -p $d/repos
	fi
	cd $d/repos
	for ((i=1; i<=${#repos}; i++)); do
		repo=${repos[$i]}
		echo $repo
		if [ ! -d $d/repos/${repo##*/} ];then
			git clone $repo

		fi
	done
	if [ ! -f $d/repos/feed-generator/Dockerfile ] && [ -f $d/docker/feed/Dockerfile ];then
		cp -rf $d/docker/feed/Dockerfile $d/repos/feed-generator/
	fi
	if [ ! -d $d/repos/chat ];then
		git clone https://git.syui.ai/ai/chat $d/repos/chat
	fi
}

function at-repos-pull() {
	cd $d/repos
	for ((i=1; i<=${#repos}; i++)); do
		repo=${repos[$i]}
		echo $repo
		if [ -d $d/repos/${repo##*/} ];then
			cd $d/repos/${repo##*/}
			# Clean up before pull: reset changes, remove .orig files and untracked patch-created files
			git checkout -- .
			find . -name "*.orig" -type f -delete 2>/dev/null
			git clean -fd 2>/dev/null
			git stash -u
			if ! git pull;then
				echo "❌ pull failed: ${repo##*/} — skipping (manual fix needed)"
				cd $d/repos
				continue
			fi
		fi
		rv=$(echo "$repos_v" | jq -r ".[\"${repo##*/}\"]")
		if [ "$rv" != "null" ];then
			cd $d/repos/${repo##*/}
			git reset --hard $rv
			cd ..
		fi
	done
	# Copy feed-generator Dockerfile if missing (removed by git checkout)
	if [ ! -f $d/repos/feed-generator/Dockerfile ] && [ -f $d/docker/feed/Dockerfile ];then
		cp -rf $d/docker/feed/Dockerfile $d/repos/feed-generator/
	fi
	# Pull chat repo
	if [ -d $d/repos/chat ];then
		cd $d/repos/chat
		git pull
	else
		at-repos-clone
	fi
	cd $d
}

function at-repos-checkout-pinned() {
	echo "🔒 Checking out pinned commits..."
	cd $d/repos
	for repo_name pinned_commit in ${(kv)PINNED_COMMITS}; do
		if [ -n "$pinned_commit" ] && [ -d "$d/repos/$repo_name" ]; then
			echo "  📌 $repo_name -> $pinned_commit"
			cd $d/repos/$repo_name
			git fetch origin
			git checkout $pinned_commit
			cd $d/repos
		fi
	done
	cd $d
}

function at-repos-social-app-ios-patch() {
	$d/ios/setup.zsh
}

# Common patch function with status detection
function apply-patch() {
	local patch_name=$1
	local target_dir=$2
	local patch_file=$3

	echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
	echo "📝 Patch: ${patch_name}"
	echo "   Target: ${target_dir}"
	echo "   File: ${patch_file}"

	pushd ${target_dir} > /dev/null

	# Check if patch is already applied (reverse dry-run succeeds)
	# Use -f to force dry-run to fail instead of asking questions if unapplied
	if patch -f --dry-run -p1 -R < ${patch_file} > /dev/null 2>&1; then
		echo "✅ Already applied - skipping"
		popd > /dev/null
		echo ""
		return 0
	fi

	# Check if patch can be applied (forward dry-run succeeds)
	if patch -f --dry-run -p1 < ${patch_file} > /dev/null 2>&1; then
		echo "🔧 Applying patch..."
		if patch -f -p1 < ${patch_file}; then
			echo "✅ Applied successfully"
			popd > /dev/null
			echo ""
			return 0
		else
			echo "❌ Failed to apply"
			FAILED_PATCHES+=("${patch_name} (${patch_file})")
			popd > /dev/null
			echo ""
			return 1
		fi
	else
		echo "⚠️  Cannot apply - file may have been modified"
		echo "   Please check manually"
		FAILED_PATCHES+=("${patch_name} (${patch_file}) - file modified")
		popd > /dev/null
		echo ""
		return 1
	fi
}

# Function to display failed patches summary
function show-failed-patches() {
	if [ ${#FAILED_PATCHES[@]} -eq 0 ]; then
		echo ""
		echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
		echo "✅ All patches applied successfully!"
		echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
		echo ""
		return 0
	fi

	echo ""
	echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
	echo "⚠️  FAILED PATCHES SUMMARY"
	echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
	echo ""
	echo "The following patches could not be applied:"
	echo ""
	for failed_patch in "${FAILED_PATCHES[@]}"; do
		echo "  ❌ ${failed_patch}"
	done
	echo ""
	echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
	echo ""
}

# Helper function for applying patches
function patch-apply() {
	local name=$1
	local target=$2
	local patch_file=$3
	apply-patch "${name}" "$d/repos/${target}" "$d/patching/${patch_file}"
}

# Helper function for patches with curl download
function patch-apply-with-curl() {
	local name=$1
	local target=$2
	local patch_file=$3
	local download_url=$4
	local download_target=$5

	if ! curl -sfL "${download_url}" -o "$d/repos/${target}/${download_target}"; then
		echo "❌ Download failed: ${download_url}"
		FAILED_PATCHES+=("${name} (download failed)")
		return 1
	fi
	apply-patch "${name}" "$d/repos/${target}" "$d/patching/${patch_file}"
}

# Auto-apply patches from list
function at-repos-patch-apply-all() {
	# Apply curl patches
	for patch_info in "${PATCH_FILES_CURL[@]}"; do
		local filename="${patch_info%%|*}"
		local rest="${patch_info#*|}"
		local download_url="${rest%%|*}"
		local download_target="${rest#*|}"

		local title="${filename%.*}"
		local repo=""

		# Determine repo from filename
		if [[ $filename == *"atproto"* ]]; then
			repo="atproto"
		elif [[ $filename == *"pds"* ]]; then
			repo="atproto"
		fi

		patch-apply-with-curl "$title" "$repo" "$filename" "$download_url" "$download_target"
	done

	# Apply regular patches
	for filename in "${PATCH_FILES[@]}"; do
		local title="${filename%.*}"
		local repo=""

		# Determine repo from filename
		# Note: check indigo/bgs BEFORE pds to avoid "newpds" matching "pds"
		if [[ $filename == *"social-app"* || $filename == *"statsig"* ]]; then
			repo="social-app"
			# Copy chat grapheme limit patch for Dockerfile build
			if [[ $filename == "049-social-app-dockerfile-chat-grapheme.patch" ]] && [ -f "$d/patching/230-atproto-api-chat-grapheme-limit.patch" ]; then
				cp "$d/patching/230-atproto-api-chat-grapheme-limit.patch" "$d/repos/social-app/"
			fi
		elif [[ $filename == *"indigo"* || $filename == *"bgs"* ]]; then
			repo="indigo"
		elif [[ $filename == *"atproto"* ]]; then
			repo="atproto"
		elif [[ $filename == *"pds"* ]]; then
			repo="atproto"
		elif [[ $filename == *"feed"* ]]; then
			repo="feed-generator"
			# feed-generatorパッチ適用前に既存のDockerfileを削除（upstreamと競合回避）
			if [[ $filename == "200-feed-generator-custom.patch" ]]; then
				rm -f "$d/repos/feed-generator/Dockerfile"
			fi
		fi

		patch-apply "$title" "$repo" "$filename"
	done
}

function at-repos-ozone-patch() {
	cd $d/repos
	d_=$d/repos/ozone
	rm -rf ${d_}
	git clone https://github.com/bluesky-social/ozone

	apply-patch "Ozone enable daemon" "${d_}" "$d/patching/122-ozone-enable-daemon.diff"

	if [ -f "$d/patching/150-ozone-plc-fix.patch" ]; then
		apply-patch "Ozone plc fix" "${d_}" "$d/patching/150-ozone-plc-fix.patch"
	fi

	if [ -f "$d/patching/160-ozone-oauth-redirect-fix.patch" ]; then
		apply-patch "Ozone oauth redirect fix" "${d_}" "$d/patching/160-ozone-oauth-redirect-fix.patch"
	fi

	# Apply constants fix and do additional sed replacements
	pushd ${d_} > /dev/null
	if [ -f "$d/patching/121-ozone-constants-fix.patch" ]; then
		patch -p1 < "$d/patching/121-ozone-constants-fix.patch" 2>/dev/null || true
	fi

	# Replace process.env with env()
	sediment 's/process\.env\.\(NEXT_PUBLIC_[A-Z_]*\)/env('\''\1'\'')/g' lib/constants.ts 2>/dev/null || true
	sediment 's/process\.env\.NODE_ENV/env('\''NODE_ENV'\'')/g' lib/constants.ts 2>/dev/null || true
	# Add missing SOCIAL_APP_DOMAIN constant after SOCIAL_APP_URL
	sediment '/^export const SOCIAL_APP_URL =/,/^$/{ /^$/a\
export const SOCIAL_APP_DOMAIN =\
  env('\''NEXT_PUBLIC_SOCIAL_APP_DOMAIN'\'') || '\''bsky.app'\''\

	}' lib/constants.ts 2>/dev/null || true
	# Fix multiline process.env patterns
	sediment '/^export const NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = process\.env$/,/^  : 7$/ {
		s/^export const NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = process\.env$/export const NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = env('\''NEXT_PUBLIC_NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS'\'')/
		/^  \.NEXT_PUBLIC_NEW_ACCOUNT_MARKER_THRESHOLD_IN_DAYS$/d
	}' lib/constants.ts 2>/dev/null || true
	sediment '/^export const YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = process\.env$/,/^  : 30$/ {
		s/^export const YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = process\.env$/export const YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS = env('\''NEXT_PUBLIC_YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS'\'')/
		/^  \.NEXT_PUBLIC_YOUNG_ACCOUNT_MARKER_THRESHOLD_IN_DAYS$/d
	}' lib/constants.ts 2>/dev/null || true
	sediment '/^export const HIGH_PROFILE_FOLLOWER_THRESHOLD = process\.env$/,/^  : Infinity$/ {
		s/^export const HIGH_PROFILE_FOLLOWER_THRESHOLD = process\.env$/export const HIGH_PROFILE_FOLLOWER_THRESHOLD = env('\''NEXT_PUBLIC_HIGH_PROFILE_FOLLOWER_THRESHOLD'\'')/
		/^  \.NEXT_PUBLIC_HIGH_PROFILE_FOLLOWER_THRESHOLD$/d
	}' lib/constants.ts 2>/dev/null || true
	# Fix parseInt() to handle undefined by adding || ''
	sediment "s/parseInt(env('\([^']*\)'))/parseInt(env('\1') || '0')/g" lib/constants.ts 2>/dev/null || true

	# Add next-runtime-env to package.json if missing (used by lib/constants.ts)
	if ! grep -q 'next-runtime-env' package.json 2>/dev/null; then
		echo "📦 Adding next-runtime-env to package.json..."
		python3 -c "
import json
with open('package.json') as f:
    d = json.load(f)
d.setdefault('dependencies', {})['next-runtime-env'] = '^1.6.2'
with open('package.json', 'w') as f:
    json.dump(d, f, indent=2)
    f.write('\n')
"
		echo "✅ Added next-runtime-env"
	fi

	# Update @atproto/ozone in service/package.json to latest
	echo "📦 Updating @atproto/ozone in service/package.json..."
	local latest_ozone_ver
	latest_ozone_ver=$(npm view @atproto/ozone version 2>/dev/null)
	if [ -n "$latest_ozone_ver" ] && [ -f service/package.json ]; then
		python3 -c "
import json, sys
ver = sys.argv[1]
with open('service/package.json') as f:
    d = json.load(f)
old = d.get('dependencies', {}).get('@atproto/ozone', '')
d.setdefault('dependencies', {})['@atproto/ozone'] = ver
with open('service/package.json', 'w') as f:
    json.dump(d, f, indent=2)
    f.write('\n')
print(f'✅ @atproto/ozone: {old} -> {ver}')
" "$latest_ozone_ver"
	fi

	popd > /dev/null
}

function at-repos-feed-generator-newfiles() {
	echo ""
	echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
	echo "📝 Creating feed-generator new files..."

	# Create app.ts
	cat > $d/repos/feed-generator/src/algos/app.ts <<'EOF'
import { QueryParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton'
import { AppContext } from '../config'

// max 15 chars
export const shortname = 'app'

export const handler = async (ctx: AppContext, params: QueryParams) => {
    let builder = ctx.db
        .selectFrom('post')
        .selectAll()
        .orderBy('indexedAt', 'desc')
        .orderBy('cid', 'desc')
        .limit(params.limit)

    if (params.cursor) {
        const timeStr = new Date(parseInt(params.cursor, 10)).toISOString()
        builder = builder.where('post.indexedAt', '<', timeStr)
    }
    const res = await builder.execute()

    const feed = res.map((row) => ({
        post: row.uri,
    }))

    let cursor: string | undefined
    const last = res.at(-1)
    if (last) {
        cursor = new Date(last.indexedAt).getTime().toString(10)
    }

    return {
        cursor,
        feed,
    }
}
EOF

	echo "✅ Created src/algos/app.ts"

	# Restore Dockerfile (removed during patch apply to avoid conflicts)
	if [ ! -f $d/repos/feed-generator/Dockerfile ] && [ -f $d/docker/feed/Dockerfile ];then
		cp -rf $d/docker/feed/Dockerfile $d/repos/feed-generator/
		echo "✅ Restored Dockerfile"
	fi
	echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
}

function at-repos-docker-verify() {
	local service=$1
	local image="at-${service}:latest"
	local check_file=""
	case $service in
		pds)       check_file="/app/services/pds/index.js" ;;
		bsky)      check_file="/app/services/bsky/api.js" ;;
		ozone)     check_file="/app/services/ozone/api.js" ;;
		bgs)       check_file="/relay" ;;
		plc)       check_file="/app/packages/server/dist/index.js" ;;
		chat)      check_file="/usr/local/bin/aichat" ;;
		*)         return 0 ;;
	esac
	local cid
	cid=$(docker create --entrypoint "" "$image" true 2>&1)
	if [ $? -ne 0 ]; then
		echo "  ❌ FAILED: cannot create container from $image"
		echo "  $cid"
		return 1
	fi
	if docker cp "$cid:$check_file" /tmp/.docker-verify-tmp 2>/dev/null; then
		rm -f /tmp/.docker-verify-tmp
		docker rm "$cid" > /dev/null 2>&1
		echo "  ✅ Verified: $check_file exists"
		return 0
	else
		rm -f /tmp/.docker-verify-tmp
		docker rm "$cid" > /dev/null 2>&1
		echo "  ❌ FAILED: $check_file not found in $image"
		return 1
	fi
}

function at-repos-build-docker-atproto() {
	cd $d
	local failed=()
	if [ -z "$1" ];then
		for ((i=1; i<=${#services}; i++)); do
			service=${services[$i]}
			echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
			echo "🔨 Building: $service"
			if ! docker compose build --no-cache $service; then
				echo "  ❌ Build failed: $service"
				failed+=($service)
				continue
			fi
			if ! at-repos-docker-verify $service; then
				failed+=($service)
				continue
			fi
			if [ "$service" = "ozone" ]; then
				docker compose build --no-cache ${service}-web
			fi
		done
	else
		echo "🔨 Building: $1"
		if ! docker compose build --no-cache $1; then
			echo "❌ Build failed: $1"
			return 1
		fi
		if ! at-repos-docker-verify $1; then
			return 1
		fi
	fi
	if [ ${#failed[@]} -gt 0 ]; then
		echo ""
		echo "❌ Failed builds: ${failed[*]}"
		echo "⚠️  Do NOT push these images."
		return 1
	fi
	echo ""
	echo "✅ All builds verified successfully."
}

function at-repos-push-reset() {
	if [ -n "$(docker ps -q -f name=registry)" ]; then
		echo "Registry is already running."
		docker restart registry
		docker stop registry
		docker rm registry
		docker volume rm registry-data 2>/dev/null || true
	fi
	docker run -d -p ${dport}:${dport} --name registry \
		--restart=always \
		-v registry-data:/var/lib/registry \
		registry:2
}

function at-repos-push-docker() {
	local dtag=$(date +%Y%m%d)
	if [ -z "$1" ] || [ "$1" = "push" ]; then
		for service in "${services[@]}"; do
			if ! at-repos-docker-verify $service; then
				echo "⚠️  Skipping push: $service (verification failed)"
				continue
			fi
			docker tag at-${service}:latest localhost:${dport}/${service}:latest
			docker tag at-${service}:latest localhost:${dport}/${service}:${dtag}
			docker push localhost:${dport}/${service}:latest
			docker push localhost:${dport}/${service}:${dtag}
			if [ "$service" = "ozone" ]; then
				docker tag at-${service}-web:latest localhost:${dport}/${service}-web:latest
				docker tag at-${service}-web:latest localhost:${dport}/${service}-web:${dtag}
				docker push localhost:${dport}/${service}-web:latest
				docker push localhost:${dport}/${service}-web:${dtag}
			fi
		done
	else
		if ! at-repos-docker-verify $1; then
			echo "❌ Push aborted: $1 (verification failed)"
			return 1
		fi
		docker tag at-${1}:latest localhost:${dport}/${1}:latest
		docker tag at-${1}:latest localhost:${dport}/${1}:${dtag}
		docker push localhost:${dport}/${1}:latest
		docker push localhost:${dport}/${1}:${dtag}
	fi
	echo "📦 Pushed with tags: latest, ${dtag}"
}

function at-repos-pull-docker() {
	cd $d
	docker compose -f deploy.yml up -d --pull always
	echo "💡 Run 'docker image prune' manually to clean up old images."
}

function at-repos-reset-bgs-db() {
	dp=at-database-1
	BGS_ADMIN_KEY=`cat $d/envs/bgs | grep BGS_ADMIN_KEY | cut -d '=' -f 2`
	
	echo "🛑 Stopping BGS..."
	docker compose stop bgs
	
	echo "🗑️  Cleaning data..."
	sudo rm -rf $d/data/bgs/*
	
	echo "♻️  Resetting Databases..."
	docker exec -i $dp psql -U postgres -c "DROP DATABASE IF EXISTS bgs;"
	docker exec -i $dp psql -U postgres -c "CREATE DATABASE bgs;"
	docker exec -i $dp psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE bgs TO postgres;"
	
	docker exec -i $dp psql -U postgres -c "DROP DATABASE IF EXISTS carstore;"
	docker exec -i $dp psql -U postgres -c "CREATE DATABASE carstore;"
	docker exec -i $dp psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE carstore TO postgres;"

	echo "🚀 Starting BGS to initialize tables..."
	docker compose up -d bgs
	
	echo "⏳ Waiting 10s for BGS migration..."
	sleep 10
	
	echo "⚙️  Updating Slurp Config..."
	docker exec -i $dp psql -U postgres -d bgs -c "UPDATE slurp_configs SET new_subs_disabled = false, new_pds_per_day_limit = 1000 WHERE id = 1;"

	# host=pds:3000
	echo "🔗 Registering Trusted Domain..."
	# Retry loop for addTrustedDomain as BGS might still be warming up
	for i in {1..5}; do
		if curl -f -X POST "https://bgs.${host}/admin/pds/addTrustedDomain?domain=${host}" -H "Authorization: Bearer ${BGS_ADMIN_KEY}"; then
			echo ""
			echo "✅ Trusted domain registered"
			break
		fi
		echo "Failed to contact BGS (attempt $i/5)... waiting 5s"
		sleep 5
	done

	echo "🔗 Requesting PDS Crawl..."
	# Request BGS to crawl the PDS - this registers the PDS and starts subscription
	for i in {1..5}; do
		result=$(curl -s -X POST "https://bgs.${host}/admin/pds/requestCrawl" \
			-H "Authorization: Bearer ${BGS_ADMIN_KEY}" \
			-H "Content-Type: application/json" \
			-d "{\"hostname\":\"${host}\"}" \
			-w "%{http_code}" -o /dev/null)
		if [ "$result" = "200" ]; then
			echo "✅ PDS crawl requested successfully"
			break
		fi
		echo "Failed to request crawl (attempt $i/5, status: $result)... waiting 5s"
		sleep 5
	done

	echo "⏳ Waiting 5s for BGS to connect to PDS..."
	sleep 5

	echo "🔄 Triggering repo sync for existing users..."
	for ((i=1; i<=${#handles}; i++)); do
		handle=${handles[$i]}
		did=$(curl -sL "https://${host}/xrpc/com.atproto.repo.describeRepo?repo=${handle}" | jq -r .did)
		if [ -n "$did" ] && [ "$did" != "null" ]; then
			echo "  Syncing repo: $handle ($did)"
			# Use takedown=false to trigger a resync without actually taking down
			curl -s -X POST "https://bgs.${host}/admin/repo/takedown?did=${did}&takedown=false" \
				-H "Authorization: Bearer ${BGS_ADMIN_KEY}" || true
		else
			echo "  Skipping $handle (DID not found)"
		fi
	done

	echo ""
	echo "✅ BGS reset complete!"
	echo "   PDS should now be subscribed and syncing repos."
}

function at-repos-feed-generator-start-push() {
	cd $d/repos/feed-generator
	yarn install
	FEEDGEN_HANDLE=${handle}
	FEEDGEN_PASSWORD=${APP_PASSWORD}
	FEEDGEN_RECORD_NAME=app
	FEEDGEN_AVATAR=$d/repos/atproto/packages/dev-env/assets/at.png
	npx tsx scripts/publish.ts
}

function at-repos-feed-generator-update() {

	resp=$(curl -sL -X POST -H "Content-Type: application/json" -d "{\"identifier\":\"$handle\",\"password\":\"${APP_PASSWORD}\"}" https://${host}/xrpc/com.atproto.server.createSession)
	token=$(echo $resp | jq -r .accessJwt)
	if [ -z "$token" ] || [ "$token" == "null" ]; then
		echo "Login failed: $resp"
		exit 1
	fi

	avatar_json="{\"\$type\":\"blob\",\"ref\":{\"\$link\":\"${img_id}\"},\"mimeType\":\"image/jpeg\",\"size\":375259}"

	# 3. Delete cmd record
	#curl -sL -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $token" \
	#  -d "{\"repo\":\"$handle\",\"collection\":\"app.bsky.feed.generator\",\"rkey\":\"cmd\"}" \
	#  https://${host}/xrpc/com.atproto.repo.deleteRecord

	# 4. Put app record
	echo "Creating app record..."
	now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

	# Create JSON payload
	# Note: feeding avatar_json directly into jq
	payload=$(jq -n \
		--arg repo "$handle" \
		--arg collection "app.bsky.feed.generator" \
		--arg rkey "app" \
		--arg did "did:web:feed.${host}" \
		--arg type "app.bsky.feed.generator" \
		--arg created "$now" \
		--arg display "App Feed" \
		--arg desc "Automated App Feed" \
		--argjson avatar "$avatar_json" \
		'{
			repo: $repo,
			collection: $collection,
			rkey: $rkey,
			record: {
				did: $did,
				"$type": $type,
				createdAt: $created,
				displayName: $display,
				description: $desc,
				avatar: $avatar
			}
	}')
curl -sL -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $token" \
	-d @- \
	https://${host}/xrpc/com.atproto.repo.putRecord
}

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Patch creation helpers
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

# Find existing patches that modify the same file
# Usage: at-patch-find-conflicts <filepath> <patch-dir>
function at-patch-find-conflicts() {
	local filepath="$1"
	local patch_dir="$2"
	local conflicts=()

	if [ ! -d "$patch_dir" ]; then
		return
	fi

	for pf in "$patch_dir"/*.patch(N) "$patch_dir"/*.diff(N); do
		[ -f "$pf" ] || continue
		if grep -q "^--- a/$filepath" "$pf" 2>/dev/null || grep -q "^+++ b/$filepath" "$pf" 2>/dev/null; then
			conflicts+=("$(basename "$pf")")
		fi
	done

	if [ ${#conflicts[@]} -gt 0 ]; then
		echo "⚠️  This file is also modified by:"
		for c in "${conflicts[@]}"; do
			echo "   - $c"
		done
		echo ""
		echo "   Ensure patches are applied in order before patch-begin."
		echo "   Baseline must reflect the post-earlier-patches state."
	fi
}

# Save current file state as baseline for patch creation
# Usage: ./install.zsh patch-begin <repo> <file-path> [--ios]
# Example: ./install.zsh patch-begin social-app "src/screens/Profile/Header/ProfileHeaderStandard.tsx" --ios
function at-patch-begin() {
	local repo="$1"
	local filepath="$2"
	local flag="$3"

	if [ -z "$repo" ] || [ -z "$filepath" ]; then
		echo "Usage: ./install.zsh patch-begin <repo> <file-path> [--ios]"
		echo "Example: ./install.zsh patch-begin social-app \"src/screens/Profile/Header/ProfileHeaderStandard.tsx\" --ios"
		return 1
	fi

	local full_path="$d/repos/$repo/$filepath"
	if [ ! -f "$full_path" ]; then
		echo "❌ File not found: $full_path"
		return 1
	fi

	local tmp_file="/tmp/patch-base--$(echo "$filepath" | tr '/' '-')"
	cp "$full_path" "$tmp_file"

	echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
	echo "✅ Baseline saved: $tmp_file"
	echo "   File: $full_path"
	echo ""

	# Check for conflicting patches
	if [ "$flag" = "--ios" ]; then
		at-patch-find-conflicts "$filepath" "$d/ios/patching"
	else
		at-patch-find-conflicts "$filepath" "$d/patching"
	fi

	echo "Next steps:"
	echo "  1. Edit: $full_path"
	echo "  2. Save: ./install.zsh patch-save <NNN-name.patch> $repo \"$filepath\" ${flag:---ios}"
	echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
}

# Generate patch from baseline diff
# Usage: ./install.zsh patch-save <patch-filename> <repo> <file-path> [--ios]
# Example: ./install.zsh patch-save 042-social-app-ios-feature.patch social-app "src/path/to/file.tsx" --ios
function at-patch-save() {
	local patch_filename="$1"
	local repo="$2"
	local filepath="$3"
	local flag="$4"

	if [ -z "$patch_filename" ] || [ -z "$repo" ] || [ -z "$filepath" ]; then
		echo "Usage: ./install.zsh patch-save <patch-filename> <repo> <file-path> [--ios]"
		echo "Example: ./install.zsh patch-save 042-social-app-ios-feature.patch social-app \"src/file.tsx\" --ios"
		return 1
	fi

	local full_path="$d/repos/$repo/$filepath"
	local tmp_file="/tmp/patch-base--$(echo "$filepath" | tr '/' '-')"

	if [ ! -f "$tmp_file" ]; then
		echo "❌ No baseline found. Run 'patch-begin' first."
		return 1
	fi
	if [ ! -f "$full_path" ]; then
		echo "❌ File not found: $full_path"
		return 1
	fi

	# Determine output directory
	local patch_dir="$d/patching"
	if [ "$flag" = "--ios" ]; then
		patch_dir="$d/ios/patching"
	fi

	# Generate diff with proper a/b paths
	diff -u "$tmp_file" "$full_path" \
		| sed "1s|--- .*|--- a/$filepath|" \
		| sed "2s|+++ .*|+++ b/$filepath|" \
		> "$patch_dir/$patch_filename"

	local line_count
	line_count=$(wc -l < "$patch_dir/$patch_filename" | tr -d ' ')
	if [ "$line_count" -eq 0 ]; then
		echo "⚠️  No differences found. Patch file is empty."
		rm "$patch_dir/$patch_filename"
		return 1
	fi

	echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
	echo "📝 Patch: $patch_dir/$patch_filename ($line_count lines)"

	# Dry-run verify: restore baseline, test patch, restore edit
	pushd "$d/repos/$repo" > /dev/null
	cp "$full_path" /tmp/patch-edited-tmp
	cp "$tmp_file" "$full_path"

	if patch --dry-run -p1 < "$patch_dir/$patch_filename" > /dev/null 2>&1; then
		echo "✅ Dry-run: OK"
	else
		echo "❌ Dry-run: FAILED"
		patch --dry-run -p1 < "$patch_dir/$patch_filename" 2>&1 | head -5
	fi

	cp /tmp/patch-edited-tmp "$full_path"
	rm -f /tmp/patch-edited-tmp
	popd > /dev/null

	rm -f "$tmp_file"
	echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
}

# Verify all patches can be applied (full dry-run)
# Usage: ./install.zsh patch-check [--ios]
function at-patch-check() {
	local flag="$1"

	if [ "$flag" = "--ios" ]; then
		echo "Checking iOS patches against: $d/repos/social-app"
		pushd "$d/repos/social-app" > /dev/null
		for pf in "$d/ios/patching"/*.patch; do
			[ -f "$pf" ] || continue
			local name="$(basename "$pf")"
			if patch --dry-run -p1 < "$pf" > /dev/null 2>&1; then
				echo "  ✅ $name"
			else
				echo "  ❌ $name"
			fi
		done
		popd > /dev/null
	else
		echo "Checking server patches..."
		for pf in "$d/patching"/*.patch "$d/patching"/*.diff; do
			[ -f "$pf" ] || continue
			echo "  $(basename "$pf")"
		done
	fi
}

at-repos-env
case "$1" in
	pull)
		at-repos-clone
		at-repos-pull
		exit
		;;
	patch)
		at-repos-social-app-ios-patch
		at-repos-patch-apply-all
		at-repos-ozone-patch
		at-repos-feed-generator-newfiles
		show-failed-patches
		exit
		;;
	patch-begin)
		at-patch-begin "$2" "$3" "$4"
		exit
		;;
	patch-save)
		at-patch-save "$2" "$3" "$4" "$5"
		exit
		;;
	patch-check)
		at-patch-check "$2"
		exit
		;;
	build)
		at-repos-build-docker-atproto $2
		exit
		;;
	push)
		at-repos-push-docker $2
		exit
		;;
	reset)
		at-repos-push-reset
		exit
		;;
	down)
		cd $d;docker compose down
		exit
		;;
	feed-push)
		at-repos-feed-generator-start-push
		exit
		;;
esac

case "`cat /etc/hostname`" in
	at)
		if [ "$1" = "bgs-reset" ];then
			# at-repos-reset-bgs-db
			exit
		fi
		at-repos-pull-docker
		exit
		;;
	*)
		at-repos-clone
		at-repos-pull
		at-repos-checkout-pinned
		at-repos-social-app-ios-patch
		at-repos-patch-apply-all
		at-repos-ozone-patch
		at-repos-feed-generator-newfiles
		show-failed-patches
		at-repos-build-docker-atproto
		at-repos-push-docker
		cd $d; docker compose down
		;;
esac
