"""Read-only queries over Board, Task, and Comment objects stored in a Git repo."""

from __future__ import annotations

import os
from dataclasses import dataclass
from pathlib import Path
from typing import TypedDict

from pygit2 import Oid, Repository

from pygittools.tasks import (
	BOARD_REF_PREFIX,
	TASK_REF_PREFIX,
	Comment,
	Task,
	get_board,
	get_comment,
	get_task,
	get_task_by_oid,
)

BOARD_STATUS_ORDER: tuple[str, ...] = ("TODO", "IN_PROGRESS", "IN_REVIEW", "DONE", "CANCELLED")


class CommentView(TypedDict):
	content: str
	author: str
	created_at: str | None
	edited_at: str | None


class TaskView(TypedDict, total=False):
	ref: str
	title: str
	description: str
	status: str | None
	priority: str | None
	assignee: str | None
	due_date: str | None
	created_at: str | None
	updated_at: str | None
	comments_count: int
	comments: list[CommentView]


class BoardView(TypedDict):
	name: str
	description: str
	task_count: int


class BoardColumnView(TypedDict):
	status: str
	label: str
	tasks: list[TaskView]


@dataclass(frozen=True, slots=True)
class RepoLocation:
	"""Where to open a repository for task queries."""

	path: Path
	project: str | None = None


def board_ref(name: str) -> str:
	return f"{BOARD_REF_PREFIX}{name}"


def task_ref(task_id: str) -> str:
	if task_id.startswith("refs/"):
		return task_id
	return f"{TASK_REF_PREFIX}{task_id}"


def discover_repo_path(start: Path | None = None) -> Path:
	"""Find a git repository by walking up from ``start`` (default: cwd)."""
	current = (start or Path.cwd()).resolve()
	for candidate in (current, *current.parents):
		if (candidate / ".git").exists():
			return candidate
	raise FileNotFoundError(f"No git repository found from {current}")


def resolve_repo_location(
	*,
	repo_path: str | Path | None = None,
	project: str | None = None,
) -> RepoLocation:
	"""Resolve a repository path from explicit args or environment."""
	if repo_path is not None:
		return RepoLocation(path=Path(repo_path).resolve(), project=project)

	env_repo = os.environ.get("PYGITTOOLS_REPO", "").strip()
	if env_repo and project is None:
		return RepoLocation(path=Path(env_repo).resolve())

	project_root = os.environ.get("PYGITWEB_PROJECTROOT", "").strip()
	if project:
		if not project_root:
			raise ValueError("project requires PYGITWEB_PROJECTROOT")
		return RepoLocation(path=(Path(project_root) / project).resolve(), project=project)

	if env_repo:
		return RepoLocation(path=Path(env_repo).resolve())

	return RepoLocation(path=discover_repo_path())


def open_repository(location: RepoLocation | None = None) -> Repository:
	loc = location or resolve_repo_location()
	return Repository(str(loc.path))


def _comment_to_view(comment: Comment) -> CommentView:
	return {
		"content": comment.content,
		"author": comment.tagger,
		"created_at": comment.created_at.isoformat() if comment.created_at else None,
		"edited_at": comment.edited_at.isoformat() if comment.edited_at else None,
	}


def _load_task_comments(repo: Repository, task: Task) -> list[CommentView]:
	comments: list[CommentView] = []
	for oid_hex in getattr(task, "comments", []) or []:
		try:
			oid = Oid(hex=oid_hex) if isinstance(oid_hex, str) else oid_hex
			comment = get_comment(repo, oid)
		except (ValueError, KeyError, TypeError):
			continue
		comments.append(_comment_to_view(comment))
	comments.sort(key=lambda entry: entry.get("edited_at") or entry.get("created_at") or "")
	return comments


def _task_to_view(task: Task, *, comments: list[CommentView] | None = None) -> TaskView:
	view: TaskView = {
		"ref": task.name,
		"title": task.title,
		"description": getattr(task, "description", "") or "",
		"status": task.status.value if task.status else None,
		"priority": task.priority.value if task.priority else None,
		"assignee": task.assignee,
		"due_date": task.due_date.isoformat() if task.due_date else None,
		"created_at": task.created_at.isoformat() if task.created_at else None,
		"updated_at": task.updated_at.isoformat() if task.updated_at else None,
	}
	if comments is None:
		view["comments_count"] = len(getattr(task, "comments", []) or [])
	else:
		view["comments"] = comments
	return view


def resolve_task_from_board_entry(repo: Repository, oid_hex: str | Oid) -> Task | None:
	"""Load a task listed on a board, following the ref when the board OID is stale."""
	oid = Oid(hex=oid_hex) if isinstance(oid_hex, str) else oid_hex
	task = get_task_by_oid(repo, oid)
	if not task or not task.name:
		return task
	try:
		if task.name not in repo.references:
			return task
		current_oid = repo.references[task.name].resolve().target
	except KeyError:
		return task
	if str(current_oid) == str(oid):
		return task
	return get_task(repo, task.name) or task


def list_boards(repo: Repository) -> list[BoardView]:
	boards: list[BoardView] = []
	for ref_name in repo.references:
		if not ref_name.startswith(BOARD_REF_PREFIX):
			continue
		try:
			board = get_board(repo, ref_name)
		except (ValueError, KeyError):
			continue
		if board is None:
			continue
		boards.append({
			"name": ref_name.removeprefix(BOARD_REF_PREFIX),
			"description": getattr(board, "description", "") or "",
			"task_count": len(getattr(board, "tasks", []) or []),
		})
	boards.sort(key=lambda entry: entry["name"].lower())
	return boards


def list_tasks(
	repo: Repository,
	board_name: str,
	*,
	include_comments: bool = False,
) -> list[TaskView]:
	ref = board_ref(board_name)
	board = get_board(repo, ref)
	if board is None:
		raise KeyError(f"Board not found: {board_name}")

	tasks: list[TaskView] = []
	for oid_hex in getattr(board, "tasks", []) or []:
		try:
			task = resolve_task_from_board_entry(repo, oid_hex)
		except (ValueError, KeyError, TypeError):
			continue
		if task is None:
			continue
		comments = _load_task_comments(repo, task) if include_comments else None
		tasks.append(_task_to_view(task, comments=comments))
	return tasks


def get_task_view(
	repo: Repository,
	task_id: str,
	*,
	include_comments: bool = True,
) -> TaskView:
	ref = task_ref(task_id)
	task = get_task(repo, ref)
	if task is None:
		raise KeyError(f"Task not found: {task_id}")
	comments = _load_task_comments(repo, task) if include_comments else None
	return _task_to_view(task, comments=comments)


def get_board_tasks_grouped(
	repo: Repository,
	board_name: str,
	*,
	include_comments: bool = False,
) -> list[BoardColumnView]:
	by_status: dict[str, list[TaskView]] = {status: [] for status in BOARD_STATUS_ORDER}
	for task_view in list_tasks(repo, board_name, include_comments=include_comments):
		status_key = task_view.get("status") or "TODO"
		if status_key not in by_status:
			by_status[status_key] = []
		by_status[status_key].append(task_view)
	return [
		{
			"status": status,
			"label": status.replace("_", " ").title(),
			"tasks": by_status.get(status, []),
		}
		for status in BOARD_STATUS_ORDER
	]