"""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
    ]