package handlers import ( "errors" "net/http" "github.com/go-chi/chi/v5" "github.com/orchard9/rdev/internal/auth" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/logging" "github.com/orchard9/rdev/internal/service" "github.com/orchard9/rdev/internal/validate" "github.com/orchard9/rdev/pkg/api" ) // ArchitectHandler handles architect orchestration endpoints. type ArchitectHandler struct { architectService *service.ArchitectService } // NewArchitectHandler creates a new architect handler. func NewArchitectHandler(architectService *service.ArchitectService) *ArchitectHandler { return &ArchitectHandler{ architectService: architectService, } } // Mount registers architect routes. func (h *ArchitectHandler) Mount(r api.Router) { r.Route("/projects/{id}/architect", func(r chi.Router) { // All architect operations require execute scope r.With(auth.RequireScope(auth.ScopeProjectsExecute, auth.ScopeAdmin)). Post("/start", h.StartConversation) r.With(auth.RequireScope(auth.ScopeProjectsExecute, auth.ScopeAdmin)). Post("/continue/{conversationId}", h.ContinueConversation) r.With(auth.RequireScope(auth.ScopeProjectsExecute, auth.ScopeAdmin)). Post("/generate-blueprint/{conversationId}", h.GenerateBlueprint) }) } // StartConversationRequest is the request body for POST /projects/{id}/architect/start. type StartConversationRequest struct { Prompt string `json:"prompt"` } // StartConversation begins a new architectural conversation. // POST /projects/{id}/architect/start func (h *ArchitectHandler) StartConversation(w http.ResponseWriter, r *http.Request) { projectID := chi.URLParam(r, "id") var req StartConversationRequest if err := api.DecodeJSON(r, &req); err != nil { api.WriteBadRequest(w, r, "invalid request body") return } v := validate.New() v.Required(req.Prompt, "prompt") if err := v.Error(); err != nil { api.WriteBadRequest(w, r, err.Error()) return } conv, err := h.architectService.StartConversation(r.Context(), projectID, req.Prompt) if err != nil { log := logging.FromContext(r.Context()).WithHandler("StartConversation") log.Error("failed to start conversation", logging.FieldError, err, logging.FieldProjectID, projectID) api.WriteInternalError(w, r, "failed to start conversation") return } api.WriteCreated(w, r, StartConversationResponse{ ConversationWithMessagesDTO: *toConversationWithMessagesDTO(conv), }) } // ContinueConversationRequest is the request body for POST /projects/{id}/architect/continue/{conversationId}. type ContinueConversationRequest struct { Message string `json:"message"` } // ContinueConversation continues an existing architectural conversation. // POST /projects/{id}/architect/continue/{conversationId} func (h *ArchitectHandler) ContinueConversation(w http.ResponseWriter, r *http.Request) { conversationID := domain.ConversationID(chi.URLParam(r, "conversationId")) var req ContinueConversationRequest if err := api.DecodeJSON(r, &req); err != nil { api.WriteBadRequest(w, r, "invalid request body") return } v := validate.New() v.Required(req.Message, "message") if err := v.Error(); err != nil { api.WriteBadRequest(w, r, err.Error()) return } msg, err := h.architectService.ContinueConversation(r.Context(), conversationID, req.Message) if err != nil { if errors.Is(err, domain.ErrConversationNotFound) { api.WriteNotFound(w, r, "conversation not found") return } log := logging.FromContext(r.Context()).WithHandler("ContinueConversation") log.Error("failed to continue conversation", logging.FieldError, err, "conversation_id", conversationID) api.WriteInternalError(w, r, "failed to continue conversation") return } api.WriteCreated(w, r, ContinueConversationResponse{ Message: toMessageDTO(msg), }) } // GenerateBlueprintRequest is the request body for POST /projects/{id}/architect/generate-blueprint/{conversationId}. type GenerateBlueprintRequest struct { Name string `json:"name"` } // GenerateBlueprint generates a structured blueprint from a conversation. // POST /projects/{id}/architect/generate-blueprint/{conversationId} func (h *ArchitectHandler) GenerateBlueprint(w http.ResponseWriter, r *http.Request) { conversationID := domain.ConversationID(chi.URLParam(r, "conversationId")) var req GenerateBlueprintRequest if err := api.DecodeJSON(r, &req); err != nil { api.WriteBadRequest(w, r, "invalid request body") return } v := validate.New() v.Required(req.Name, "name") if err := v.Error(); err != nil { api.WriteBadRequest(w, r, err.Error()) return } blueprint, err := h.architectService.GenerateBlueprint(r.Context(), conversationID, req.Name) if err != nil { if errors.Is(err, domain.ErrConversationNotFound) { api.WriteNotFound(w, r, "conversation not found") return } log := logging.FromContext(r.Context()).WithHandler("GenerateBlueprint") log.Error("failed to generate blueprint", logging.FieldError, err, "conversation_id", conversationID) api.WriteInternalError(w, r, "failed to generate blueprint") return } api.WriteCreated(w, r, GenerateBlueprintResponse{ Blueprint: toBlueprintDTO(blueprint), }) }