# clide — local dev targets. CI scripts in ci/ shell out to these.
#
# Single Flutter package at the repo root. Native supporter tools
# (ptyc, future peers) live in their own directories.

INSTALL_DIR ?= $(HOME)/.local/bin

# -- OS detection ------------------------------------------------------------
# Maps uname to Flutter device/build targets: linux, macos, windows.

UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
  FLUTTER_OS := linux
else ifeq ($(UNAME_S),Darwin)
  FLUTTER_OS := macos
else
  # Windows (MSYS2, Git Bash, Cygwin all report variants with "MINGW"/"MSYS"/"CYGWIN")
  FLUTTER_OS := windows
endif

VERSION      ?= $(shell awk -F': *' '/^version:/ {gsub(/[" ]/,"",$$2); print $$2; exit}' pubspec.yaml)
NAME         ?= $(shell awk -F': *' '/^name:/ {gsub(/[" ]/,"",$$2); print $$2; exit}' pubspec.yaml)
TAGLINE      ?= $(shell awk -F': *' '/^tagline:/ {sub(/^tagline: *"?/,"",$$0); sub(/"$$/,"",$$0); print; exit}' pubspec.yaml)
REPOSITORY   ?= $(shell awk -F': *' '/^repository:/ {sub(/^repository: */,"",$$0); print; exit}' pubspec.yaml)
COMMIT       ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
DATE         ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)

.PHONY: help
help: ## Show this help.
	@awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z0-9_-]+:.*##/ {printf "  \033[36m%-22s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)

# -- app (Flutter) -------------------------------------------------------

.PHONY: run
run: gen-build-info clide-cli ## Launch the Flutter desktop app.
	# CLIDE_CLI_BIN points the in-app "Install clide command in PATH"
	# affordance (T-212) at the dev-tree C client; a packaged build finds it
	# beside the GUI runner in the bundle instead.
ifeq ($(FLUTTER_OS),linux)
	CLIDE_CLI_BIN=$(CURDIR)/$(CLIDE_CLI_BIN) GDK_BACKEND=x11 LD_LIBRARY_PATH=$(CURDIR)/native/linux-x64$${LD_LIBRARY_PATH:+:$$LD_LIBRARY_PATH} flutter run -d linux --dart-define=CLIDE_PROJECT=$(CURDIR)
else
	CLIDE_CLI_BIN=$(CURDIR)/$(CLIDE_CLI_BIN) flutter run -d $(FLUTTER_OS) --dart-define=CLIDE_PROJECT=$(CURDIR)
endif

TESTMODE_CATEGORY ?= all
TESTMODE_TIMEOUT  ?= 60

.PHONY: run-testmode
run-testmode: ## Launch ClideTestApp (TESTMODE_CATEGORY=toolchain|ipc|extensions|all).
ifeq ($(FLUTTER_OS),linux)
	@GDK_BACKEND=x11 LD_LIBRARY_PATH=$(CURDIR)/native/linux-x64$${LD_LIBRARY_PATH:+:$$LD_LIBRARY_PATH} \
	  flutter run -d linux \
	    --dart-define=CLIDE_PROJECT=$(CURDIR) \
	    --dart-define=CLIDE_TESTMODE=$(TESTMODE_CATEGORY) 2>&1 \
	  | tee /tmp/clide-testmode.log & PID=$$!; \
	  (sleep $(TESTMODE_TIMEOUT) && kill $$PID 2>/dev/null) & TIMER=$$!; \
	  wait $$PID 2>/dev/null; kill $$TIMER 2>/dev/null; \
	  grep -q '"failed":0' /tmp/clide-testmode.log
else
	@flutter run -d $(FLUTTER_OS) \
	    --dart-define=CLIDE_PROJECT=$(CURDIR) \
	    --dart-define=CLIDE_TESTMODE=$(TESTMODE_CATEGORY) 2>&1 \
	  | tee /tmp/clide-testmode.log & PID=$$!; \
	  (sleep $(TESTMODE_TIMEOUT) && kill $$PID 2>/dev/null) & TIMER=$$!; \
	  wait $$PID 2>/dev/null; kill $$TIMER 2>/dev/null; \
	  grep -q '"failed":0' /tmp/clide-testmode.log
endif

.PHONY: pubget
pubget: ## flutter pub get.
	flutter pub get

.PHONY: build-check
build-check: gen-build-info ## Verify native + Dart build compiles (no run).
ifeq ($(FLUTTER_OS),linux)
	LD_LIBRARY_PATH=$(CURDIR)/native/linux-x64$${LD_LIBRARY_PATH:+:$$LD_LIBRARY_PATH} flutter build linux
else
	flutter build $(FLUTTER_OS)
endif

.PHONY: analyze
analyze: ## flutter analyze.
	flutter analyze

.PHONY: format
format: ## dart format --set-exit-if-changed.
	dart format --set-exit-if-changed .

# The single bake of every build-time fact derived from pubspec.yaml
# + git + clock. Runs implicitly as a prereq of every target that
# compiles or executes Dart code so nobody has to remember it.
#
# Outputs:
#  - lib/src/build_info.g.dart — gitignored, fresh on every build.
#    Exposes `clideName`, `clideTagline`, `clideVersion`, `clideRepository`
#    (from pubspec), `clideCommit` (git short SHA), `clideDate` (UTC
#    now) for the app to read directly.
#  - assets/licenses.yaml `self.version:` — rewritten in place so the
#    bundled license manifest never drifts from pubspec. (Tracked in
#    git; the rewrite is a no-op when in sync.)
.PHONY: gen-build-info
gen-build-info:
	@printf '// GENERATED — do not edit. Regenerated by `make` on every\n// build/run/test (see gen-build-info in Makefile). Name / tagline /\n// version / repository come from pubspec.yaml — the single source\n// of truth. Commit + date stamp at run time.\nconst String clideName = '"'"'%s'"'"';\nconst String clideTagline = '"'"'%s'"'"';\nconst String clideVersion = '"'"'%s'"'"';\nconst String clideRepository = '"'"'%s'"'"';\nconst String clideCommit = '"'"'%s'"'"';\nconst String clideDate = '"'"'%s'"'"';\n' "$(NAME)" "$(TAGLINE)" "$(VERSION)" "$(REPOSITORY)" "$(COMMIT)" "$(DATE)" > lib/src/build_info.g.dart
	@awk -v v="$(VERSION)" '/^self:/ {in_self=1} in_self && /^[[:space:]]+version:/ {sub(/version:.*/, "version: \"" v "\""); in_self=0} {print}' assets/licenses.yaml > assets/licenses.yaml.tmp && mv assets/licenses.yaml.tmp assets/licenses.yaml

.PHONY: verify
verify: gen-build-info analyze format decisions-validate changelog-gate ## No-tests sweep — gen-build-info + analyze + format + decisions-validate + changelog-gate. For mid-edit "are the gates green?" checks; `push-check` is the full pre-push pipeline.

.PHONY: t
t: gen-build-info ## Run one test path with tee'd output. Usage: make t T=test/path/to/spec.dart
	@mkdir -p test/.test-output
	@if [ -z "$(T)" ]; then echo "usage: make t T=test/path/to/spec.dart" >&2; exit 2; fi
	flutter test $(T) 2>&1 | tee test/.test-output/last.log

.PHONY: test
test: gen-build-info ## Fast dev loop: analyze + format + unit + widget + golden, NO coverage, parallel (~20s).
	ci/test.sh

.PHONY: test-coverage
test-coverage: gen-build-info ## Same suite WITH coverage → coverage/lcov.info (for the gate / CI). Slower.
	ci/test.sh --coverage

.PHONY: test-core
test-core: gen-build-info ## Core subsystem tests (IPC, PTY, git, pane registry).
	ci/test_core.sh

.PHONY: test-a11y
test-a11y: gen-build-info ## A11y contract (semantic coverage + keyboard + contrast + i18n).
	ci/test_a11y.sh

.PHONY: test-integration
test-integration: gen-build-info ## Integration tests (real app boot; xvfb on headless Linux).
	ci/test_integration.sh

.PHONY: test-e2e
test-e2e: ## End-to-end Playwright smoke.
	ci/test_e2e.sh

.PHONY: test-all
test-all: test-core test test-a11y test-integration test-e2e ## Everything, sequentially.

.PHONY: coverage-gate
coverage-gate: ## Coverage gate — fails if total line % < pubspec.yaml `coverage_floor:` (D-66). Assumes `make test-coverage` ran first.
	ci/coverage_gate.sh

.PHONY: changelog-gate
changelog-gate: ## Changelog concision gate — fails on `## [Unreleased]` bullets over 60 words.
	ci/changelog_gate.sh

.PHONY: smoke-bundle
smoke-bundle: ## Build Linux release bundle and run it under xvfb for 5s.
	ci/smoke_bundle.sh

# -- web UI harness ------------------------------------------------------

.PHONY: ui-dev
ui-dev: ## Build web WASM + start localhost:4280 in the background.
	tools/ui/build.sh
	tools/ui/serve.sh

.PHONY: ui-stop
ui-stop: ## Stop the background web server.
	tools/ui/stop.sh

.PHONY: ui-smoke
ui-smoke: ## Build + serve + run Playwright smoke + stop.
	tools/ui/build.sh
	tools/ui/serve.sh
	@sh -c 'trap "tools/ui/stop.sh >/dev/null 2>&1" EXIT; cd tools/ui && npx playwright test smoke.spec.ts'

.PHONY: build
build: gen-build-info clide-cli ## flutter build for the current OS (incl. the C CLI client).
	flutter build $(FLUTTER_OS)
	@install -m 755 $(CLIDE_CLI_BIN) $(CLI_BUNDLE_DEST)
	@echo "==> bundled C client at $(CLI_BUNDLE_DEST)"

.PHONY: build-linux
build-linux: gen-build-info ## flutter build linux (desktop bundle).
	flutter build linux

.PHONY: build-macos
build-macos: gen-build-info ## flutter build macos (desktop bundle).
	flutter build macos

.PHONY: build-windows
build-windows: gen-build-info ## flutter build windows (desktop bundle).
	flutter build windows

# -- install / uninstall -----------------------------------------------------

# Install prefix. Bundle lands at $(INSTALL_PREFIX)/clide/ with a
# symlink at $(INSTALL_DIR)/clide pointing into it.
INSTALL_PREFIX ?= $(HOME)/.local/lib

ifeq ($(FLUTTER_OS),linux)
  BUNDLE_DIR := build/linux/x64/release/bundle
  # The C client ships beside the GUI runner so the in-app "Install clide
  # command in PATH" affordance (T-212) can self-install from the bundle.
  CLI_BUNDLE_DEST := $(BUNDLE_DIR)/clide-cli
else ifeq ($(FLUTTER_OS),macos)
  BUNDLE_DIR := build/macos/Build/Products/Release/clide.app
  CLI_BUNDLE_DEST := $(BUNDLE_DIR)/Contents/MacOS/clide-cli
else ifeq ($(FLUTTER_OS),windows)
  BUNDLE_DIR := build/windows/x64/runner/Release
  CLI_BUNDLE_DEST := $(BUNDLE_DIR)/clide-cli.exe
endif

ICON_SIZES := 16 32 48 128 192 256 512

.PHONY: install
install: build clide-cli ## Build + install clide to ~/.local (INSTALL_PREFIX, INSTALL_DIR).
ifeq ($(FLUTTER_OS),linux)
	@mkdir -p $(INSTALL_PREFIX) $(INSTALL_DIR)
	rm -rf $(INSTALL_PREFIX)/clide
	cp -a $(BUNDLE_DIR) $(INSTALL_PREFIX)/clide
	install -m 755 $(CLIDE_CLI_BIN) $(INSTALL_DIR)/clide
	@for size in $(ICON_SIZES); do \
	  dir=$(HOME)/.local/share/icons/hicolor/$${size}x$${size}/apps; \
	  mkdir -p $$dir; \
	  cp assets/logo/appicon-$${size}.png $$dir/clide.png; \
	done
	@mkdir -p $(HOME)/.local/share/applications
	@sed 's|Exec=clide|Exec=$(INSTALL_PREFIX)/clide/clide|' linux/clide.desktop \
	  > $(HOME)/.local/share/applications/net.schweitz.clide.desktop
	@gtk-update-icon-cache -f -t $(HOME)/.local/share/icons/hicolor 2>/dev/null || true
	@update-desktop-database $(HOME)/.local/share/applications 2>/dev/null || true
	@echo "cli:       $(INSTALL_DIR)/clide (C client)"
	@echo "gui:       $(INSTALL_PREFIX)/clide/clide"
	@echo "desktop:   ~/.local/share/applications/net.schweitz.clide.desktop"
	@echo "version:   $(VERSION)"
else ifeq ($(FLUTTER_OS),macos)
	@mkdir -p $(HOME)/Applications $(INSTALL_DIR)
	rm -rf $(HOME)/Applications/clide.app
	cp -a $(BUNDLE_DIR) $(HOME)/Applications/clide.app
	install -m 755 $(CLIDE_CLI_BIN) $(INSTALL_DIR)/clide
	@echo "gui:       ~/Applications/clide.app"
	@echo "cli:       $(INSTALL_DIR)/clide (C client)"
	@echo "version:   $(VERSION)"
else
	@echo "install not yet supported on $(FLUTTER_OS)"
	@exit 1
endif

.PHONY: uninstall
uninstall: ## Remove installed clide.
ifeq ($(FLUTTER_OS),linux)
	rm -f $(INSTALL_DIR)/clide
	rm -rf $(INSTALL_PREFIX)/clide
	rm -f $(HOME)/.local/share/applications/net.schweitz.clide.desktop
	rm -f $(HOME)/.local/share/applications/clide.desktop
	@for size in $(ICON_SIZES); do \
	  rm -f $(HOME)/.local/share/icons/hicolor/$${size}x$${size}/apps/clide.png; \
	done
	@gtk-update-icon-cache -f -t $(HOME)/.local/share/icons/hicolor 2>/dev/null || true
	@update-desktop-database $(HOME)/.local/share/applications 2>/dev/null || true
	@echo "uninstalled"
else ifeq ($(FLUTTER_OS),macos)
	rm -f $(INSTALL_DIR)/clide
	rm -rf $(HOME)/Applications/clide.app
	@echo "uninstalled"
endif

# -- dugite-native (bundled git) ------------------------------------------

DUGITE_VERSION := v2.53.0-3
DUGITE_COMMIT  := f49d009

ifeq ($(FLUTTER_OS),macos)
  ifeq ($(shell uname -m),arm64)
    DUGITE_PLATFORM := macOS-arm64
  else
    DUGITE_PLATFORM := macOS-x64
  endif
else ifeq ($(FLUTTER_OS),linux)
  DUGITE_PLATFORM := ubuntu-x64
else
  DUGITE_PLATFORM := windows-x64
endif

DUGITE_TAR := dugite-native-v2.53.0-$(DUGITE_COMMIT)-$(DUGITE_PLATFORM).tar.gz
DUGITE_URL := https://github.com/desktop/dugite-native/releases/download/$(DUGITE_VERSION)/$(DUGITE_TAR)
DUGITE_DIR := native/dugite

.PHONY: dugite-fetch
dugite-fetch: ## Download and extract the dugite-native git distribution.
	@if [ -f $(DUGITE_DIR)/bin/git ]; then echo "dugite already present at $(DUGITE_DIR)/bin/git"; exit 0; fi
	@mkdir -p $(DUGITE_DIR)
	curl -sL "$(DUGITE_URL)" | tar xz -C $(DUGITE_DIR)
	@echo "dugite-native $(DUGITE_VERSION) ($(DUGITE_PLATFORM)) extracted to $(DUGITE_DIR)/"

.PHONY: dugite-clean
dugite-clean: ## Remove the dugite-native directory.
	rm -rf $(DUGITE_DIR)

# -- clide-cli ----------------------------------------------------------
# The C `clide` shell client that talks to the in-process IPC server
# (T-99 / T-126). One source file, no third-party deps; the build
# target picks up whatever `cc` is on PATH.

CLIDE_CLI_SRC := native/clide-cli/clide.c
ifeq ($(FLUTTER_OS),windows)
  CLIDE_CLI_BIN := native/windows-x64/clide.exe
else
  CLIDE_CLI_BIN := native/$(if $(filter Darwin,$(shell uname -s)),macos,linux)-$(shell uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/')/clide
endif
CC ?= cc

.PHONY: clide-cli
clide-cli: $(CLIDE_CLI_BIN) ## Compile the C `clide` shell client.

$(CLIDE_CLI_BIN): $(CLIDE_CLI_SRC)
	@mkdir -p $(dir $(CLIDE_CLI_BIN))
ifeq ($(FLUTTER_OS),windows)
	ci/build_cli_windows.sh
else
	$(CC) -std=c99 -O2 -Wall -Wextra -o $(CLIDE_CLI_BIN) $(CLIDE_CLI_SRC)
endif
	@echo "==> built $(CLIDE_CLI_BIN)"

.PHONY: clide-cli-clean
clide-cli-clean: ## Remove the compiled C `clide` client.
	rm -f $(CLIDE_CLI_BIN)

# -- security -------------------------------------------------------------

.PHONY: security
security: ## Supply-chain gate — osv-scanner over pubspec.lock (CI PR-merge pipeline; run locally on demand). Fails on a known advisory.
	ci/osv_scan.sh

# -- pre-push gate --------------------------------------------------------

.PHONY: decisions-validate
decisions-validate: ## Parser dry-run over governance/{decisions,questions,rejected}/*.md.
	pql decisions validate

.PHONY: push-check
# NOTE: the `security` (osv-scanner) gate is deliberately NOT in push-check —
# it runs in the CI PR-merge pipeline (where the scanner is provisioned) so we
# don't force every dev machine to install osv-scanner. Run it locally any time
# with `make security`.
push-check: decisions-validate changelog-gate test-coverage coverage-gate test-core ## Pre-push gate (fast — <2 min target). Order is fail-fast: instant gates (decisions, changelog) first, then the coverage suite + gate (the expensive, most-likely-to-fail stage) BEFORE test-core — a coverage miss aborts here instead of after running everything, so a fix doesn't force a full re-run of the rest. test-coverage already runs the a11y suite (test/a11y), so no separate test-a11y pass.

.PHONY: push-check-full
push-check-full: push-check test-integration smoke-bundle ## Pre-release gate (push-check + integration + smoke; slower).

.PHONY: hooks
hooks: ## Install the repo's git hooks.
	git config core.hooksPath .githooks
	@echo "hooks installed (core.hooksPath = .githooks)"

# -- housekeeping ---------------------------------------------------------

.PHONY: clean
clean: ## Remove build artefacts.
	rm -rf build .dart_tool

.DEFAULT_GOAL := help
