package handlers import ( "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/breakpilot/edu-search-service/internal/database" "github.com/breakpilot/edu-search-service/internal/publications" "github.com/breakpilot/edu-search-service/internal/staff" ) // StaffHandlers handles staff-related API endpoints type StaffHandlers struct { repo *database.Repository crawler *staff.StaffCrawler pubCrawler *publications.PublicationCrawler } // NewStaffHandlers creates new staff handlers func NewStaffHandlers(repo *database.Repository, email string) *StaffHandlers { return &StaffHandlers{ repo: repo, crawler: staff.NewStaffCrawler(repo), pubCrawler: publications.NewPublicationCrawler(repo, email), } } // SearchStaff searches for university staff // GET /api/v1/staff/search?q=...&university_id=...&state=...&position_type=...&is_professor=... func (h *StaffHandlers) SearchStaff(c *gin.Context) { params := database.StaffSearchParams{ Query: c.Query("q"), Limit: parseIntDefault(c.Query("limit"), 20), Offset: parseIntDefault(c.Query("offset"), 0), } // Optional filters if uniID := c.Query("university_id"); uniID != "" { id, err := uuid.Parse(uniID) if err == nil { params.UniversityID = &id } } if deptID := c.Query("department_id"); deptID != "" { id, err := uuid.Parse(deptID) if err == nil { params.DepartmentID = &id } } if state := c.Query("state"); state != "" { params.State = &state } if uniType := c.Query("uni_type"); uniType != "" { params.UniType = &uniType } if posType := c.Query("position_type"); posType != "" { params.PositionType = &posType } if isProfStr := c.Query("is_professor"); isProfStr != "" { isProf := isProfStr == "true" || isProfStr == "1" params.IsProfessor = &isProf } result, err := h.repo.SearchStaff(c.Request.Context(), params) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) } // GetStaff gets a single staff member by ID // GET /api/v1/staff/:id func (h *StaffHandlers) GetStaff(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid staff ID"}) return } staff, err := h.repo.GetStaff(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Staff not found"}) return } c.JSON(http.StatusOK, staff) } // GetStaffPublications gets publications for a staff member // GET /api/v1/staff/:id/publications func (h *StaffHandlers) GetStaffPublications(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid staff ID"}) return } pubs, err := h.repo.GetStaffPublications(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "publications": pubs, "total": len(pubs), "staff_id": id, }) } // SearchPublications searches for publications // GET /api/v1/publications/search?q=...&year=...&pub_type=... func (h *StaffHandlers) SearchPublications(c *gin.Context) { params := database.PublicationSearchParams{ Query: c.Query("q"), Limit: parseIntDefault(c.Query("limit"), 20), Offset: parseIntDefault(c.Query("offset"), 0), } if staffID := c.Query("staff_id"); staffID != "" { id, err := uuid.Parse(staffID) if err == nil { params.StaffID = &id } } if year := c.Query("year"); year != "" { y := parseIntDefault(year, 0) if y > 0 { params.Year = &y } } if yearFrom := c.Query("year_from"); yearFrom != "" { y := parseIntDefault(yearFrom, 0) if y > 0 { params.YearFrom = &y } } if yearTo := c.Query("year_to"); yearTo != "" { y := parseIntDefault(yearTo, 0) if y > 0 { params.YearTo = &y } } if pubType := c.Query("pub_type"); pubType != "" { params.PubType = &pubType } result, err := h.repo.SearchPublications(c.Request.Context(), params) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) } // GetStaffStats gets statistics about staff data // GET /api/v1/staff/stats func (h *StaffHandlers) GetStaffStats(c *gin.Context) { stats, err := h.repo.GetStaffStats(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, stats) } // ListUniversities lists all universities // GET /api/v1/universities func (h *StaffHandlers) ListUniversities(c *gin.Context) { universities, err := h.repo.ListUniversities(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "universities": universities, "total": len(universities), }) } // StartStaffCrawl starts a staff crawl for a university // POST /api/v1/admin/crawl/staff func (h *StaffHandlers) StartStaffCrawl(c *gin.Context) { var req struct { UniversityID string `json:"university_id"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) return } uniID, err := uuid.Parse(req.UniversityID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid university ID"}) return } uni, err := h.repo.GetUniversity(c.Request.Context(), uniID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "University not found"}) return } // Start crawl in background go func() { result, err := h.crawler.CrawlUniversity(c.Request.Context(), uni) if err != nil { // Log error return } _ = result }() c.JSON(http.StatusAccepted, gin.H{ "status": "started", "university_id": uniID, "message": "Staff crawl started in background", }) } // StartPublicationCrawl starts a publication crawl for a university // POST /api/v1/admin/crawl/publications func (h *StaffHandlers) StartPublicationCrawl(c *gin.Context) { var req struct { UniversityID string `json:"university_id"` Limit int `json:"limit"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) return } uniID, err := uuid.Parse(req.UniversityID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid university ID"}) return } limit := req.Limit if limit <= 0 { limit = 50 } // Start crawl in background go func() { status, err := h.pubCrawler.CrawlForUniversity(c.Request.Context(), uniID, limit) if err != nil { // Log error return } _ = status }() c.JSON(http.StatusAccepted, gin.H{ "status": "started", "university_id": uniID, "message": "Publication crawl started in background", }) } // ResolveDOI resolves a DOI and saves the publication // POST /api/v1/publications/resolve-doi func (h *StaffHandlers) ResolveDOI(c *gin.Context) { var req struct { DOI string `json:"doi"` StaffID string `json:"staff_id,omitempty"` } if err := c.ShouldBindJSON(&req); err != nil || req.DOI == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "DOI is required"}) return } pub, err := h.pubCrawler.ResolveDOI(c.Request.Context(), req.DOI) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Link to staff if provided if req.StaffID != "" { staffID, err := uuid.Parse(req.StaffID) if err == nil { link := &database.StaffPublication{ StaffID: staffID, PublicationID: pub.ID, } h.repo.LinkStaffPublication(c.Request.Context(), link) } } c.JSON(http.StatusOK, pub) } // GetCrawlStatus gets crawl status for a university // GET /api/v1/admin/crawl/status/:university_id func (h *StaffHandlers) GetCrawlStatus(c *gin.Context) { idStr := c.Param("university_id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid university ID"}) return } status, err := h.repo.GetCrawlStatus(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if status == nil { c.JSON(http.StatusOK, gin.H{ "university_id": id, "staff_crawl_status": "never", "pub_crawl_status": "never", }) return } c.JSON(http.StatusOK, status) } // Helper to parse int with default func parseIntDefault(s string, def int) int { if s == "" { return def } var n int _, err := fmt.Sscanf(s, "%d", &n) if err != nil { return def } return n } // RegisterStaffRoutes registers staff-related routes func (h *StaffHandlers) RegisterRoutes(r *gin.RouterGroup) { // Public endpoints r.GET("/staff/search", h.SearchStaff) r.GET("/staff/stats", h.GetStaffStats) r.GET("/staff/:id", h.GetStaff) r.GET("/staff/:id/publications", h.GetStaffPublications) r.GET("/publications/search", h.SearchPublications) r.POST("/publications/resolve-doi", h.ResolveDOI) r.GET("/universities", h.ListUniversities) // Admin endpoints r.POST("/admin/crawl/staff", h.StartStaffCrawl) r.POST("/admin/crawl/publications", h.StartPublicationCrawl) r.GET("/admin/crawl/status/:university_id", h.GetCrawlStatus) }