# LOCAL MACHINE
# This is a bunch of stuff that idk what even is this
uname 					:= $(shell uname)
mkfile_dir 				:= $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST)))))

gdatecmd 				:= $(shell "command" -v gdate)
ifdef gdatecmd
gdate = gdate
else
gdate = date
endif
gdateok := $(shell 2>&1 $(gdate) --version | grep coreutils > /dev/null; echo $$?)
ifneq ($(gdateok), 0)
$(info **WARNING**: Missing coreutils "date" command. Please install coreutils.) # Needed for make dist
endif

gtarcmd 				:= $(shell "command" -v gtar)
ifdef gtarcmd
gtar = gtar
else
gtar = tar
endif
gtarok := $(shell 2>&1 $(gtar) --version | grep 'GNU tar' > /dev/null; echo $$?)
ifneq ($(gtarok), 0)
$(info **WARNING**: Missing GNU "tar" command. Please install gnu-tar.) # Needed for make dist
endif

permstr = -perm +111
ifneq ($(uname), Darwin)
permstr = -perm /111
endif

# These sections set variables for make to track timestamps for the purposes of rebuilding files
# PYTHON ENVIRONMENT
PYTHONHASHSEED 			= 10
VIRTUAL_ENV 			?= $(mkfile_dir)/.venv
root_dir 				= $(shell pwd)
# python 					= $(VIRTUAL_ENV)/bin/python3
python					= $(shell find $(VIRTUAL_ENV)/bin -name "python3.12" -or -name "python3" 2>/dev/null | head -n 1)
sphinx 					= $(python) -m sphinx
pip 					= $(python) -m pip
flake8 					= $(python) -m flake8
sphinx-apidoc 			= $(python) -m sphinx.ext.apidoc

# NSCLI
version 				= $(shell $(python) -c "import sys; sys.path.insert(0, \"$(mkfile_dir)\"); import nscli; print(nscli.__version__)")
requires.txt 			= nscli.egg-info/requires.txt
#egginfo 				= nscli.egg-info
config.db 				= nscli/data/config.db
nscli_files 			= $(shell find nscli -type f \! -name \*.pyc)
pkg_sources 			= $(nscli_files) $(manpages) $(completions)
constraints.txt			= constraints.txt

# DOCUMENTATION
email_src 				= docs/src/ReleaseEmail.rst
email_html 				= docs/email/ReleaseEmail.html
changelog 				= docs/src/Changelog.rst
current_changelog 		= docs/src/CurrentChangelog.rst
news_files 				= $(shell find docs/src/newsfragments -type f)
regionlist 				= docs/src/RegionList.rst
docs/src 				= $(shell find docs/src -type f -name \*.rst)
webpage 				= docs/gh-pages
manpages 				= docs/man/nscli.1.gz docs/man/nscli_settings.5.gz
bash_completion 		= completions/bash/nscli
zsh_completion 			= completions/zsh/_nscli
completions 			= $(bash_completion) $(zsh_completion)

# PYTHON PACKAGING
tarball 				= dist/nscli-$(version).tar.gz
wheel 					= dist/nscli-$(version)-py3-none-any.whl

# DEBIAN PACKAGING
# The .deb installer files encode a lot of information in the filename. Here we declare the filenames we wish to create,
# which makes for easy targets. The recipe to make the .deb files parses relevant information from the desired filename
# (using the debparse_* macros defined below) and passes it to the build environment. To support a new .deb release,
# just declare the filename below and include it in $(debian_releases) for convenience. There needs to be a
# corresponding docker image available on the hub; for example, debian:10 or ubuntu:19.04.
ubuntu2204 				= dist/debian/nscli_$(version)-1ubuntu1~22.04_amd64.deb
ubuntu2404				= dist/debian/nscli_$(version)-1ubuntu1~24.04_amd64.deb
debian_releases			= $(ubuntu2204) $(ubuntu2404)
debian_control_files 	= $(shell find debian -type f)
debparse_dist_id 		= $(shell echo $(1) | cut -d '-' -f 2 | cut -d '~' -f 1 | tr -d '[:digit:]')
debparse_dist_ver 		= $(shell echo $(1) | cut -d '-' -f 2 | cut -d '~' -f 2 | cut -d '_' -f 1)
debparse_image_name 	= nscli_builder_$(call debparse_dist_id,$1)_$(call debparse_dist_ver,$1):$(version)
debparse_codename 		= $(shell docker run --rm $(call debparse_image_name,$1) grep -m 1 CODENAME /etc/os-release | cut -d '=' -f 2)

.PHONY: all
all: help

.PHONY: clean
clean: docclean  ## Delete temporary files
	-rm -rf dist build .eggs nscli.egg-info .tox .coverage htmlcov coverage.xml .config share version
	-rm -f $(config.db)
	-find nscli -name __pycache__ -exec rm -rf "{}" \;
	-find nscli -name '*.pyc' -exec rm -f "{}" \;

.PHONY: distclean
distclean: clean  ## Delete anything that's not part of the repo
	git reset HEAD --hard
	git clean -fxd

.PHONY: imageclean
imageclean:
	docker rmi $(docker images --filter=reference='*/nscli:*' -q) 2> /dev/null || echo "no nscli images found"

.PHONY: test
test: $(config.db) $(manpages) $(python) $(completions) ## Run tests
	# Ensure necessary test credentials are loaded
	$(python) -m nscli.tests.tools
	# Run test suites
	$(python) -m tox

# make the tar.gz and whl
.PHONY: dist
dist: $(config.db) $(pkg_sources) $(manpages) $(python) | version_check
	$(python) -m build

.PHONY: debian
debian: $(debian_releases)  ## Create new Debian and Ubuntu packages

.PHONY: debian22
debian22: $(ubuntu2204)

.PHONY: debian24
debian24: $(ubuntu2404)

.PHONY: webpage
webpage: $(webpage)  ## Regenerate the webpage files

# something happened with editable installs and the location returned from importlib.metadata.distribution("nscli")
# we create a symlink to trick python that has nscli installed as an editable package where to find the files
.PHONY: site_link
site_link:
	ln -s $(root_dir)/nscli $(shell $(python) -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])') || echo "Link already exists"

.PHONY: devenv
devenv:  ## Set up your nscli dev environment. Creates a virtual environment.
	# ensure VENV is not active
	if [[ ! -z $$VIRTUAL_ENV ]]; then echo ; echo "!! ERROR !! Deactivate VENV before running 'make devenv'"; echo; exit 1; fi
	# install dev packages from lockfile and don't update lockfile
	-pdm venv remove in-project -y
	pdm sync -G dev
	make $(config.db)

.PHONY: build_env
build_env:  ## Set up your nscli dev environment. Creates a virtual environment.
	# ensure VENV is not active
	if [[ ! -z $$VIRTUAL_ENV ]]; then echo ; echo "!! ERROR !! Deactivate VENV before running 'make build_env'"; echo; exit 1; fi
	-pdm venv remove in-project -y
	# install build packages from lockfile and don't update lockfile
	pdm sync -G build
	make $(config.db)

.PHONY: release_env
release_env:  # Set up your nscli dev environment. Creates a virtual environment.
	# ensure VENV is not active
	if [[ ! -z $$VIRTUAL_ENV ]]; then echo ; echo "!! ERROR !! Deactivate VENV before running 'make release_env'"; echo; exit 1; fi
	-pdm venv remove in-project -y
	# install prod packages from lockfile and don't update lockfile
	pdm sync --prod
	make $(config.db)

.PHONY: lint
lint: $(python) version_check  ## Check code style
	# Check for files with executable bits set. This should only be nscli/shared/2048.sh and nscli/__main__.py
	$(eval execs:=$(shell find nscli -type f $(permstr)))
	test "$(execs)" = "nscli/shared/2048.sh nscli/__main__.py" -o "nscli/__main__.py nscli/shared/2048.sh"
	# All directories and Python files in NSCLI must be lowercase and must not include spaces or dashes.
	! find nscli -type f -name '*.py' -o -type d | grep -e '[A-Z ]'
	# All Python packages under nscli must include a __init__.py file. If a directory contains Python code but no init,
	# the next line will error out.
	test -z `find nscli -type d '!' -exec test -e "{}/__init__.py" ';' -exec find "{}" -name '*.py' ';'`
	$(python) scripts/plugin_checker.py
	$(flake8) --config=$(mkfile_dir)/pyproject.toml

.PHONY: webpage_upload
webpage_upload: $(webpage) ## Upload the nscli documentation webpage to the gh-pages branch in GitHub
	# Verify the gh_pages branch exists. This branch should be pulled down, not created new
	git rev-parse --verify gh-pages || (echo "gh-pages branch not found. Try 'git fetch --all && git checkout gh-pages'" && exit 1)
	$(eval branch := $(shell git rev-parse --abbrev-ref HEAD))
	git checkout gh-pages
	git pull
	git rm -rf .  # un-stage all files
	git add docs/gh-pages/*  # stage only the output files we need
	git mv docs/gh-pages/* .  # move output files to root of repo
	touch .nojekyll && git add .nojekyll  # This file tells github to serve up the site as-is
	git commit --all -m "rebuild docs"
	git push
	git checkout $(branch)
	-$(python) -c 'import webbrowser; webbrowser.open("https://gh-pages.pvceng.rax.io/RSS-Engineering/nsi-nscli/")'

.PHONY: email
email: $(email_html) $(python)  ## Send the release-day email
	$(python) scripts/send_release_email.py -t

.PHONY: upload
upload: ## Upload a new distribution to artifactory from the dist/ folder.
	pdm publish --no-build --repository https://artifacts.rackspace.net/artifactory/api/pypi/nscli-pypi-local

.PHONY: version_check
version_check: $(python)
	@echo Validating version number...
	@$(python) scripts/versioning.py

.PHONY: docclean
docclean:
	# Delete cached documentation to avoid a lot of bugs in Sphinx. Especially required for sphinx_selective_exclude
	-find . -name .doctrees -exec rm -rf "{}" \;

.PHONY: help
help:
# Help function shamelessly stolen from the Rackspace Engineering Handbook
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

.PHONY: buildcontainer
buildcontainer:
# Build the nscli testing environment locally. Best left to Jenkins.
	docker build --progress=plain --pull -t nscli_base:latest .
	test -f docker/credentials.json
	echo 'FROM nscli_base:latest' | docker build --progress=plain -t nscli_loaded:latest . -f -

# this creates a requirements.txt with "prod" packages from the .egg-info, the flag to use artifactory, and a forced install of importlib_metadata
requirements.txt: $(requires.txt)
	@printf "%s\n" '-i https://artifacts.rackspace.net/artifactory/api/pypi/pypi/simple/' > requirements.txt
	@cat $(requires.txt) | sed -e '/^$$/,$$d' >> requirements.txt
	# TODO: This next line is a gross hack
	@printf "%s\n" importlib_metadata >> requirements.txt

$(constraints.txt): pdm.lock scripts/generate_constraints.py
	$(python) scripts/generate_constraints.py > constraints.txt

version: nscli/__init__.py
	@printf "$(version)\n" > version && chmod 644 version

%.deb: requirements.txt $(pkg_sources) $(manpages) $(debian_control_files)
# $@ will be the target name (the deb file)
	@mkdir -p "$(dir $@)"
# we use docker so we can build the .deb package in the exact Ubuntu version target environment
	docker build --no-cache --platform linux/amd64 --progress=plain --pull -t $(call debparse_image_name,$@) \
		--build-arg VERSION=$(version) \
		--build-arg DIST_VERSION=$(call debparse_dist_ver,$@) \
		--build-arg DIST_ID=$(call debparse_dist_id,$@) \
		-f debian/Dockerfile .
# change dir to /dpkg and create a new archive named for the deb target (notdir strips the directory path leaving just the filename)
# pipe this into a verbose extraction of the archive into the directory specified by $(dir $@) (i.e. the dir named after the deb target)
# --rm deletes the container after use
	docker run --platform linux/amd64 --rm $(call debparse_image_name,$@) tar -C /dpkg -c "$(notdir $@)" | tar -C "$(dir $@)" -xv

.PHONY: debian_upload
debian_upload: $(debian_releases:.deb=.deb.upload)  ## Uploads the .deb release files to artifactory

%.deb.upload: %.deb
	$(eval FILE=$*.deb)
	@if test -z "$(API_TOKEN)"; then \
		echo "The API_TOKEN environment variable for artifactory is not exported."; \
		exit 1; \
	fi
	@if curl -fsI "https://artifacts.rackspace.net/artifactory/nscli-debian-local/pool/$(notdir $(FILE))" > /dev/null; then \
		echo $(notdir $(FILE)) already exists on artifactory.; \
		return 0; \
	fi
	@echo Uploading $(FILE)
	@curl -i -H "Authorization: Bearer $(API_TOKEN)" -XPUT -T $(FILE) \
	-H "X-Checksum-Sha1: $(shell shasum -a 1 $(FILE) | awk '{print $$1}')" -H "X-Checksum-Md5: $(shell md5sum $(FILE) | awk '{print $$1}')" \
	"https://artifacts.rackspace.net/artifactory/nscli-debian-local/pool/$(notdir $(FILE));deb.distribution=$(call debparse_codename,$(FILE));deb.component=main;deb.architecture=amd64" && echo

$(python):
	make build_env

$(webpage): $(regionlist) $(docs/src) $(python) $(constraints.txt)
	PYTHONHASHSEED=$(PYTHONHASHSEED) $(sphinx) -b html docs/src docs/gh-pages -D version="$(version)" -D release="$(version)"
	cp constraints.txt docs/gh-pages/

# NOTE: The $(webpage) target depends on $(docs/src), which is a shell command that expands
# to all .rst files in docs/src/ (including Changelog.rst). Because Changelog.rst is a
# prerequisite, Make looks for a rule to build it and finds the $(changelog) target below.
# Make is a timestamp-based build system that works backwards through the dependency chain,
# rebuilding anything that's out of date before building what you asked for. It checks if
# Changelog.rst is older than any newsfragments (comparing file modification times) and if
# newsfragments are newer, Make automatically runs towncrier to rebuild the changelog before
# building the webpage. This is why 'make webpage' automatically consumes newsfragments
# without explicitly calling 'make changelog'.

.PHONY: changelog
changelog: $(changelog)  ## Updates the changelog from the news files
$(changelog): $(news_files)
	$(python) -m towncrier --yes

$(current_changelog): $(changelog)
	head -n $$(($$(grep -n '=======' docs/src/Changelog.rst | head -n 2 | tail -n 1 | cut -d ':' -f 1)-2)) $(changelog) > $(current_changelog)

$(email_html): $(email_src) $(current_changelog) $(python) | docclean
	$(sphinx) -t email -b html docs/src docs/email -D version="$(version)" -D release="$(version)"

$(regionlist): $(config.db) $(python)
	$(python) scripts/print_regions.py > $(regionlist)

$(config.db): $(python) site_link
	$(python) -c 'from nscli.config_db import DataBase; DataBase("nscli/data/config.db")';

.PHONY: manpage
manpage: $(manpages)  ## Rebuild the man pages
$(manpages): docs/src $(regionlist) $(python) | docclean
	PYTHONHASHSEED=$(PYTHONHASHSEED) $(sphinx) -b man docs/src docs/man -D version="$(version)" -D release="$(version)"
	-rm -rf docs/man/*.gz
	gzip --no-name -9 docs/man/*

.PHONY: completions
completions: $(completions)  ## Rebuild the shell completion files

$(zsh_completion): $(nscli_files) $(python)
	@mkdir -p completions/zsh
	$(python) -m nscli settings --zsh > $(zsh_completion)

$(bash_completion): $(nscli_files) $(python)
	@mkdir -p completions/bash
	$(python) -m nscli settings --bash > $(bash_completion)

needs_review:  ## Generate a list of files that need review per NSCLI's SDLC policy.
	@-rm -f needs_review
	@$(eval all_files := $(shell git ls-tree --name-only -r HEAD nscli))
	@echo "Evaluating each file's commit history, please wait..."
	@for f in $(all_files); do \
		found=$$(git log --since='1 year ago' --pretty=format:"%ct %s" -- "$$f" | grep 'SDLC Review Passed'); \
		if [ -z "$$found" ]; then \
			git log --pretty=format:"%ct $$f%n" -n 1 -- "$$f" >> needs_review; \
		fi; \
	done
	@sort needs_review > needs_review_tmp
	@mv needs_review_tmp needs_review
