"""API endpoints for Forgejo issue operations."""

from __future__ import annotations

from typing import TYPE_CHECKING
from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import String, cast
from sqlmodel import func, select

from app.api.deps import require_org_member
from app.core.auth import AuthContext, get_auth_context
from app.core.time import utcnow
from app.db import crud
from app.db.session import get_session
from app.models.agents import Agent
from app.models.board_repository_links import BoardRepositoryLink
from app.models.boards import Board
from app.models.forgejo_issue_task_links import ForgejoIssueTaskLink
from app.models.forgejo_issues import ForgejoIssue
from app.models.forgejo_repositories import ForgejoRepository
from app.models.tasks import Task
from app.schemas.forgejo_issues import (
    CloseIssueResponse,
    CreateIssueRequest,
    CreateIssueResponse,
    EditIssueRequest,
    EditIssueResponse,
    ForgejoIssueDetailRead,
    ForgejoIssueListResponse,
    ForgejoIssueRead,
    ForgejoIssueTaskRequest,
    ForgejoIssueTaskResponse,
    PostCommentRequest,
    PostCommentResponse,
)
from app.services.activity_log import record_activity
from app.services.forgejo_issue_create import (
    CreateIssueNotFoundError,
    CreateIssueRemoteError,
    create_issue_on_repository,
)
from app.services.forgejo_issue_close import (
    CloseIssueAccessError,
    CloseIssueNotFoundError,
    CloseIssueRemoteError,
    close_issue_by_id,
)
from app.services.forgejo_issue_comment import (
    PostCommentNotFoundError,
    PostCommentRemoteError,
    post_comment_by_issue_id,
)
from app.services.forgejo_issue_edit import (
    EditIssueNotFoundError,
    EditIssueRemoteError,
    edit_issue_by_id,
)
from app.services.organizations import OrganizationContext, list_accessible_board_ids

if TYPE_CHECKING:
    from sqlmodel.ext.asyncio.session import AsyncSession


router = APIRouter(prefix="/forgejo/issues", tags=["forgejo-issues"])
SESSION_DEP = Depends(get_session)
AUTH_DEP = Depends(get_auth_context)
ORG_MEMBER_DEP = Depends(require_org_member)


async def _linked_issue_board_ids(
    session: AsyncSession,
    *,
    issue: ForgejoIssue,
    organization_id: UUID,
) -> list[UUID]:
    rows = (
        await session.exec(
            select(BoardRepositoryLink.board_id).where(
                BoardRepositoryLink.organization_id == organization_id,
                BoardRepositoryLink.repository_id == issue.repository_id,
            )
        )
    ).all()
    return list(rows)


async def _resolve_issue_task_board(
    session: AsyncSession,
    *,
    issue: ForgejoIssue,
    requested_board_id: UUID | None,
    ctx: OrganizationContext,
) -> Board:
    linked_board_ids = await _linked_issue_board_ids(
        session,
        issue=issue,
        organization_id=ctx.organization.id,
    )
    if not linked_board_ids:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Issue repository is not linked to any board",
        )

    allowed_board_ids = set(await list_accessible_board_ids(session, member=ctx.member, write=True))
    authorized_board_ids = [
        board_id for board_id in linked_board_ids if board_id in allowed_board_ids
    ]
    if not authorized_board_ids:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Board access denied",
        )

    if requested_board_id is None:
        if len(authorized_board_ids) > 1:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
                detail="board_id is required because this issue is linked to multiple writable boards",
            )
        board_id = authorized_board_ids[0]
    else:
        if requested_board_id not in linked_board_ids:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail="Issue repository is not linked to the requested board",
            )
        if requested_board_id not in allowed_board_ids:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Board access denied",
            )
        board_id = requested_board_id

    board = await crud.get_by_id(session, Board, board_id)
    if board is None or board.organization_id != ctx.organization.id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Board not found")
    return board


async def _validate_issue_task_agent(
    session: AsyncSession,
    *,
    board: Board,
    assigned_agent_id: UUID | None,
) -> Agent | None:
    if assigned_agent_id is None:
        return None
    agent = await crud.get_by_id(session, Agent, assigned_agent_id)
    if agent is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found")
    if agent.board_id != board.id:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
            detail="Agent must belong to the selected board",
        )
    if board.gateway_id is not None and agent.gateway_id not in {None, board.gateway_id}:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
            detail="Agent gateway does not match the selected board",
        )
    return agent


async def _issue_repository_name(
    session: AsyncSession,
    *,
    issue: ForgejoIssue,
) -> str:
    repository = await crud.get_by_id(session, ForgejoRepository, issue.repository_id)
    if repository is None:
        return str(issue.repository_id)
    return repository.display_name or f"{repository.owner}/{repository.repo}"


def _task_status_for_issue_task(
    payload: ForgejoIssueTaskRequest,
    *,
    assigned_agent_id: UUID | None,
) -> str:
    if payload.status is not None:
        return payload.status
    if assigned_agent_id is not None and payload.start_immediately:
        return "in_progress"
    return "inbox"


def _issue_task_title(issue: ForgejoIssue) -> str:
    return f"Git issue #{issue.forgejo_issue_number}: {issue.title}"


def _label_names(issue: ForgejoIssue) -> str:
    names = [
        str(label.get("name"))
        for label in issue.labels
        if isinstance(label, dict) and label.get("name")
    ]
    return ", ".join(names)


def _issue_task_description(
    *,
    issue: ForgejoIssue,
    repository_name: str,
    instructions: str | None,
) -> str:
    parts = [
        "Source: Forgejo issue",
        f"Repository: {repository_name}",
        f"Issue: #{issue.forgejo_issue_number}",
        f"URL: {issue.html_url}",
        f"State: {issue.state}",
    ]
    labels = _label_names(issue)
    if labels:
        parts.append(f"Labels: {labels}")
    if issue.author:
        parts.append(f"Author: {issue.author}")
    if instructions and instructions.strip():
        parts.append(f"\nInstructions:\n{instructions.strip()}")
    if issue.body:
        parts.append(f"\nIssue body:\n{issue.body.strip()}")
    elif issue.body_preview:
        parts.append(f"\nIssue preview:\n{issue.body_preview.strip()}")
    return "\n".join(parts)


async def _existing_issue_task(
    session: AsyncSession,
    *,
    issue_id: UUID,
    board_id: UUID,
) -> tuple[ForgejoIssueTaskLink, Task] | None:
    link = (
        await session.exec(
            select(ForgejoIssueTaskLink).where(
                ForgejoIssueTaskLink.issue_id == issue_id,
                ForgejoIssueTaskLink.board_id == board_id,
            )
        )
    ).first()
    if link is None:
        return None
    task = await crud.get_by_id(session, Task, link.task_id)
    if task is None:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail="Issue task link points to a missing task",
        )
    return link, task


async def _notify_issue_task_assignee(
    *,
    session: AsyncSession,
    board: Board,
    task: Task,
    agent: Agent | None,
) -> None:
    if agent is None:
        return
    from app.api.tasks import notify_agent_on_task_assign

    await notify_agent_on_task_assign(
        session=session,
        board=board,
        task=task,
        agent=agent,
    )


def _normalize_issue_task_priority(priority: str) -> str:
    normalized = priority.strip().lower()
    return normalized or "medium"


def _set_issue_task_status_fields(task: Task, status_value: str) -> None:
    previous_status = task.status
    task.status = status_value
    if status_value == "in_progress":
        if task.in_progress_at is None or previous_status != "in_progress":
            task.in_progress_at = utcnow()
        return
    if status_value == "inbox":
        task.in_progress_at = None


def _issue_task_response(
    *,
    created: bool,
    issue: ForgejoIssue,
    task: Task,
    board_id: UUID,
) -> ForgejoIssueTaskResponse:
    return ForgejoIssueTaskResponse(
        success=True,
        created=created,
        issue_id=issue.id,
        task_id=task.id,
        board_id=board_id,
        assigned_agent_id=task.assigned_agent_id,
        status=task.status,
        title=task.title,
    )


@router.get("", response_model=ForgejoIssueListResponse)
async def list_issues(
    session: AsyncSession = SESSION_DEP,
    ctx: OrganizationContext = ORG_MEMBER_DEP,
    repository_id: str | None = Query(None, description="Filter by repository ID"),
    board_id: str | None = Query(
        None, description="Filter by board ID (returns issues from all repos linked to this board)"
    ),
    state: str | None = Query(None, description="Filter by state (open, closed)"),
    label: str | None = Query(None, description="Filter by label name"),
    assignee: str | None = Query(None, description="Filter by assignee login"),
    search: str | None = Query(None, description="Search in title and body"),
    page: int = Query(1, ge=1, description="Page number"),
    limit: int = Query(30, ge=1, le=100, description="Items per page"),
) -> ForgejoIssueListResponse:
    """List cached issues with optional filters."""
    # Build query with filters
    statement = select(ForgejoIssue).where(
        ForgejoIssue.organization_id == ctx.organization.id,
        ForgejoIssue.is_pull_request.is_(False),
    )

    if board_id:
        try:
            board_uuid = UUID(board_id)
        except ValueError:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Invalid board_id format"
            )
        linked_repo_ids = (
            await session.exec(
                select(BoardRepositoryLink.repository_id).where(
                    BoardRepositoryLink.board_id == board_uuid,
                    BoardRepositoryLink.organization_id == ctx.organization.id,
                )
            )
        ).all()
        if not linked_repo_ids:
            return ForgejoIssueListResponse(items=[], total=0, page=page, limit=limit)
        statement = statement.where(ForgejoIssue.repository_id.in_(linked_repo_ids))

    if repository_id:
        try:
            repo_uuid = UUID(repository_id)
        except ValueError:
            raise HTTPException(
                status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
                detail="Invalid repository_id format",
            )
        statement = statement.where(ForgejoIssue.repository_id == repo_uuid)

    if state:
        statement = statement.where(ForgejoIssue.state == state)

    if label:
        # Filter by label name — search within the JSON labels array cast to text
        statement = statement.where(cast(ForgejoIssue.labels, String).ilike(f"%{label}%"))

    if assignee:
        # Filter by assignee login — search within the JSON assignees array cast to text
        statement = statement.where(cast(ForgejoIssue.assignees, String).ilike(f"%{assignee}%"))

    if search:
        statement = statement.where(
            (ForgejoIssue.title.ilike(f"%{search}%"))
            | (ForgejoIssue.body_preview.ilike(f"%{search}%"))
            | (ForgejoIssue.body.ilike(f"%{search}%"))
        )

    # Count total
    total_statement = (
        select(func.count())
        .select_from(ForgejoIssue)
        .where(
            ForgejoIssue.organization_id == ctx.organization.id,
            ForgejoIssue.is_pull_request.is_(False),
        )
    )
    if board_id:
        try:
            board_uuid = UUID(board_id)
            linked_repo_ids_for_count = (
                await session.exec(
                    select(BoardRepositoryLink.repository_id).where(
                        BoardRepositoryLink.board_id == board_uuid,
                        BoardRepositoryLink.organization_id == ctx.organization.id,
                    )
                )
            ).all()
            if linked_repo_ids_for_count:
                total_statement = total_statement.where(
                    ForgejoIssue.repository_id.in_(linked_repo_ids_for_count)
                )
        except ValueError:
            pass
    if repository_id:
        try:
            repo_uuid = UUID(repository_id)
            total_statement = total_statement.where(ForgejoIssue.repository_id == repo_uuid)
        except ValueError:
            pass
    if state:
        total_statement = total_statement.where(ForgejoIssue.state == state)
    if label:
        total_statement = total_statement.where(
            cast(ForgejoIssue.labels, String).ilike(f"%{label}%")
        )
    if assignee:
        total_statement = total_statement.where(
            cast(ForgejoIssue.assignees, String).ilike(f"%{assignee}%")
        )
    if search:
        total_statement = total_statement.where(
            (ForgejoIssue.title.ilike(f"%{search}%"))
            | (ForgejoIssue.body_preview.ilike(f"%{search}%"))
            | (ForgejoIssue.body.ilike(f"%{search}%"))
        )
    total_result = await session.exec(total_statement)
    total = total_result.one()

    # Pagination
    offset = (page - 1) * limit
    statement = (
        statement.offset(offset).limit(limit).order_by(ForgejoIssue.forgejo_issue_number.desc())
    )

    issues = (await session.exec(statement)).all()
    items = [ForgejoIssueRead.model_validate(issue) for issue in issues]

    return ForgejoIssueListResponse(
        items=items,
        total=total,
        page=page,
        limit=limit,
    )


@router.get("/{issue_id}", response_model=ForgejoIssueDetailRead)
async def get_issue(
    issue_id: str,
    session: AsyncSession = SESSION_DEP,
    ctx: OrganizationContext = ORG_MEMBER_DEP,
) -> ForgejoIssueDetailRead:
    """Get one cached issue by ID."""
    try:
        uuid = UUID(issue_id)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Invalid issue_id format"
        )

    issue = await crud.get_by_id(session, ForgejoIssue, uuid)
    if issue is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
    if issue.organization_id != ctx.organization.id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)

    return ForgejoIssueDetailRead.model_validate(issue)


@router.post(
    "/{issue_id}/task",
    response_model=ForgejoIssueTaskResponse,
    summary="Create or update a Pipeline task for a Forgejo issue",
    responses={
        status.HTTP_200_OK: {"description": "Task created or existing task updated"},
        status.HTTP_401_UNAUTHORIZED: {"description": "User authentication required"},
        status.HTTP_403_FORBIDDEN: {"description": "User lacks board write access"},
        status.HTTP_404_NOT_FOUND: {"description": "Issue, board, or agent not found"},
        status.HTTP_409_CONFLICT: {"description": "Existing link points to a missing task"},
        status.HTTP_422_UNPROCESSABLE_CONTENT: {
            "description": "Invalid issue, board, agent, or tasking request"
        },
    },
)
async def task_issue(
    issue_id: str,
    payload: ForgejoIssueTaskRequest | None = None,
    session: AsyncSession = SESSION_DEP,
    auth: AuthContext = AUTH_DEP,
    ctx: OrganizationContext = ORG_MEMBER_DEP,
) -> ForgejoIssueTaskResponse:
    """Create or update a board task from a cached Forgejo issue."""
    payload = payload or ForgejoIssueTaskRequest()
    try:
        uuid = UUID(issue_id)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
            detail="Invalid issue_id format",
        )

    issue = await crud.get_by_id(session, ForgejoIssue, uuid)
    if issue is None or issue.organization_id != ctx.organization.id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Issue not found")
    if issue.is_pull_request:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
            detail="Pull requests cannot be tasked through the issue tasking endpoint",
        )
    if auth.user is None:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)

    board = await _resolve_issue_task_board(
        session,
        issue=issue,
        requested_board_id=payload.board_id,
        ctx=ctx,
    )
    agent_requested = "assigned_agent_id" in payload.model_fields_set
    status_requested = "status" in payload.model_fields_set
    instructions_requested = "instructions" in payload.model_fields_set
    priority_requested = "priority" in payload.model_fields_set
    agent = await _validate_issue_task_agent(
        session,
        board=board,
        assigned_agent_id=payload.assigned_agent_id if agent_requested else None,
    )

    existing = await _existing_issue_task(session, issue_id=issue.id, board_id=board.id)
    if existing is not None:
        _link, task = existing
        previous_assignee_id = task.assigned_agent_id
        previous_status = task.status
        if agent_requested:
            task.assigned_agent_id = agent.id if agent is not None else None
        if status_requested:
            _set_issue_task_status_fields(task, payload.status or "inbox")
        elif agent_requested and agent is not None and payload.start_immediately:
            _set_issue_task_status_fields(task, "in_progress")
        elif agent_requested and agent is None:
            _set_issue_task_status_fields(task, "inbox")
        if priority_requested:
            task.priority = _normalize_issue_task_priority(payload.priority)
        if instructions_requested:
            repository_name = await _issue_repository_name(session, issue=issue)
            task.description = _issue_task_description(
                issue=issue,
                repository_name=repository_name,
                instructions=payload.instructions,
            )
        task.updated_at = utcnow()
        session.add(task)
        record_activity(
            session,
            event_type="forgejo.issue.task.updated",
            task_id=task.id,
            board_id=board.id,
            message=f"Forgejo issue task updated: #{issue.forgejo_issue_number}.",
        )
        await session.commit()
        await session.refresh(task)
        notify_agent = agent
        if notify_agent is None and task.assigned_agent_id and task.status == "in_progress":
            current_agent = await crud.get_by_id(session, Agent, task.assigned_agent_id)
            if current_agent is not None and current_agent.board_id == board.id:
                notify_agent = current_agent
        should_notify = (
            notify_agent is not None
            and task.assigned_agent_id is not None
            and (
                task.assigned_agent_id != previous_assignee_id
                or (previous_status != "in_progress" and task.status == "in_progress")
            )
        )
        if notify_agent is not None and should_notify:
            await _notify_issue_task_assignee(
                session=session,
                board=board,
                task=task,
                agent=notify_agent,
            )
        return _issue_task_response(created=False, issue=issue, task=task, board_id=board.id)

    status_value = _task_status_for_issue_task(
        payload,
        assigned_agent_id=agent.id if agent is not None else None,
    )
    repository_name = await _issue_repository_name(session, issue=issue)
    task = Task(
        board_id=board.id,
        title=_issue_task_title(issue),
        description=_issue_task_description(
            issue=issue,
            repository_name=repository_name,
            instructions=payload.instructions,
        ),
        status=status_value,
        priority=_normalize_issue_task_priority(payload.priority),
        assigned_agent_id=agent.id if agent is not None else None,
        created_by_user_id=auth.user.id,
        auto_created=True,
        auto_reason=f"forgejo_issue:{issue.id}",
        in_progress_at=utcnow() if status_value == "in_progress" else None,
    )
    session.add(task)
    await session.flush()
    session.add(
        ForgejoIssueTaskLink(
            organization_id=ctx.organization.id,
            board_id=board.id,
            issue_id=issue.id,
            task_id=task.id,
            created_by_user_id=auth.user.id,
        )
    )
    record_activity(
        session,
        event_type="forgejo.issue.task.created",
        task_id=task.id,
        board_id=board.id,
        message=f"Forgejo issue task created: #{issue.forgejo_issue_number}.",
    )
    await session.commit()
    await session.refresh(task)
    await _notify_issue_task_assignee(
        session=session,
        board=board,
        task=task,
        agent=agent,
    )
    return _issue_task_response(created=True, issue=issue, task=task, board_id=board.id)


@router.post(
    "/{issue_id}/close",
    response_model=CloseIssueResponse,
    summary="Close a Forgejo issue (human user)",
    description=(
        "Close a Forgejo issue by its local ID. The user must have write access "
        "to the board that the issue's repository is linked to."
    ),
    responses={
        status.HTTP_200_OK: {
            "description": "Issue closed successfully",
            "content": {
                "application/json": {
                    "example": {
                        "success": True,
                        "issue_id": "123e4567-e89b-12d3-a456-426614174000",
                        "forgejo_issue_number": 42,
                        "state": "closed",
                        "forgejo_closed_at": "2026-05-19T03:43:00+00:00",
                        "last_synced_at": "2026-05-19T03:43:00+00:00",
                    }
                }
            },
        },
        status.HTTP_404_NOT_FOUND: {
            "description": "Issue not found or not linked to a board",
        },
        status.HTTP_403_FORBIDDEN: {
            "description": "User lacks write access to the board",
        },
        status.HTTP_409_CONFLICT: {
            "description": "Organization mismatch or access denied",
        },
        status.HTTP_502_BAD_GATEWAY: {
            "description": "Forgejo API call failed",
        },
    },
)
async def close_issue(
    issue_id: str,
    session: AsyncSession = SESSION_DEP,
    auth: AuthContext = AUTH_DEP,
    ctx: OrganizationContext = ORG_MEMBER_DEP,
) -> CloseIssueResponse:
    """Close a Forgejo issue as an authenticated user.

    The user must have write access to the board that the issue's repository
    is linked to. The issue must belong to a repository linked to that board.
    """
    try:
        uuid = UUID(issue_id)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Invalid issue_id format"
        )

    issue = await crud.get_by_id(session, ForgejoIssue, uuid)
    if issue is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Issue not found")
    if issue.organization_id != ctx.organization.id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Issue not found")
    if auth.user is None:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)

    # Check board links — used for authorization and activity logging.
    # If the repository is not linked to any board the user can still close
    # the issue (org membership is sufficient); we just skip the board-scoped
    # activity event.
    links = (
        await session.exec(
            select(BoardRepositoryLink).where(
                BoardRepositoryLink.organization_id == ctx.organization.id,
                BoardRepositoryLink.repository_id == issue.repository_id,
            )
        )
    ).all()

    authorized_board_id: object = None
    if links:
        allowed_board_ids = set(await list_accessible_board_ids(session, member=ctx.member, write=True))
        authorized_board_id = next(
            (link.board_id for link in links if link.board_id in allowed_board_ids),
            None,
        )
        if authorized_board_id is None:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail="Board access denied",
            )

    # Close the issue using the service.
    try:
        result = await close_issue_by_id(
            session=session,
            issue_id=uuid,
            actor_user_id=auth.user.id,
        )
    except CloseIssueNotFoundError as e:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
    except CloseIssueAccessError as e:
        raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
    except CloseIssueRemoteError as e:
        raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e))

    repository_full_name = str(result.get("repository_full_name") or "unknown/unknown")
    if authorized_board_id is not None:
        record_activity(
            session,
            event_type="forgejo.issue.closed",
            message=(
                "Forgejo issue closed by user "
                f"{auth.user.id}: {repository_full_name}#{result['forgejo_issue_number']}"
            ),
            board_id=authorized_board_id,
        )
    await session.commit()

    return CloseIssueResponse(
        success=result["success"],
        issue_id=result["issue_id"],
        forgejo_issue_number=result["forgejo_issue_number"],
        state=result["state"],
        forgejo_closed_at=result.get("forgejo_closed_at"),
        last_synced_at=result.get("last_synced_at") or "",
    )


@router.post(
    "/{issue_id}/comments",
    response_model=PostCommentResponse,
    summary="Post a comment on a Forgejo issue",
    responses={
        status.HTTP_404_NOT_FOUND: {"description": "Issue not found"},
        status.HTTP_502_BAD_GATEWAY: {"description": "Forgejo API call failed"},
    },
)
async def post_comment(
    issue_id: str,
    body: PostCommentRequest,
    session: AsyncSession = SESSION_DEP,
    auth: AuthContext = AUTH_DEP,
    ctx: OrganizationContext = ORG_MEMBER_DEP,
) -> PostCommentResponse:
    """Post a comment on a Forgejo issue as an authenticated user."""
    try:
        uuid = UUID(issue_id)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Invalid issue_id format"
        )

    issue = await crud.get_by_id(session, ForgejoIssue, uuid)
    if issue is None or issue.organization_id != ctx.organization.id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Issue not found")
    if auth.user is None:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)

    try:
        result = await post_comment_by_issue_id(
            session=session,
            issue_id=uuid,
            body=body.body,
            actor_user_id=auth.user.id,
        )
    except PostCommentNotFoundError as e:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
    except PostCommentRemoteError as e:
        raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e))

    await session.commit()
    return PostCommentResponse(
        success=True,
        issue_id=uuid,
        comment_id=result.get("comment_id"),
        body=body.body,
        created_at=str(result.get("created_at") or ""),
    )


@router.patch(
    "/{issue_id}",
    response_model=EditIssueResponse,
    summary="Edit a Forgejo issue",
    responses={
        status.HTTP_404_NOT_FOUND: {"description": "Issue not found"},
        status.HTTP_502_BAD_GATEWAY: {"description": "Forgejo API call failed"},
    },
)
async def edit_issue(
    issue_id: str,
    body: EditIssueRequest,
    session: AsyncSession = SESSION_DEP,
    auth: AuthContext = AUTH_DEP,
    ctx: OrganizationContext = ORG_MEMBER_DEP,
) -> EditIssueResponse:
    """Edit a Forgejo issue's title, body, and/or state."""
    try:
        uuid = UUID(issue_id)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="Invalid issue_id format"
        )

    issue = await crud.get_by_id(session, ForgejoIssue, uuid)
    if issue is None or issue.organization_id != ctx.organization.id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Issue not found")
    if auth.user is None:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)

    if body.title is None and body.body is None and body.state is None:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
            detail="At least one field must be provided",
        )

    try:
        result = await edit_issue_by_id(
            session=session,
            issue_id=uuid,
            title=body.title,
            body=body.body,
            state=body.state,
            actor_user_id=auth.user.id,
        )
    except EditIssueNotFoundError as e:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
    except EditIssueRemoteError as e:
        raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e))

    record_activity(
        session,
        event_type="forgejo.issue.edited",
        message=f"Forgejo issue edited by user {auth.user.id}: #{result['forgejo_issue_number']}",
    )
    await session.commit()
    return EditIssueResponse(
        success=True,
        issue_id=uuid,
        forgejo_issue_number=int(result["forgejo_issue_number"]),
        title=str(result["title"]),
        body=result.get("body") if isinstance(result.get("body"), str) else None,
        state=str(result["state"]),
        forgejo_updated_at=str(result["forgejo_updated_at"]),
    )


@router.post(
    "",
    response_model=CreateIssueResponse,
    status_code=status.HTTP_201_CREATED,
    summary="Create a new Forgejo issue",
    responses={
        status.HTTP_404_NOT_FOUND: {"description": "Repository not found"},
        status.HTTP_502_BAD_GATEWAY: {"description": "Forgejo API call failed"},
    },
)
async def create_issue(
    body: CreateIssueRequest,
    session: AsyncSession = SESSION_DEP,
    auth: AuthContext = AUTH_DEP,
    ctx: OrganizationContext = ORG_MEMBER_DEP,
) -> CreateIssueResponse:
    """Create a new issue on a Forgejo repository and cache it locally."""
    if auth.user is None:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)

    repository = await crud.get_by_id(session, ForgejoRepository, body.repository_id)
    if repository is None or repository.organization_id != ctx.organization.id:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Repository not found")

    try:
        issue = await create_issue_on_repository(
            session,
            repository=repository,
            title=body.title,
            body=body.body,
        )
    except CreateIssueNotFoundError as e:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
    except CreateIssueRemoteError as e:
        raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(e))

    record_activity(
        session,
        event_type="forgejo.issue.created",
        message=f"Issue created by user {auth.user.id}: {body.title}",
    )
    await session.commit()

    return CreateIssueResponse(
        success=True,
        issue_id=issue.id,
        forgejo_issue_number=issue.forgejo_issue_number,
        html_url=issue.html_url,
        repository_id=issue.repository_id,
    )
