"""
Install, remove, and inspect pygittools hook samples in a project's hooks directory.

A "sample" is a file in `pygittools/hook_samples/` whose name encodes the target git
hook as the prefix before the first dot (e.g. `post-receive.notify` installs as
`<hooks_dir>/post-receive`). The hooks directory is whichever path pygit2 reports for
the repo (`<repo>/.git/hooks` for non-bare, `<repo>/hooks` for bare).
"""

from __future__ import annotations

import os
import shutil
import stat
from enum import StrEnum
from pathlib import Path
from typing import TypedDict

import pygit2

import pygittools
from pygitweb.config import settings

SAMPLES_DIR: Path = Path(pygittools.__file__).parent / "hook_samples"

_SAMPLE_LABELS: dict[str, str] = {
	"post-commit.notify": "Post-commit Notify",
	"post-receive.notify": "Post-receive Notify",
	"pre-commit.ruff": "Ruff Pre-commit",
	"commit-msg.pattern": "Commit Message Pattern",
}


HOOK_BUNDLES: dict[str, list[str]] = {
	# "Update Hook" wires both server (post-receive on push) and client (post-commit on local
	# commit) sides into the long-poll change queue, so subscribers wake regardless of where
	# the commit was made.
	"update": ["post-commit.notify", "post-receive.notify"],
}

_BUNDLE_LABELS: dict[str, str] = {
	"update": "Update Hook",
}


class HookStatus(StrEnum):
	INSTALLED = "installed"
	NOT_INSTALLED = "not_installed"
	DIFFERENT = "different"


class HookSample(TypedDict):
	name: str
	target: str
	source: str
	label: str


class HookBundle(TypedDict):
	name: str
	label: str
	members: list[HookSample]


def list_samples() -> list[HookSample]:
	if not SAMPLES_DIR.is_dir():
		return []
	samples: list[HookSample] = []
	for entry in sorted(SAMPLES_DIR.iterdir()):
		if not entry.is_file() or "." not in entry.name or entry.name.lower().endswith(".md"):
			continue
		target: str = entry.name.split(".", 1)[0]
		samples.append({
			"name": entry.name,
			"target": target,
			"source": str(entry),
			"label": _SAMPLE_LABELS.get(entry.name, entry.name),
		})
	return samples


def get_sample(name: str) -> HookSample | None:
	for sample in list_samples():
		if sample["name"] == name:
			return sample
	return None


def _hooks_dir(project: str) -> Path:
	repo: pygit2.Repository = pygit2.Repository(os.path.join(settings.PROJECTROOT, project))
	return Path(repo.path) / "hooks"


def _target_path(project: str, sample: HookSample) -> Path:
	return _hooks_dir(project) / sample["target"]


def status(project: str, sample_name: str) -> HookStatus:
	sample: HookSample | None = get_sample(sample_name)
	if sample is None:
		raise KeyError(f"Unknown hook sample: {sample_name}")
	target: Path = _target_path(project, sample)
	if not target.is_file():
		return HookStatus.NOT_INSTALLED
	try:
		installed_bytes: bytes = target.read_bytes()
		source_bytes: bytes = Path(sample["source"]).read_bytes()
	except OSError:
		return HookStatus.NOT_INSTALLED
	return HookStatus.INSTALLED if installed_bytes == source_bytes else HookStatus.DIFFERENT


def is_installed(project: str, sample_name: str) -> bool:
	return status(project, sample_name) == HookStatus.INSTALLED


def install(project: str, sample_name: str) -> HookStatus:
	"""Copy the sample to the hooks dir. Refuses to overwrite a file with different content."""
	sample: HookSample | None = get_sample(sample_name)
	if sample is None:
		raise KeyError(f"Unknown hook sample: {sample_name}")
	current: HookStatus = status(project, sample_name)
	if current == HookStatus.DIFFERENT:
		return current
	target: Path = _target_path(project, sample)
	target.parent.mkdir(parents=True, exist_ok=True)
	shutil.copyfile(sample["source"], target)
	try:
		mode: int = target.stat().st_mode
		target.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
	except OSError:
		pass
	return status(project, sample_name)


def remove(project: str, sample_name: str) -> HookStatus:
	"""Delete the hook file only if its content still matches the sample (preserves custom hooks)."""
	sample: HookSample | None = get_sample(sample_name)
	if sample is None:
		raise KeyError(f"Unknown hook sample: {sample_name}")
	current: HookStatus = status(project, sample_name)
	if current == HookStatus.INSTALLED:
		_target_path(project, sample).unlink(missing_ok=True)
	return status(project, sample_name)


def list_bundles() -> list[HookBundle]:
	bundles: list[HookBundle] = []
	for name, member_names in HOOK_BUNDLES.items():
		members: list[HookSample] = []
		for member_name in member_names:
			sample: HookSample | None = get_sample(member_name)
			if sample is not None:
				members.append(sample)
		bundles.append({
			"name": name,
			"label": _BUNDLE_LABELS.get(name, name),
			"members": members,
		})
	return bundles


def get_bundle(name: str) -> HookBundle | None:
	for bundle in list_bundles():
		if bundle["name"] == name:
			return bundle
	return None


def bundle_status(project: str, bundle_name: str) -> HookStatus:
	"""Aggregate status for a bundle: DIFFERENT if any member conflicts, INSTALLED if all are,
	otherwise NOT_INSTALLED (treats partial installs as not-installed so re-clicking install
	completes the bundle)."""
	bundle: HookBundle | None = get_bundle(bundle_name)
	if bundle is None:
		raise KeyError(f"Unknown hook bundle: {bundle_name}")
	if not bundle["members"]:
		return HookStatus.NOT_INSTALLED
	statuses: list[HookStatus] = [status(project, member["name"]) for member in bundle["members"]]
	if any(s == HookStatus.DIFFERENT for s in statuses):
		return HookStatus.DIFFERENT
	if all(s == HookStatus.INSTALLED for s in statuses):
		return HookStatus.INSTALLED
	return HookStatus.NOT_INSTALLED


def install_bundle(project: str, bundle_name: str) -> HookStatus:
	"""Install every member of the bundle. Refuses (no-op) if any target holds a different file."""
	bundle: HookBundle | None = get_bundle(bundle_name)
	if bundle is None:
		raise KeyError(f"Unknown hook bundle: {bundle_name}")
	for member in bundle["members"]:
		if status(project, member["name"]) == HookStatus.DIFFERENT:
			return HookStatus.DIFFERENT
	for member in bundle["members"]:
		install(project, member["name"])
	return bundle_status(project, bundle_name)


def remove_bundle(project: str, bundle_name: str) -> HookStatus:
	"""Remove every member of the bundle. Refuses (no-op) if any target holds a different file."""
	bundle: HookBundle | None = get_bundle(bundle_name)
	if bundle is None:
		raise KeyError(f"Unknown hook bundle: {bundle_name}")
	for member in bundle["members"]:
		if status(project, member["name"]) == HookStatus.DIFFERENT:
			return HookStatus.DIFFERENT
	for member in bundle["members"]:
		remove(project, member["name"])
	return bundle_status(project, bundle_name)