import json from typing import cast from fastapi import APIRouter, Depends, Request, UploadFile from fastapi.responses import FileResponse from sqlalchemy.orm import Session from starlette.datastructures import UploadFile as StarletteUploadFile from app.db.session import get_db from app.schemas.ticket import ( SupportTicketCommentRecord, SupportTicketRecord, TicketCommentCreateRequest, TicketIssueLinkStatusSyncRequest, TicketCommentUpdateRequest, TicketCreateRequest, TicketCreateResponse, TicketMutationResponse, WorkspaceFormTemplateResponse, WorkspaceSummary, ) from app.services.ticket_service import TicketService router = APIRouter(prefix="/workspaces", tags=["tickets"]) service = TicketService() @router.get("", response_model=list[WorkspaceSummary]) def list_workspaces(db: Session = Depends(get_db)) -> list[WorkspaceSummary]: return service.list_workspaces(db) @router.get("/{workspace_code}/form-template", response_model=WorkspaceFormTemplateResponse) def get_workspace_form_template( workspace_code: str, db: Session = Depends(get_db), ) -> WorkspaceFormTemplateResponse: return service.get_workspace_form_template(db, workspace_code) @router.post("/{workspace_code}/tickets", response_model=TicketCreateResponse) async def create_ticket( workspace_code: str, request: Request, db: Session = Depends(get_db), ) -> TicketCreateResponse: content_type = request.headers.get("content-type", "") if content_type.startswith("multipart/form-data"): form = await request.form() extra_fields_value = form.get("extra_fields") extra_fields = ( json.loads(extra_fields_value) if isinstance(extra_fields_value, str) and extra_fields_value else {} ) attachments = [ cast(UploadFile, value) for key, value in form.multi_items() if key == "attachments" and isinstance(value, (UploadFile, StarletteUploadFile)) ] request_payload = TicketCreateRequest( workspace_code=workspace_code, requester_id=str(form.get("requester_id") or ""), requester_tenant_id=str(form.get("requester_tenant_id") or ""), requester_contact=str(form.get("requester_contact") or ""), title=str(form.get("title") or ""), description=str(form.get("description") or ""), category_code=str(form.get("category_code") or ""), ticket_type=str(form.get("ticket_type") or "GENERAL"), requires_approval=str(form.get("requires_approval") or "false").lower() == "true", extra_fields=extra_fields, ) return service.create_ticket(db, request_payload, attachments) payload = TicketCreateRequest.model_validate(await request.json()) request_payload = payload.model_copy(update={"workspace_code": workspace_code}) return service.create_ticket(db, request_payload) @router.get("/{workspace_code}/tickets", response_model=list[SupportTicketRecord]) def list_workspace_tickets( workspace_code: str, requesterId: str | None = None, requesterTenantId: str | None = None, db: Session = Depends(get_db), ) -> list[SupportTicketRecord]: return service.list_workspace_tickets(db, workspace_code, requesterId, requesterTenantId) @router.get("/../tickets", include_in_schema=False) def _noop_redirect() -> None: return None ticket_router = APIRouter(prefix="/tickets", tags=["support-tickets"]) @ticket_router.get("", response_model=list[SupportTicketRecord]) def list_tickets(db: Session = Depends(get_db)) -> list[SupportTicketRecord]: return service.list_tickets(db) @ticket_router.get("/{ticket_id}", response_model=SupportTicketRecord) def get_ticket( ticket_id: int, workspaceCode: str | None = None, db: Session = Depends(get_db), ) -> SupportTicketRecord: return service.get_ticket(db, ticket_id, workspaceCode) @ticket_router.get("/{ticket_id}/attachments/{attachment_id}") def download_ticket_attachment( ticket_id: int, attachment_id: int, workspaceCode: str | None = None, db: Session = Depends(get_db), ) -> FileResponse: attachment = service.get_ticket_attachment(db, ticket_id, attachment_id, workspaceCode) file_path = service.resolve_attachment_path(attachment) return FileResponse( path=file_path, media_type=attachment.mime_type or "application/octet-stream", filename=attachment.original_file_name, ) @ticket_router.get("/{ticket_id}/comments", response_model=list[SupportTicketCommentRecord]) def list_ticket_comments( ticket_id: int, workspaceCode: str | None = None, db: Session = Depends(get_db), ) -> list[SupportTicketCommentRecord]: return service.list_ticket_comments(db, ticket_id, workspaceCode) @ticket_router.post("/{ticket_id}/comments", response_model=SupportTicketCommentRecord) def create_ticket_comment( ticket_id: int, payload: TicketCommentCreateRequest, workspaceCode: str | None = None, db: Session = Depends(get_db), ) -> SupportTicketCommentRecord: return service.create_ticket_comment(db, ticket_id, payload, workspaceCode) @ticket_router.put("/{ticket_id}/comments/{comment_id}", response_model=SupportTicketCommentRecord) def update_ticket_comment( ticket_id: int, comment_id: int, payload: TicketCommentUpdateRequest, db: Session = Depends(get_db), ) -> SupportTicketCommentRecord: return service.update_ticket_comment(db, ticket_id, comment_id, payload) @ticket_router.delete("/{ticket_id}/comments/{comment_id}") def delete_ticket_comment( ticket_id: int, comment_id: int, requesterId: str, requesterTenantId: str, db: Session = Depends(get_db), ) -> dict[str, int | bool]: return service.delete_ticket_comment(db, ticket_id, comment_id, requesterId, requesterTenantId) @ticket_router.post("/{ticket_id}/approve", response_model=TicketMutationResponse) def approve_ticket(ticket_id: int, db: Session = Depends(get_db)) -> TicketMutationResponse: return service.approve_ticket(db, ticket_id) @ticket_router.post("/{ticket_id}/issue", response_model=TicketMutationResponse) def create_issue(ticket_id: int, db: Session = Depends(get_db)) -> TicketMutationResponse: return service.create_issue(db, ticket_id) @ticket_router.post("/{ticket_id}/issue-link-status") def sync_issue_link_status( ticket_id: int, payload: TicketIssueLinkStatusSyncRequest, workspaceCode: str | None = None, db: Session = Depends(get_db), ) -> dict[str, int | str | bool]: return service.sync_issue_link_status(db, ticket_id, payload.linked, workspaceCode)