import re
from sys import stderr

import pygit2

from pygittools.hooks import CommitMsg, Hook, HookResult, PreReceive, RefUpdate

NULL_OID: str = "0" * 40

"""
Commit-Msg Pattern Hook (Client-side)
Validates the commit message against a regular expression.
"""


class CommitMsgPattern(CommitMsg):
	def __init__(self, repo: pygit2.Repository, pattern: str):
		super().__init__(repo)
		self.exp = re.compile(pattern)

	def run(self, message_file: str) -> HookResult:
		with open(message_file) as f:
			message = f.read()
		if not self.exp.match(message):
			stderr.write(f"Commit message does not match pattern: {self.exp.pattern}\n")
			return HookResult.FAILURE
		return HookResult.SUCCESS


"""
Update Pattern Hook (Server-side)
Invoked by git-receive-pack once per ref being updated, before the ref is updated.
Use this hook to:
- Enforce fast-forward only (reject non-FF updates)
- Implement per-ref access control
- Log or validate old → new for specific refs
Takes three parameters: ref name, old object name, new object name.
"""


class UpdatePattern(Hook):
	def __init__(self, repo: pygit2.Repository, ref_pattern: str, msg_pattern: str):
		super().__init__(repo)
		self.ref_exp = re.compile(ref_pattern)
		self.msg_exp = re.compile(msg_pattern)

	def run(self, ref_name: str, old_oid: str, new_oid: str) -> HookResult:
		if self.ref_exp.match(ref_name):
			commit = self.repo.revparse_single(new_oid).peel(pygit2.Commit)
			if not self.msg_exp.match(commit.message):
				stderr.write(f"Commit message does not match pattern: {self.msg_exp.pattern}\n")
				return HookResult.FAILURE
		return HookResult.SUCCESS


"""
Pre-Receive Protected Pattern Hook (Server-side)
Invoked by git-receive-pack once before any refs are updated. For each ref update that
matches a protected ref pattern, walks every commit being introduced (old..new) and
validates non-merge commit messages against a pattern. Merge commits are skipped by
default; set reject_merge_commits to refuse merge commits and require squash/rebase.
"""


class PreReceiveProtectedPattern(PreReceive):
	def __init__(
		self,
		repo: pygit2.Repository,
		ref_pattern: str,
		msg_pattern: str,
		*,
		reject_merge_commits: bool = False,
	) -> None:
		super().__init__(repo)
		self.ref_exp: re.Pattern[str] = re.compile(ref_pattern)
		self.msg_exp: re.Pattern[str] = re.compile(msg_pattern)
		self.reject_merge_commits: bool = reject_merge_commits

	def run(self, ref_updates: list[RefUpdate]) -> HookResult:
		for ref_name, old_oid, new_oid in ref_updates:
			if not self.ref_exp.match(ref_name):
				continue
			result: HookResult = self._validate_ref_update(ref_name, old_oid, new_oid)
			if result != HookResult.SUCCESS:
				return result
		return HookResult.SUCCESS

	def _validate_ref_update(self, ref_name: str, old_oid: str, new_oid: str) -> HookResult:
		if new_oid == NULL_OID:
			return HookResult.SUCCESS
		try:
			new_commit: pygit2.Commit = self.repo.revparse_single(new_oid).peel(pygit2.Commit)
		except (KeyError, pygit2.GitError):
			stderr.write(f"Invalid commit {new_oid} for {ref_name}\n")
			return HookResult.FAILURE

		old_commit: pygit2.Commit | None = None
		if old_oid != NULL_OID:
			try:
				old_commit = self.repo.revparse_single(old_oid).peel(pygit2.Commit)
			except (KeyError, pygit2.GitError):
				old_commit = None

		for commit in self._commits_in_range(new_commit, old_commit):
			if len(commit.parents) > 1:
				if self.reject_merge_commits:
					stderr.write(
						f"Merge commit {commit.id} rejected on protected ref {ref_name}; "
						"use squash or rebase instead.\n",
					)
					return HookResult.FAILURE
				continue
			if not self.msg_exp.match(commit.message):
				stderr.write(
					f"Commit {commit.id} on {ref_name} does not match pattern: {self.msg_exp.pattern}\n",
				)
				return HookResult.FAILURE
		return HookResult.SUCCESS

	def _commits_in_range(
		self,
		new_commit: pygit2.Commit,
		old_commit: pygit2.Commit | None,
	) -> list[pygit2.Commit]:
		excluded: set[pygit2.Oid] = set()
		if old_commit is not None:
			for ancestor in self.repo.walk(old_commit.id):
				excluded.add(ancestor.id)
		commits: list[pygit2.Commit] = []
		for commit in self.repo.walk(new_commit.id):
			if commit.id in excluded:
				continue
			commits.append(commit)
		return commits