# Local process control for LazyBoy.
#   make build     cargo + production web UI (web/dist)
#   make box       Docker computer image (lazyboy-box:local)
#   make start     rebuild, then API :8787 + Vite :5173
#   make restart   stop, build, start
#   make stop

.DEFAULT_GOAL := help
.NOTPARALLEL: build start restart
export CARGO_BUILD_JOBS ?= 2

ROOT    := $(abspath $(dir $(lastword $(MAKEFILE_LIST))))
RUNDIR  := $(ROOT)/.run
BIN     := $(ROOT)/target/debug/lazyboy
WEBDIR  := $(ROOT)/web

HOST ?= 0.0.0.0
PORT_API ?= 8787
PORT_WEB ?= 5173

PID_SERVE := $(RUNDIR)/serve.pid
PID_WEB   := $(RUNDIR)/web.pid
LOG_SERVE := $(RUNDIR)/serve.log
LOG_WEB   := $(RUNDIR)/web.log

.PHONY: help build build-bin build-ui box start stop restart status logs prepare-ui start-serve start-web stop-serve stop-web

# Shared POSIX helpers. Sourced by each recipe.
define CTL
alive() {
	pidfile="$$1"
	[ -f "$$pidfile" ] || return 1
	pid=$$(cat "$$pidfile")
	[ -n "$$pid" ] && kill -0 "$$pid" 2>/dev/null
}

kill_tree() {
	pid="$$1"
	[ -n "$$pid" ] || return 0
	for child in $$(pgrep -P "$$pid" 2>/dev/null); do
		kill_tree "$$child"
	done
	kill -TERM "$$pid" 2>/dev/null || true
}

kill_tree_hard() {
	pid="$$1"
	[ -n "$$pid" ] || return 0
	for child in $$(pgrep -P "$$pid" 2>/dev/null); do
		kill_tree_hard "$$child"
	done
	kill -KILL "$$pid" 2>/dev/null || true
}

free_port() {
	port="$$1"
	extra=$$(lsof -nP -tiTCP:"$$port" -sTCP:LISTEN 2>/dev/null || true)
	[ -n "$$extra" ] || return 0
	echo "freeing :$$port ($$extra)"
	kill -TERM $$extra 2>/dev/null || true
	sleep 0.2
	extra=$$(lsof -nP -tiTCP:"$$port" -sTCP:LISTEN 2>/dev/null || true)
	[ -n "$$extra" ] && kill -KILL $$extra 2>/dev/null || true
}

stop_one() {
	name="$$1"
	pidfile="$$2"
	port="$$3"
	if alive "$$pidfile"; then
		pid=$$(cat "$$pidfile")
		echo "stopping $$name pid=$$pid"
		kill_tree "$$pid"
		n=0
		while [ $$n -lt 15 ] && kill -0 "$$pid" 2>/dev/null; do
			sleep 0.2
			n=$$((n + 1))
		done
		kill_tree_hard "$$pid"
	elif [ -f "$$pidfile" ]; then
		echo "$$name not running (stale pidfile)"
	else
		echo "$$name not running"
	fi
	rm -f "$$pidfile"
	free_port "$$port"
}

wait_alive() {
	pidfile="$$1"
	name="$$2"
	log="$$3"
	i=0
	while [ $$i -lt 15 ]; do
		if alive "$$pidfile"; then
			return 0
		fi
		sleep 0.1
		i=$$((i + 1))
	done
	echo "$$name exited immediately. Last log:"
	tail -n 40 "$$log" 2>/dev/null || true
	rm -f "$$pidfile"
	return 1
}

wait_listen() {
	pidfile="$$1"
	port="$$2"
	name="$$3"
	log="$$4"
	i=0
	while [ $$i -lt 150 ]; do
		if lsof -nP -iTCP:"$$port" -sTCP:LISTEN >/dev/null 2>&1; then
			return 0
		fi
		if [ -f "$$pidfile" ]; then
			pid=$$(cat "$$pidfile")
			if [ -n "$$pid" ] && ! kill -0 "$$pid" 2>/dev/null; then
				echo "$$name died before listening on :$$port. Last log:"
				tail -n 40 "$$log" 2>/dev/null || true
				rm -f "$$pidfile"
				return 1
			fi
		fi
		sleep 0.2
		i=$$((i + 1))
	done
	echo "$$name did not listen on :$$port. Last log:"
	tail -n 40 "$$log" 2>/dev/null || true
	return 1
}

status_one() {
	name="$$1"
	pidfile="$$2"
	port="$$3"
	if alive "$$pidfile"; then
		pid=$$(cat "$$pidfile")
		listen=no
		lsof -nP -iTCP:"$$port" -sTCP:LISTEN >/dev/null 2>&1 && listen=yes
		echo "$$name  running  pid=$$pid  :$$port listen=$$listen"
	elif [ -f "$$pidfile" ]; then
		echo "$$name  stopped  (stale pidfile)"
	else
		echo "$$name  stopped"
	fi
}

load_env() {
	if [ -f "$(ROOT)/.env" ]; then
		set -a
		. "$(ROOT)/.env"
		set +a
	fi
}
endef
export CTL

help:
	@echo "make build     Compile API + production web UI (web/dist; phone uses :$(PORT_API))"
	@echo "make box       Build Docker computer image lazyboy-box:local (first time / after box/)"
	@echo "make start     Rebuild and start API (:$(PORT_API)) and frontend (:$(PORT_WEB))"
	@echo "make stop      Stop both"
	@echo "make restart   Stop, rebuild, start"
	@echo "make status    Show PIDs and ports"
	@echo "make logs      Tail both logs (Ctrl-C to leave)"

build: build-bin build-ui
	@echo "built $(BIN) and $(WEBDIR)/dist"

build-bin:
	@echo "building lazyboy…"
	@cargo build -p lazyboy

build-ui:
	@if [ ! -f "$(WEBDIR)/node_modules/.lazyboy-lock" ] || ! cmp -s "$(WEBDIR)/package-lock.json" "$(WEBDIR)/node_modules/.lazyboy-lock"; then \
		echo "installing locked web dependencies…"; \
		(cd "$(WEBDIR)" && npm ci) || exit 1; \
		cp "$(WEBDIR)/package-lock.json" "$(WEBDIR)/node_modules/.lazyboy-lock"; \
	fi
	@echo "building web UI…"
	@(cd "$(WEBDIR)" && NODE_OPTIONS="$${NODE_OPTIONS:---max-old-space-size=1024}" npm run build) || exit 1

box:
	@echo "building computer image lazyboy-box:local…"
	@bash "$(ROOT)/box/build.sh"

start: build
	@$(MAKE) start-serve
	@$(MAKE) start-web
	@echo
	@echo "API  http://127.0.0.1:$(PORT_API)"
	@echo "UI   http://127.0.0.1:$(PORT_WEB)"
	@lan=$$(awk '/手機同一個 Wi-Fi：/ {sub("http://", "", $$NF); sub(/:[0-9]+$$/, "", $$NF); print $$NF; exit}' "$(LOG_SERVE)"); \
	if [ -n "$$lan" ]; then \
		echo "Phone http://$$lan:$(PORT_API)"; \
	fi

restart: stop
	@$(MAKE) start

stop: stop-web stop-serve
	@echo "stopped"

status:
	@eval "$$CTL"; \
	status_one serve "$(PID_SERVE)" "$(PORT_API)"; \
	status_one web "$(PID_WEB)" "$(PORT_WEB)"

logs:
	@mkdir -p "$(RUNDIR)"
	@touch "$(LOG_SERVE)" "$(LOG_WEB)"
	@echo "=== serve $(LOG_SERVE) ==="
	@echo "=== web   $(LOG_WEB) ==="
	@tail -f "$(LOG_SERVE)" "$(LOG_WEB)"

prepare-ui: build-ui

start-serve:
	@mkdir -p "$(RUNDIR)"
	@eval "$$CTL"; \
	if alive "$(PID_SERVE)"; then \
		echo "restarting serve with rebuilt binary"; \
		stop_one serve "$(PID_SERVE)" "$(PORT_API)"; \
	fi; \
	load_env; \
	LAZYBOY_WEB_HOST="$(HOST)" LAZYBOY_WEB_PORT="$(PORT_API)" nohup "$(BIN)" serve > "$(LOG_SERVE)" 2>&1 < /dev/null & echo $$! > "$(PID_SERVE)"; \
	wait_alive "$(PID_SERVE)" serve "$(LOG_SERVE)" || exit 1; \
	wait_listen "$(PID_SERVE)" "$(PORT_API)" serve "$(LOG_SERVE)" || exit 1; \
	echo "started serve pid=$$(cat "$(PID_SERVE)")  :$(PORT_API)  log=$(LOG_SERVE)"

start-web:
	@mkdir -p "$(RUNDIR)"
	@eval "$$CTL"; \
	if alive "$(PID_WEB)"; then \
		stop_one web "$(PID_WEB)" "$(PORT_WEB)"; \
	fi; \
	if [ ! -d "$(WEBDIR)/node_modules" ]; then \
		echo "npm install…"; \
		(cd "$(WEBDIR)" && npm install) || exit 1; \
	fi; \
	NODE_OPTIONS="$${NODE_OPTIONS:---max-old-space-size=1024}" LAZYBOY_WEB_PORT="$(PORT_API)" nohup npm --prefix "$(WEBDIR)" run dev -- --host "$(HOST)" --port "$(PORT_WEB)" > "$(LOG_WEB)" 2>&1 < /dev/null & echo $$! > "$(PID_WEB)"; \
	wait_alive "$(PID_WEB)" web "$(LOG_WEB)" || exit 1; \
	wait_listen "$(PID_WEB)" "$(PORT_WEB)" web "$(LOG_WEB)" || exit 1; \
	echo "started web pid=$$(cat "$(PID_WEB)")  :$(PORT_WEB)  log=$(LOG_WEB)"

stop-serve:
	@eval "$$CTL"; stop_one serve "$(PID_SERVE)" "$(PORT_API)"

stop-web:
	@eval "$$CTL"; stop_one web "$(PID_WEB)" "$(PORT_WEB)"
