"""
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 clear_timeline_cache_async() -> bool:
    global PROJECT_TIMELINE_CACHE
    with _TIMELINE_CACHE_LOCK:
        PROJECT_TIMELINE_CACHE = {}
    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