#################################################################################
# GLOBALS                                                                       #
#################################################################################

PROJECT_NAME = lnp-ml
PYTHON_VERSION = 3.8
PYTHON_INTERPRETER = python

# --- CLI flag 变量 ---
MPNN_FLAG = $(if $(NO_MPNN),,--use-mpnn)
DEVICE_FLAG = $(if $(DEVICE),--device $(DEVICE),)
SEED_FLAG = $(if $(SEED),--seed $(SEED),)
N_TRIALS_FLAG = $(if $(N_TRIALS),--n-trials $(N_TRIALS),)
EPOCHS_PER_TRIAL_FLAG = $(if $(EPOCHS_PER_TRIAL),--epochs-per-trial $(EPOCHS_PER_TRIAL),)
MIN_STRATUM_FLAG = $(if $(MIN_STRATUM_COUNT),--min-stratum-count $(MIN_STRATUM_COUNT),)
OUTPUT_DIR_FLAG = $(if $(OUTPUT_DIR),--output-dir $(OUTPUT_DIR),)
USE_SWA_FLAG = $(if $(USE_SWA),--use-swa,)
PARALLEL_FLAG = $(if $(PARALLEL),--parallel,)
INIT_PRETRAIN_FLAG = $(if $(NO_PRETRAIN),,--init-from-pretrain $(or $(INIT_PRETRAIN),models/pretrain_delivery.pt))

# --- MoE 相关 flag ---
USE_MOE_FLAG       = $(if $(USE_MOE),--use-moe,)
MOE_EXPERTS_FLAG   = $(if $(MOE_EXPERTS),--moe-n-experts $(MOE_EXPERTS),)
MOE_TOPK_FLAG      = $(if $(MOE_TOPK),--moe-top-k $(MOE_TOPK),)
MOE_HIDDEN_FLAG    = $(if $(MOE_HIDDEN_MULT),--moe-expert-hidden-mult $(MOE_HIDDEN_MULT),)
MOE_JITTER_FLAG    = $(if $(MOE_JITTER),--moe-jitter-noise $(MOE_JITTER),)
MOE_FLAGS          = $(USE_MOE_FLAG) $(MOE_EXPERTS_FLAG) $(MOE_TOPK_FLAG) $(MOE_HIDDEN_FLAG) $(MOE_JITTER_FLAG)

#################################################################################
# ENVIRONMENT & CODE QUALITY                                                    #
#################################################################################

## Install Python dependencies
.PHONY: requirements
requirements:
	pixi install

## Set up Python interpreter environment
.PHONY: create_environment
create_environment:
	@echo ">>> Pixi environment will be created when running 'make requirements'"
	@echo ">>> Activate with:\npixi shell"

## Delete all compiled Python files
.PHONY: clean
clean:
	find . -type f -name "*.py[co]" -delete
	find . -type d -name "__pycache__" -delete

## Lint using ruff (use `make format` to do formatting)
.PHONY: lint
lint:
	ruff format --check
	ruff check

## Format source code with ruff
.PHONY: format
format:
	ruff check --fix
	ruff format

#################################################################################
# DATA PROCESSING                                                              #
#################################################################################

## Preprocess internal data (raw -> interim)
.PHONY: preprocess
preprocess: requirements
	$(PYTHON_INTERPRETER) scripts/preprocess_internal.py

## Process external data for pretrain (external -> processed)
.PHONY: data_pretrain
data_pretrain: requirements
	$(PYTHON_INTERPRETER) scripts/process_external.py

## Process baseline CV data for benchmark (external/all_amine_split_for_LiON -> processed/benchmark)
.PHONY: data_benchmark
data_benchmark: requirements
	$(PYTHON_INTERPRETER) scripts/process_benchmark_data.py

#################################################################################
# BENCHMARKING                                                                  #
#################################################################################

## Benchmark on baseline CV data: 5-fold train + test (delivery only)
.PHONY: benchmark
benchmark: requirements
	$(PYTHON_INTERPRETER) -m lnp_ml.modeling.benchmark main $(MPNN_FLAG) $(DEVICE_FLAG)
	$(PYTHON_INTERPRETER) -m lnp_ml.modeling.benchmark test $(DEVICE_FLAG)

## Evaluate baseline method on public test splits (test.csv vs preds.csv in cv_*)
.PHONY: baseline
baseline: requirements
	$(PYTHON_INTERPRETER) scripts/evaluate_external.py

#################################################################################
# TRAINING (Nested CV + Optuna)                                                #
#################################################################################
# 通用参数：
#   SEED              随机种子 (默认: 42)
#   N_TRIALS          Optuna 试验数 (默认: 20)
#   EPOCHS_PER_TRIAL  每个试验的最大 epoch (默认: 30)
#   MIN_STRATUM_COUNT 复合分层标签的最小样本数 (默认: 5)
#   OUTPUT_DIR        输出目录 (根据命令有不同默认值)
#   INIT_PRETRAIN     预训练权重路径 (默认: models/pretrain_delivery.pt)
#   NO_PRETRAIN=1     禁用预训练权重
#   USE_SWA=1         启用 SWA (final train 阶段)
#   PARALLEL=1        并行运行外层 fold (nested CV 阶段，需足够 GPU 显存)
#
# 使用示例: 
# make pretrain
# make train DEVICE=cuda N_TRIALS=30 USE_SWA=1 INIT_PRETRAIN=models/pretrain_delivery.pt
# make train DEVICE=cuda PARALLEL=1


## Pretrain on external data (delivery only)
.PHONY: pretrain
pretrain: requirements
	$(PYTHON_INTERPRETER) -m lnp_ml.modeling.pretrain main $(MPNN_FLAG) $(DEVICE_FLAG)

## Train: nested CV evaluation + final model training
## Step 1: 外层 5-fold 产生无偏性能估计，内层 3-fold 做超参搜索
## Step 2: 3-fold 调参后用全量数据训练最终模型
.PHONY: train
train: requirements
	$(PYTHON_INTERPRETER) -m lnp_ml.modeling.nested_cv_optuna \
		$(DEVICE_FLAG) $(MPNN_FLAG) $(SEED_FLAG) $(INIT_PRETRAIN_FLAG) \
		$(N_TRIALS_FLAG) $(EPOCHS_PER_TRIAL_FLAG) $(MIN_STRATUM_FLAG) $(OUTPUT_DIR_FLAG) $(PARALLEL_FLAG) \
		$(MOE_FLAGS)
	$(PYTHON_INTERPRETER) -m lnp_ml.modeling.final_train_optuna_cv \
		$(DEVICE_FLAG) $(MPNN_FLAG) $(SEED_FLAG) $(INIT_PRETRAIN_FLAG) \
		$(N_TRIALS_FLAG) $(EPOCHS_PER_TRIAL_FLAG) $(MIN_STRATUM_FLAG) $(OUTPUT_DIR_FLAG) $(USE_SWA_FLAG) \
		$(MOE_FLAGS)
		
#################################################################################
# INTERPRETABILITY  (biodistribution feature importance)                       #
#################################################################################
# 参数：
#   ORGAN    器官 (lymph_nodes, heart, liver, spleen, lung, kidney, muscle, all; 默认: all)
#   METHOD   token 级方法 (ig, ablation, attention, all; 默认: ig)
#   DESC_IG  同时计算 desc 内部特征 IG (1 启用; 默认不启用)
#   DESC_TOP_K 可视化展示的 top-K 特征数 (默认: 30)
#   DATA     数据路径 (默认: data/interim/internal.csv)
#   MODEL    模型路径 (默认: models/final/model.pt)

METHOD_FLAG = $(if $(METHOD),--method $(METHOD),)
DATA_FLAG = $(if $(DATA),--data-path $(DATA),)
MODEL_FLAG = $(if $(MODEL),--model-path $(MODEL),)
ORGAN_FLAG = $(if $(ORGAN),--organ $(ORGAN),)
DESC_IG_FLAG = $(if $(DESC_IG),--desc-ig,)
DESC_TOP_K_FLAG = $(if $(DESC_TOP_K),--desc-top-k $(DESC_TOP_K),)

## Compute biodistribution feature importance (token-level)
.PHONY: feature_importance
feature_importance: requirements
	$(PYTHON_INTERPRETER) -m lnp_ml.interpretability.token_importance \
		$(ORGAN_FLAG) $(METHOD_FLAG) $(DATA_FLAG) $(MODEL_FLAG) $(DEVICE_FLAG) \
		$(DESC_IG_FLAG) $(DESC_TOP_K_FLAG)

## Compute biodistribution feature importance (token + descriptor-level)
.PHONY: desc_importance
desc_importance: requirements
	$(PYTHON_INTERPRETER) -m lnp_ml.interpretability.token_importance \
		--desc-ig $(ORGAN_FLAG) $(METHOD_FLAG) $(DATA_FLAG) $(MODEL_FLAG) \
		$(DEVICE_FLAG) $(DESC_TOP_K_FLAG)

#################################################################################
# SERVING & DEPLOYMENT                                                          #
#################################################################################

## Formulation optimization: find optimal LNP formulation for target organ
## Usage: make optimize SMILES="CC(C)..." ORGAN=liver
.PHONY: optimize
optimize: requirements
	$(PYTHON_INTERPRETER) -m app.optimize --smiles "$(SMILES)" --organ $(ORGAN) $(DEVICE_FLAG)

## Start FastAPI backend server (port 8000)
.PHONY: api
api: requirements
	uvicorn app.api:app --host 0.0.0.0 --port 8000 --reload

## Start Streamlit frontend app (port 8501)
.PHONY: webapp
webapp: requirements
	streamlit run app/app.py --server.port 8501

## Start both API and webapp (run in separate terminals)
.PHONY: serve
serve:
	@echo "请在两个终端分别运行:"
	@echo "  终端 1: make api"
	@echo "  终端 2: make webapp"
	@echo ""
	@echo "然后访问: http://localhost:8501"

#################################################################################
# DOCKER                                                                        #
#################################################################################

## Build Docker images
.PHONY: docker-build
docker-build:
	docker compose build

## Start all services with Docker Compose
.PHONY: docker-up
docker-up:
	docker compose up -d

## Stop all Docker services
.PHONY: docker-down
docker-down:
	docker compose down

## View Docker logs
.PHONY: docker-logs
docker-logs:
	docker compose logs -f

## Build and start all services
.PHONY: docker-serve
docker-serve: docker-build docker-up
	@echo ""
	@echo "🚀 服务已启动!"
	@echo "   - API:       http://localhost:8000"
	@echo "   - Web 应用:  http://localhost:8501"
	@echo ""
	@echo "查看日志: make docker-logs"
	@echo "停止服务: make docker-down"

## Clean Docker resources (images, volumes, etc.)
.PHONY: docker-clean
docker-clean:
	docker compose down -v --rmi local
	docker system prune -f

#################################################################################
# Self Documenting Commands                                                     #
#################################################################################

.DEFAULT_GOAL := help

define PRINT_HELP_PYSCRIPT
import re, sys; \
lines = '\n'.join([line for line in sys.stdin]); \
matches = re.findall(r'\n## (.*)\n[\s\S]+?\n([a-zA-Z_-]+):', lines); \
print('Available rules:\n'); \
print('\n'.join(['{:25}{}'.format(*reversed(match)) for match in matches]))
endef
export PRINT_HELP_PYSCRIPT

help:
	@$(PYTHON_INTERPRETER) -c "${PRINT_HELP_PYSCRIPT}" < $(MAKEFILE_LIST)
