"""
Per-project timeline cache for recent repository activity.
"""

from __future__ import annotations

import os
import threading
from typing import Literal, TypedDict

import pygit2

from pygitweb.config import settings
from pygitweb.projects import git_get_projects_list

TIMELINE_CACHE_SIZE: int = 10
TIMELINE_CACHE_TOTAL_EVENTS: int = 200
TIMELINE_CACHE_BATCH_SIZE: int = 5


class TimelineEvent(TypedDict):
	timestamp: int
	oid: str
	kind: Literal["commit", "ref"]
	description: str | None


class TimelineProjectEvent(TypedDict):
	project: str
	event: TimelineEvent


PROJECT_TIMELINE_CACHE: dict[str, list[TimelineEvent]] = {}
_TIMELINE_CACHE_LOCK = threading.Lock()
_TIMELINE_WARMING: bool = False


def _commit_description(commit: pygit2.Commit) -> str | None:
	if not commit.message:
		return None
	first_line: str = commit.message.splitlines()[0].strip()
	return first_line or None


def _collect_commit_events(repo: pygit2.Repository) -> list[TimelineEvent]:
	start_points: list[pygit2.Oid | str] = []
	for ref_name in repo.references:
		if not ref_name.startswith("refs/heads/"):
			continue
		try:
			resolved: pygit2.Reference = repo.references[ref_name].resolve()
			oid: pygit2.Oid | str = resolved.target
			start_points.append(oid)
		except (KeyError, pygit2.GitError, ValueError):
			continue
	if not start_points:
		try:
			head_ref: pygit2.Reference = repo.head
			start_points.append(head_ref.target)
		except (pygit2.GitError, ValueError, KeyError):
			pass
	if not start_points:
		return []

	walker: pygit2.Walker = repo.walk(
		start_points[0],
		pygit2.enums.SortMode(pygit2.GIT_SORT_TIME | pygit2.GIT_SORT_TOPOLOGICAL),
	)
	for oid in start_points[1:]:
		try:
			walker.push(oid)
		except (pygit2.GitError, ValueError):
			continue

	events: list[TimelineEvent] = []
	seen: set[str] = set()
	for commit in walker:
		commit_oid: str = str(commit.id)
		if commit_oid in seen:
			continue
		seen.add(commit_oid)
		events.append({
			"timestamp": commit.commit_time,
			"oid": commit_oid,
			"kind": "commit",
			"description": _commit_description(commit),
		})
	return events


def _collect_ref_events(repo: pygit2.Repository) -> list[TimelineEvent]:
	events: list[TimelineEvent] = []
	for ref_name in repo.references:
		if not ref_name.startswith("refs/tags/"):
			continue
		try:
			ref_obj: pygit2.Reference = repo.references[ref_name].resolve()
			oid_str: str = str(ref_obj.target)
			obj = repo[ref_obj.target]
		except (KeyError, pygit2.GitError, ValueError):
			continue

		if isinstance(obj, pygit2.Tag):
			tag_tagger = obj.tagger
			timestamp: int = tag_tagger.time if tag_tagger else 0
			description: str | None = ref_name.replace("refs/tags/", "", 1)
		elif isinstance(obj, pygit2.Commit):
			timestamp = obj.commit_time
			description = ref_name.replace("refs/tags/", "", 1)
		else:
			continue

		events.append({
			"timestamp": timestamp,
			"oid": oid_str,
			"kind": "ref",
			"description": description or None,
		})
	return events


def build_project_timeline_cache(project: str, max_events: int = TIMELINE_CACHE_SIZE) -> list[TimelineEvent]:
	try:
		repo: pygit2.Repository = pygit2.Repository(os.path.join(settings.PROJECTROOT, project))
	except (pygit2.GitError, OSError, ValueError):
		return []

	commit_events: list[TimelineEvent] = _collect_commit_events(repo)
	ref_events: list[TimelineEvent] = _collect_ref_events(repo)
	all_events: list[TimelineEvent] = commit_events + ref_events
	all_events.sort(key=lambda event: (event["timestamp"], event["oid"]), reverse=True)
	return all_events[:max_events]


def warm_timeline_cache() -> None:
	global PROJECT_TIMELINE_CACHE, _TIMELINE_WARMING
	with _TIMELINE_CACHE_LOCK:
		_TIMELINE_WARMING = True
	try:
		new_cache: dict[str, list[TimelineEvent]] = {}
		projects = git_get_projects_list(
			filter_path="",
			paranoid=settings.STRICT_EXPORT,
			export_ok=settings.EXPORT_OK,
		)
		project_names: list[str] = []
		project_event_pool: dict[str, list[TimelineEvent]] = {}
		for project_entry in projects:
			project_name = project_entry.get("path")
			if not isinstance(project_name, str) or not project_name:
				continue
			project_names.append(project_name)
			try:
				project_event_pool[project_name] = build_project_timeline_cache(
					project_name,
					max_events=TIMELINE_CACHE_TOTAL_EVENTS,
				)
			except (pygit2.GitError, OSError, ValueError, KeyError):
				project_event_pool[project_name] = []

		for project_name in project_names:
			new_cache[project_name] = []

		selected_events: list[TimelineProjectEvent] = []
		next_index_by_project: dict[str, int] = {project_name: 0 for project_name in project_names}

		for project_name in project_names:
			if len(selected_events) >= TIMELINE_CACHE_TOTAL_EVENTS:
				break
			project_events = project_event_pool.get(project_name, [])
			if not project_events:
				continue
			selected_events.append({"project": project_name, "event": project_events[0]})
			next_index_by_project[project_name] = 1

		while len(selected_events) < TIMELINE_CACHE_TOTAL_EVENTS:
			candidate_projects: list[str] = [
				project_name
				for project_name in project_names
				if next_index_by_project[project_name] < len(project_event_pool.get(project_name, []))
			]
			if not candidate_projects:
				break
			best_project: str = max(
				candidate_projects,
				key=lambda project_name: (
					project_event_pool[project_name][next_index_by_project[project_name]]["timestamp"],
					project_event_pool[project_name][next_index_by_project[project_name]]["oid"],
					project_name,
				),
			)
			start_index = next_index_by_project[best_project]
			remaining_global = TIMELINE_CACHE_TOTAL_EVENTS - len(selected_events)
			remaining_project = len(project_event_pool[best_project]) - start_index
			take_count = min(TIMELINE_CACHE_BATCH_SIZE, remaining_global, remaining_project)
			for offset in range(take_count):
				selected_events.append({
					"project": best_project,
					"event": project_event_pool[best_project][start_index + offset],
				})
			next_index_by_project[best_project] = start_index + take_count

		for item in selected_events:
			new_cache[item["project"]].append(item["event"])
		with _TIMELINE_CACHE_LOCK:
			PROJECT_TIMELINE_CACHE = new_cache
	finally:
		with _TIMELINE_CACHE_LOCK:
			_TIMELINE_WARMING = False


def warm_timeline_cache_async() -> bool:
	global _TIMELINE_WARMING
	with _TIMELINE_CACHE_LOCK:
		if _TIMELINE_WARMING:
			return False
		_TIMELINE_WARMING = True
	thread = threading.Thread(target=warm_timeline_cache, daemon=True)
	thread.start()
	return True


def get_all_timeline_events() -> list[TimelineProjectEvent]:
	events: list[TimelineProjectEvent] = []
	with _TIMELINE_CACHE_LOCK:
		cache_snapshot = {
			project_name: list(project_events) for project_name, project_events in PROJECT_TIMELINE_CACHE.items()
		}
	for project_name, project_events in cache_snapshot.items():
		for event in project_events:
			events.append({"project": project_name, "event": event})
	events.sort(
		key=lambda item: (item["event"]["timestamp"], item["event"]["oid"], item["project"]),
		reverse=True,
	)
	return events