package api

import (
	"errors"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/aisupport/services/oncall/db"
	"github.com/tidbcloud/aisupport/services/oncall/models"
	"github.com/tidbcloud/aisupport/services/oncall/service"
	"github.com/tidbcloud/aisupport/services/oncall/service/pager"

	"github.com/gin-gonic/gin"
	"gorm.io/gorm"
)

// teamRefToID resolves the {ref} path segment of a team route. A numeric ref is
// the team id; otherwise it is treated as the unique slug name (shareable URLs).
func teamRefToID(c *gin.Context, s *service.ScheduleService) (uint, bool) {
	raw := strings.TrimSpace(c.Param("ref"))
	if raw == "" {
		return 0, false
	}
	if n, err := strconv.ParseUint(raw, 10, 64); err == nil && n > 0 {
		return uint(n), true
	}
	id, err := s.FindTeamIDByName(raw)
	if err != nil || id == 0 {
		return 0, false
	}
	return id, true
}

// teamEngineerRefToID resolves the :eref path segment (numeric engineer id or
// email) to an existing engineer, writing the error response itself when it
// cannot. Unlike a team ref it never creates the engineer.
func teamEngineerRefToID(c *gin.Context, s *service.ScheduleService) (uint, bool) {
	raw := strings.TrimSpace(c.Param("eref"))
	if raw == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid engineer ref"})
		return 0, false
	}
	if n, err := strconv.ParseUint(raw, 10, 64); err == nil && n > 0 {
		return uint(n), true
	}
	id, err := s.FindEngineerID(0, raw)
	if err != nil || id == 0 {
		c.JSON(http.StatusNotFound, gin.H{"error": "engineer not found"})
		return 0, false
	}
	return id, true
}

func RegisterRoutes(r gin.IRouter) {
	registerRoutesAt(r, service.LegacyAPIBasePath)
	registerRoutesAt(r, service.SharedAPIBasePath)
}

func registerRoutesAt(r gin.IRouter, basePath string) {
	s := service.NewScheduleService()

	// ---------------------------------------------------------------------
	// Schedule (cross-team reads)
	// ---------------------------------------------------------------------
	r.GET(basePath+"/schedule", func(c *gin.Context) {
		parseTeamIDs := func() ([]uint, error) {
			ids := make([]uint, 0)
			for _, raw := range c.QueryArray("team_ids") {
				for _, p := range strings.Split(strings.TrimSpace(raw), ",") {
					p = strings.TrimSpace(p)
					if p == "" {
						continue
					}
					n, err := strconv.Atoi(p)
					if err != nil || n <= 0 {
						return nil, strconv.ErrSyntax
					}
					ids = append(ids, uint(n))
				}
			}
			for _, raw := range c.QueryArray("team_ids[]") {
				for _, p := range strings.Split(strings.TrimSpace(raw), ",") {
					p = strings.TrimSpace(p)
					if p == "" {
						continue
					}
					n, err := strconv.Atoi(p)
					if err != nil || n <= 0 {
						return nil, strconv.ErrSyntax
					}
					ids = append(ids, uint(n))
				}
			}
			if rawCSV := strings.TrimSpace(c.Query("team_ids_csv")); rawCSV != "" {
				for _, p := range strings.Split(rawCSV, ",") {
					p = strings.TrimSpace(p)
					if p == "" {
						continue
					}
					n, err := strconv.Atoi(p)
					if err != nil || n <= 0 {
						return nil, strconv.ErrSyntax
					}
					ids = append(ids, uint(n))
				}
			}
			if single := strings.TrimSpace(c.Query("team_id")); single != "" {
				n, err := strconv.Atoi(single)
				if err != nil || n <= 0 {
					return nil, strconv.ErrSyntax
				}
				ids = append(ids, uint(n))
			}
			uniq := make(map[uint]struct{}, len(ids))
			out := make([]uint, 0, len(ids))
			for _, id := range ids {
				if _, ok := uniq[id]; ok {
					continue
				}
				uniq[id] = struct{}{}
				out = append(out, id)
			}
			return out, nil
		}

		teamIDs, err := parseTeamIDs()
		if err != nil || len(teamIDs) == 0 {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid or missing team_ids"})
			return
		}

		startTs := int64(0)
		endTs := int64(0)
		isBackup := false
		if v := strings.ToLower(strings.TrimSpace(c.Query("is_backup"))); v != "" {
			if v == "1" || v == "true" || v == "yes" {
				isBackup = true
			}
		}
		includeEmpty := false
		if v := strings.ToLower(strings.TrimSpace(c.Query("include_empty"))); v != "" {
			if v == "1" || v == "true" || v == "yes" {
				includeEmpty = true
			}
		}
		if v := strings.TrimSpace(c.Query("start_time")); v != "" {
			n, err := strconv.ParseInt(v, 10, 64)
			if err != nil || n <= 0 {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid start_time"})
				return
			}
			startTs = n
		}
		if v := strings.TrimSpace(c.Query("end_time")); v != "" {
			n, err := strconv.ParseInt(v, 10, 64)
			if err != nil || n <= 0 {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid end_time"})
				return
			}
			endTs = n
		}
		if startTs == 0 {
			startTs = time.Now().Unix()
		}
		if endTs == 0 {
			endTs = startTs
		}

		rows, err := s.GetEffectiveScheduleV2(teamIDs, startTs, endTs, isBackup, includeEmpty)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"team_ids":   teamIDs,
			"start_time": startTs,
			"end_time":   endTs,
			"is_backup":  isBackup,
			"rows":       rows,
		})
	})

	r.GET(basePath+"/schedule/overview", func(c *gin.Context) {
		groupName := strings.TrimSpace(c.Query("group_name"))
		res, err := s.GetCurrentOverview(groupName)
		if err != nil {
			c.JSON(500, gin.H{"error": err.Error()})
			return
		}
		c.JSON(200, res)
	})

	// ---------------------------------------------------------------------
	// Teams (resource + sub-resources)
	// ---------------------------------------------------------------------
	teams := r.Group(basePath + "/teams")
	{
		teams.GET("", func(c *gin.Context) {
			groupName := strings.TrimSpace(c.Query("group_name"))
			res, err := s.ListTeams(groupName)
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, gin.H{"teams": res})
		})

		teams.POST("", func(c *gin.Context) {
			var req struct {
				Name           string   `json:"name"`
				DisplayName    string   `json:"display_name"`
				Managers       []string `json:"managers"`
				GroupNames     []string `json:"group_names"`
				SortPriority   int      `json:"sort_priority"`
				Description    string   `json:"description"`
				ValidateFeishu *bool    `json:"validate_feishu"`
				ValidateDomain *bool    `json:"validate_domain"`
			}
			if err := c.BindJSON(&req); err != nil {
				c.JSON(400, gin.H{"error": "bad request"})
				return
			}
			requestUserEmail, err := service.RequestUserEmailFromContext(c)
			if err != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
				return
			}
			if !service.IsSuperAdmin(requestUserEmail) {
				c.JSON(http.StatusForbidden, gin.H{"error": "forbidden: only super admin can create team"})
				return
			}
			if len(req.Managers) == 0 {
				req.Managers = []string{requestUserEmail}
			}
			validateFeishu := true
			if req.ValidateFeishu != nil {
				validateFeishu = *req.ValidateFeishu
			}
			validateDomain := true
			if req.ValidateDomain != nil {
				validateDomain = *req.ValidateDomain
			}
			if err := s.AddTeam(req.Name, req.DisplayName, req.Managers, req.GroupNames, req.SortPriority, req.Description, validateFeishu, validateDomain); err != nil {
				c.JSON(500, gin.H{"error": err.Error()})
				return
			}
			c.JSON(200, gin.H{"status": "successfully added team"})
		})

		// GET /api/v1/teams/{ref} (id or slug) -> team detail. Defined in team_router.go.
		AddTeamRoutes(teams, s)

		teams.PATCH("/:ref", func(c *gin.Context) {
			teamID, ok := teamRefToID(c, s)
			if !ok {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team ref"})
				return
			}
			var req struct {
				Name           string    `json:"name"`
				DisplayName    *string   `json:"display_name"`
				GroupNames     *[]string `json:"group_names"`
				SortPriority   *int      `json:"sort_priority"`
				Managers       []string  `json:"managers"`
				Description    *string   `json:"description"`
				FeishuChatID   *string   `json:"feishu_chat_id"`
				ValidateFeishu *bool     `json:"validate_feishu"`
				ValidateDomain *bool     `json:"validate_domain"`
			}
			if err := c.BindJSON(&req); err != nil {
				c.JSON(400, gin.H{"error": "bad request"})
				return
			}

			userEmail, authErr := service.RequestUserEmailFromContext(c)
			if authErr != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": authErr.Error()})
				return
			}
			// Managers replaces the whole manager set, so it is a role change in
			// disguise and needs the manager gate; the descriptive fields only
			// need write access.
			permErr := s.EnsureTeamWritePermission(teamID, userEmail)
			if permErr == nil && len(req.Managers) > 0 {
				permErr = s.EnsureTeamAdminPermission(teamID, userEmail)
			}
			if permErr != nil {
				c.JSON(http.StatusForbidden, gin.H{"error": permErr.Error()})
				return
			}

			validateFeishu := true
			if req.ValidateFeishu != nil {
				validateFeishu = *req.ValidateFeishu
			}
			validateDomain := true
			if req.ValidateDomain != nil {
				validateDomain = *req.ValidateDomain
			}
			if err := s.UpdateTeam(teamID, service.UpdateTeamRequest{
				Name:           req.Name,
				DisplayName:    req.DisplayName,
				GroupNames:     req.GroupNames,
				SortPriority:   req.SortPriority,
				Managers:       req.Managers,
				Description:    req.Description,
				FeishuChatID:   req.FeishuChatID,
				ValidateFeishu: validateFeishu,
				ValidateDomain: validateDomain,
			}); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				return
			}

			if req.FeishuChatID != nil && strings.TrimSpace(*req.FeishuChatID) != "" {
				chatID := strings.TrimSpace(*req.FeishuChatID)
				go func() {
					_, members, err := s.GetTeamWithMembers(teamID)
					if err != nil {
						return
					}
					emails := make([]string, 0, len(members))
					for _, m := range members {
						emails = append(emails, m.Email)
					}
					service.InviteToFeishuChat(chatID, emails)
				}()
			}

			c.JSON(http.StatusOK, gin.H{"status": "successfully updated team"})
		})

		teams.POST("/:ref/chat", func(c *gin.Context) {
			teamID, ok := teamRefToID(c, s)
			if !ok {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team ref"})
				return
			}
			userEmail, authErr := service.RequestUserEmailFromContext(c)
			if authErr != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": authErr.Error()})
				return
			}
			if err := s.EnsureTeamWritePermission(teamID, userEmail); err != nil {
				c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
				return
			}

			team, members, err := s.GetTeamWithMembers(teamID)
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
				return
			}
			displayName := team.DisplayName
			if displayName == "" {
				displayName = team.Name
			}
			chatName := "Oncall – " + displayName

			emails := make([]string, 0, len(members))
			for _, m := range members {
				emails = append(emails, m.Email)
			}

			chatID, err := service.CreateFeishuGroupChat(chatName, emails)
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
				return
			}

			cid := chatID
			if err := s.UpdateTeam(teamID, service.UpdateTeamRequest{FeishuChatID: &cid}); err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": "chat created but failed to save: " + err.Error()})
				return
			}

			c.JSON(http.StatusOK, gin.H{"chat_id": chatID})
		})

		teams.DELETE("/:ref", func(c *gin.Context) {
			teamID, ok := teamRefToID(c, s)
			if !ok {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team ref"})
				return
			}
			userEmail, authErr := service.RequestUserEmailFromContext(c)
			if authErr != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": authErr.Error()})
				return
			}
			if err := s.EnsureTeamAdminPermission(teamID, userEmail); err != nil {
				c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
				return
			}
			if err := s.DisableTeam(teamID); err != nil {
				c.JSON(500, gin.H{"error": err.Error()})
				return
			}
			c.JSON(200, gin.H{"status": "successfully disabled team"})
		})

		teams.GET("/:ref/current-oncall", func(c *gin.Context) {
			teamID, ok := teamRefToID(c, s)
			if !ok {
				respondError(c, http.StatusNotFound, ErrorCodeTeamNotFound, "team does not exist")
				return
			}
			atTime := int64(0)
			if rawAtTime := strings.TrimSpace(c.Query("at_time")); rawAtTime != "" {
				parsedAtTime, err := strconv.ParseInt(rawAtTime, 10, 64)
				if err != nil || parsedAtTime <= 0 {
					respondError(c, http.StatusBadRequest, ErrorCodeInvalidArgument, "at_time must be a positive Unix timestamp in seconds")
					return
				}
				atTime = parsedAtTime
			}
			res, err := s.GetCurrentOnCall(teamID, atTime)
			if err != nil {
				if errors.Is(err, gorm.ErrRecordNotFound) {
					respondError(c, http.StatusNotFound, ErrorCodeTeamNotFound, "team does not exist")
					return
				}
				respondInternalError(c)
				return
			}
			c.JSON(http.StatusOK, res)
		})

		teams.GET("/:ref/schedule/computed", func(c *gin.Context) {
			teamID, ok := teamRefToID(c, s)
			if !ok {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team ref"})
				return
			}
			from, _ := strconv.ParseInt(strings.TrimSpace(c.Query("from")), 10, 64)
			to, _ := strconv.ParseInt(strings.TrimSpace(c.Query("to")), 10, 64)
			if from <= 0 {
				from = time.Now().Unix()
			}
			if to <= from {
				to = from + 14*24*3600
			}
			segs, err := s.ComputeScheduleWindow(teamID, from, to)
			if err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, gin.H{"team_id": teamID, "from": from, "to": to, "segments": segs})
		})

		teams.POST("/:ref/schedule/preview", func(c *gin.Context) {
			teamID, ok := teamRefToID(c, s)
			if !ok {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team ref"})
				return
			}
			var req struct {
				StartTime     int64  `json:"start_time"`
				EndTime       int64  `json:"end_time"`
				EngineerEmail string `json:"engineer_email"`
				IsBackup      bool   `json:"is_backup"`
			}
			if err := c.BindJSON(&req); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
				return
			}
			userEmail, authErr := service.RequestUserEmailFromContext(c)
			if authErr != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": authErr.Error()})
				return
			}
			previewTargets := []string{}
			if em := strings.TrimSpace(req.EngineerEmail); em != "" {
				previewTargets = append(previewTargets, em)
			}
			if err := s.EnsureTeamOverridePermission(teamID, userEmail, previewTargets); err != nil {
				c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
				return
			}
			res, err := s.PreviewManualSegmentChange(teamID, req.StartTime, req.EndTime, req.EngineerEmail, req.IsBackup)
			if err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, res)
		})

		teams.GET("/:ref/layers", func(c *gin.Context) {
			teamID, ok := teamRefToID(c, s)
			if !ok {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team ref"})
				return
			}
			res, err := s.GetTeamLayers(teamID)
			if err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, res)
		})

		teams.PUT("/:ref/layers", func(c *gin.Context) {
			teamID, ok := teamRefToID(c, s)
			if !ok {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team ref"})
				return
			}
			var req struct {
				Layers []service.LayerInput `json:"layers"`
			}
			if err := c.BindJSON(&req); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
				return
			}
			userEmail, err := service.RequestUserEmailFromContext(c)
			if err != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
				return
			}
			if err := s.EnsureTeamWritePermission(teamID, userEmail); err != nil {
				c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
				return
			}
			res, err := s.ReplaceTeamLayers(teamID, req.Layers)
			if err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, res)
		})

		// Dry-run of PUT /layers: same payload, nothing written. Returns the
		// handover summary, the stitched baseline preview and warnings so the
		// UI can show what a save will do before applying it.
		teams.POST("/:ref/layers/preview", func(c *gin.Context) {
			teamID, ok := teamRefToID(c, s)
			if !ok {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team ref"})
				return
			}
			var req struct {
				Layers []service.LayerInput `json:"layers"`
			}
			if err := c.BindJSON(&req); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
				return
			}
			userEmail, err := service.RequestUserEmailFromContext(c)
			if err != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
				return
			}
			if err := s.EnsureTeamWritePermission(teamID, userEmail); err != nil {
				c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
				return
			}
			res, err := s.PreviewTeamLayers(teamID, req.Layers)
			if err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, res)
		})

		teams.PUT("/:ref/backup-lane", func(c *gin.Context) {
			teamID, ok := teamRefToID(c, s)
			if !ok {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team ref"})
				return
			}
			var req struct {
				Enabled bool `json:"enabled"`
			}
			if err := c.BindJSON(&req); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
				return
			}
			userEmail, err := service.RequestUserEmailFromContext(c)
			if err != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
				return
			}
			if err := s.EnsureTeamWritePermission(teamID, userEmail); err != nil {
				c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
				return
			}
			res, err := s.SetTeamBackupEnabled(teamID, req.Enabled)
			if err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, res)
		})

		teams.GET("/:ref/available-engineers", func(c *gin.Context) {
			teamID, ok := teamRefToID(c, s)
			if !ok {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team ref"})
				return
			}
			res, err := s.GetTeamAvailableEngineers(teamID)
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, gin.H{"team_id": teamID, "engineers": res})
		})

		teams.POST("/:ref/engineers", func(c *gin.Context) {
			teamID, ok := teamRefToID(c, s)
			if !ok {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team ref"})
				return
			}
			var req struct {
				EngineerID    uint   `json:"engineer_id"`
				EngineerEmail string `json:"engineer_email"`
			}
			if err := c.BindJSON(&req); err != nil {
				c.JSON(400, gin.H{"error": "bad request"})
				return
			}
			userEmail, authErr := service.RequestUserEmailFromContext(c)
			if authErr != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": authErr.Error()})
				return
			}
			if err := s.EnsureTeamWritePermission(teamID, userEmail); err != nil {
				c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
				return
			}
			var opErr error
			if req.EngineerEmail != "" {
				opErr = s.AddEngineerToTeamByEmail(req.EngineerEmail, teamID)
			} else {
				opErr = s.AddEngineerToTeam(req.EngineerID, teamID)
			}
			if opErr != nil {
				c.JSON(500, gin.H{"error": opErr.Error()})
				return
			}
			c.JSON(200, gin.H{"status": "successfully added engineer to team"})
		})

		teams.DELETE("/:ref/engineers/:eref", func(c *gin.Context) {
			teamID, ok := teamRefToID(c, s)
			if !ok {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team ref"})
				return
			}
			userEmail, authErr := service.RequestUserEmailFromContext(c)
			if authErr != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": authErr.Error()})
				return
			}
			if err := s.EnsureTeamWritePermission(teamID, userEmail); err != nil {
				c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
				return
			}
			engineerID, ok := teamEngineerRefToID(c, s)
			if !ok {
				return
			}
			// Dropping a membership also drops the role that came with it, so an
			// editor may only remove plain members.
			if err := s.EnsureCanManageMember(teamID, userEmail, engineerID); err != nil {
				c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
				return
			}
			if opErr := s.DeleteEngineerFromTeam(engineerID, teamID); opErr != nil {
				if errors.Is(opErr, service.ErrLastManager) {
					c.JSON(http.StatusBadRequest, gin.H{"error": opErr.Error()})
					return
				}
				c.JSON(500, gin.H{"error": opErr.Error()})
				return
			}
			c.JSON(200, gin.H{"status": "successfully deleted"})
		})

		// Roles are the team's permission ladder: member (read-only, may still
		// take an on-call slot themselves) < editor (schedule + membership) <
		// manager (also roles and disabling the team). Manager-only by design.
		teams.PUT("/:ref/engineers/:eref/role", func(c *gin.Context) {
			teamID, ok := teamRefToID(c, s)
			if !ok {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team ref"})
				return
			}
			var req struct {
				Role string `json:"role"`
			}
			if err := c.BindJSON(&req); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
				return
			}
			userEmail, authErr := service.RequestUserEmailFromContext(c)
			if authErr != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": authErr.Error()})
				return
			}
			if err := s.EnsureTeamAdminPermission(teamID, userEmail); err != nil {
				c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
				return
			}

			eref := strings.TrimSpace(c.Param("eref"))
			var opErr error
			if n, err := strconv.ParseUint(eref, 10, 64); err == nil && n > 0 {
				opErr = s.SetTeamMemberRole(uint(n), teamID, req.Role)
			} else {
				opErr = s.SetTeamMemberRoleByEmail(eref, teamID, req.Role)
			}
			if opErr != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": opErr.Error()})
				return
			}
			c.JSON(http.StatusOK, gin.H{"status": "successfully updated role", "role": req.Role})
		})

		teams.GET("/:ref/managers", func(c *gin.Context) {
			teamID, ok := teamRefToID(c, s)
			if !ok {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team ref"})
				return
			}
			res, err := s.GetTeamManagers(teamID)
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, gin.H{"team_id": teamID, "managers": res})
		})

		teams.POST("/:ref/managers", func(c *gin.Context) {
			teamID, ok := teamRefToID(c, s)
			if !ok {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team ref"})
				return
			}
			var req struct {
				EngineerID    uint   `json:"engineer_id"`
				EngineerEmail string `json:"engineer_email"`
			}
			if err := c.BindJSON(&req); err != nil {
				c.JSON(400, gin.H{"error": "bad request"})
				return
			}
			userEmail, authErr := service.RequestUserEmailFromContext(c)
			if authErr != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": authErr.Error()})
				return
			}
			if err := s.EnsureTeamAdminPermission(teamID, userEmail); err != nil {
				c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
				return
			}
			var opErr error
			if req.EngineerEmail != "" {
				opErr = s.AddManagerToTeamByEmail(req.EngineerEmail, teamID)
			} else {
				opErr = s.AddManagerToTeam(req.EngineerID, teamID)
			}
			if opErr != nil {
				c.JSON(500, gin.H{"error": opErr.Error()})
				return
			}
			c.JSON(200, gin.H{"status": "successfully added manager to team"})
		})

		teams.DELETE("/:ref/managers/:email", func(c *gin.Context) {
			teamID, ok := teamRefToID(c, s)
			if !ok {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team ref"})
				return
			}
			email := strings.TrimSpace(c.Param("email"))
			userEmail, err := service.RequestUserEmailFromContext(c)
			if err != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
				return
			}
			if err := s.EnsureTeamAdminPermission(teamID, userEmail); err != nil {
				c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
				return
			}
			if err := s.RemoveManagerFromTeamByEmail(email, teamID); err != nil {
				if errors.Is(err, service.ErrLastManager) {
					c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
					return
				}
				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, gin.H{"status": "successfully removed manager from team"})
		})
	}

	// ---------------------------------------------------------------------
	// Notifications (group-scoped actions)
	// ---------------------------------------------------------------------
	notifications := r.Group(basePath + "/notifications")
	{
		notifications.POST("/feishu/preview", func(c *gin.Context) {
			var req struct {
				GroupName          string `json:"group_name"`
				AtTime             int64  `json:"at_time"`
				SkipSameSubstitute *bool  `json:"skip_same_substitute"`
				ExcludeTestTeams   *bool  `json:"exclude_test_teams"`
			}
			if err := c.BindJSON(&req); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
				return
			}
			skipSame := true
			if req.SkipSameSubstitute != nil {
				skipSame = *req.SkipSameSubstitute
			}
			excludeTests := true
			if req.ExcludeTestTeams != nil {
				excludeTests = *req.ExcludeTestTeams
			}
			preview, err := s.BuildFeishuNotifyPreview(req.GroupName, req.AtTime, skipSame, excludeTests)
			if err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, preview)
		})

		notifications.POST("/feishu", func(c *gin.Context) {
			var req struct {
				GroupName          string   `json:"group_name"`
				AtTime             int64    `json:"at_time"`
				SkipSameSubstitute *bool    `json:"skip_same_substitute"`
				ExcludeTestTeams   *bool    `json:"exclude_test_teams"`
				Recipients         []string `json:"recipients"`
				RecipientEmails    []string `json:"recipient_emails"`
			}
			if err := c.BindJSON(&req); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
				return
			}
			skipSame := true
			if req.SkipSameSubstitute != nil {
				skipSame = *req.SkipSameSubstitute
			}
			excludeTests := true
			if req.ExcludeTestTeams != nil {
				excludeTests = *req.ExcludeTestTeams
			}
			preview, err := s.SendFeishuNotifyPreview(req.GroupName, req.AtTime, skipSame, excludeTests, req.Recipients, req.RecipientEmails)
			if err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, gin.H{"status": "sent", "recipients": req.Recipients, "recipient_emails": req.RecipientEmails, "preview": preview})
		})

		notifications.POST("/private/preview", func(c *gin.Context) {
			var req struct {
				GroupName        string `json:"group_name"`
				AtTime           int64  `json:"at_time"`
				ExcludeTestTeams *bool  `json:"exclude_test_teams"`
			}
			if err := c.BindJSON(&req); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
				return
			}
			userEmail, err := service.RequestUserEmailFromContext(c)
			if err != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
				return
			}
			if !service.IsSuperAdmin(userEmail) {
				c.JSON(http.StatusForbidden, gin.H{"error": "forbidden: only super admin can preview privilege batches"})
				return
			}
			excludeTests := true
			if req.ExcludeTestTeams != nil {
				excludeTests = *req.ExcludeTestTeams
			}
			preview, err := s.BuildPrivilegePreview(req.GroupName, req.AtTime, excludeTests)
			if err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, preview)
		})

		notifications.POST("/private", func(c *gin.Context) {
			var req struct {
				GroupName        string `json:"group_name"`
				AtTime           int64  `json:"at_time"`
				ExcludeTestTeams *bool  `json:"exclude_test_teams"`
			}
			if err := c.BindJSON(&req); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
				return
			}
			userEmail, err := service.RequestUserEmailFromContext(c)
			if err != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
				return
			}
			if !service.IsSuperAdmin(userEmail) {
				c.JSON(http.StatusForbidden, gin.H{"error": "forbidden: only super admin can send privilege batches"})
				return
			}
			excludeTests := true
			if req.ExcludeTestTeams != nil {
				excludeTests = *req.ExcludeTestTeams
			}
			summary, err := s.SendPrivilege(req.GroupName, req.AtTime, excludeTests)
			if err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(), "summary": summary})
				return
			}
			c.JSON(http.StatusOK, summary)
		})
	}

	// ---------------------------------------------------------------------
	// Overlays (manual overrides; may span teams, so kept top-level)
	// ---------------------------------------------------------------------
	overlays := r.Group(basePath + "/overlays")
	{
		overlays.POST("", func(c *gin.Context) {
			var req struct {
				Type          string `json:"type"`
				TeamID        uint   `json:"team_id"`
				PlanSignature string `json:"plan_signature"`
				StartTime     int64  `json:"start_time"`
				EndTime       int64  `json:"end_time"`
				EngineerEmail string `json:"engineer_email"`
				IsBackup      bool   `json:"is_backup"`
				Text          string `json:"text"`
				Items         []struct {
					TeamID        uint   `json:"team_id"`
					EngineerID    uint   `json:"engineer_id"`
					EngineerEmail string `json:"engineer_email"`
					StartTime     int64  `json:"start_time"`
					EndTime       int64  `json:"end_time"`
					Overlay       *int   `json:"overlay"`
					OverlayMode   string `json:"overlay_mode"`
					IsBackup      bool   `json:"is_backup"`
					SourceType    string `json:"source_type"`
					Reason        string `json:"reason"`
				} `json:"items"`
			}
			if err := c.BindJSON(&req); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
				return
			}
			userEmail, err := service.RequestUserEmailFromContext(c)
			if err != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
				return
			}

			t := strings.ToLower(strings.TrimSpace(req.Type))
			switch t {
			case "segment":
				engineerEmails := make([]string, 0)
				if len(req.Items) > 0 {
					for _, it := range req.Items {
						em := strings.TrimSpace(it.EngineerEmail)
						if em != "" {
							engineerEmails = append(engineerEmails, em)
						}
					}
				}
				if len(engineerEmails) == 0 {
					em := strings.TrimSpace(req.EngineerEmail)
					if em != "" {
						engineerEmails = append(engineerEmails, em)
					}
				}
				if err := s.EnsureTeamOverridePermission(req.TeamID, userEmail, engineerEmails); err != nil {
					c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
					return
				}
				rows, err := s.ApplyManualRangeOverlay(req.TeamID, req.StartTime, req.EndTime, engineerEmails, req.IsBackup, userEmail)
				if err != nil {
					c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
					return
				}
				c.JSON(http.StatusOK, gin.H{"status": "applied", "type": "segment", "is_backup": req.IsBackup, "items": rows})
				return
			default:
				if len(req.Items) == 0 {
					c.JSON(http.StatusBadRequest, gin.H{"error": "missing type or items"})
					return
				}
				applyItems := make([]service.OverlayApplyItem, 0, len(req.Items))
				permittedCats := map[uint]struct{}{}
				for _, it := range req.Items {
					if it.TeamID == 0 {
						c.JSON(http.StatusBadRequest, gin.H{"error": "items[].team_id is required"})
						return
					}
					if _, ok := permittedCats[it.TeamID]; !ok {
						if err := s.EnsureTeamWritePermission(it.TeamID, userEmail); err == nil {
							// Full manager/super-admin write access for this team:
							// no need to re-check subsequent items for the same team.
							permittedCats[it.TeamID] = struct{}{}
						} else if err := s.EnsureTeamOverridePermission(it.TeamID, userEmail, []string{it.EngineerEmail}); err != nil {
							// Not a manager: each item is checked individually since
							// a self-assigning team member's items may target
							// different people across items for the same team.
							c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
							return
						}
					}
					applyItems = append(applyItems, service.OverlayApplyItem{
						TeamID:        it.TeamID,
						EngineerID:    it.EngineerID,
						EngineerEmail: it.EngineerEmail,
						StartTime:     it.StartTime,
						EndTime:       it.EndTime,
						Overlay:       it.Overlay,
						OverlayMode:   it.OverlayMode,
						IsBackup:      it.IsBackup,
						SourceType:    it.SourceType,
						Reason:        it.Reason,
					})
				}
				rows, err := s.ApplyOverlayItems(applyItems, userEmail)
				if err != nil {
					c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
					return
				}
				c.JSON(http.StatusOK, gin.H{"status": "applied", "type": "items", "items": rows})
				return
			}
		})

		overlays.DELETE("", func(c *gin.Context) {
			teamID, _ := strconv.ParseUint(strings.TrimSpace(c.Query("team_id")), 10, 64)
			startTime, _ := strconv.ParseInt(strings.TrimSpace(c.Query("start_time")), 10, 64)
			endTime, _ := strconv.ParseInt(strings.TrimSpace(c.Query("end_time")), 10, 64)
			isBackup := false
			if v := strings.ToLower(strings.TrimSpace(c.Query("is_backup"))); v == "1" || v == "true" || v == "yes" {
				isBackup = true
			}
			if teamID == 0 {
				c.JSON(http.StatusBadRequest, gin.H{"error": "missing team_id"})
				return
			}
			userEmail, err := service.RequestUserEmailFromContext(c)
			if err != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
				return
			}
			if err := s.EnsureTeamWritePermission(uint(teamID), userEmail); err != nil {
				c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
				return
			}
			n, err := s.RemoveOverlayRange(uint(teamID), startTime, endTime, isBackup)
			if err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, gin.H{"status": "removed", "removed_count": n, "is_backup": isBackup})
		})
	}

	// ---------------------------------------------------------------------
	// System phones (caller IDs oncallers should whitelist; auth-gated)
	// ---------------------------------------------------------------------
	r.GET(basePath+"/system-phones", func(c *gin.Context) {
		if _, err := service.RequestUserEmailFromContext(c); err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, gin.H{
			"groups": []gin.H{
				{
					"name": "Mainland China (Powered by Aliyun)",
					"phones": []gin.H{
						{"region": "Hangzhou", "phone": "0571-56775972"},
						{"region": "Hangzhou", "phone": "0571-56089310"},
						{"region": "Hangzhou", "phone": "0571-56106752"},
						{"region": "Guangzhou", "phone": "020-28307952"},
					},
				},
				{
					"name": "Global (Powered by Twilio)",
					"phones": []gin.H{
						{"region": "US & Canada (+1)", "phone": "+1 561 918 5301"},
						{"region": "Singapore (+65)", "phone": "+65 3152 1431"},
					},
				},
			},
			"feishu_notice": "Feishu urgent calls are becoming the default phone notification channel. Please add the Feishu urgent-call numbers to your contacts: open Feishu → Settings → Notifications → Add urgent-call numbers to contacts.\n飞书加急电话即将成为默认的电话通知方式。请将飞书加急号码添加到通讯录：打开飞书 → 设置 → 通知 → 添加飞书加急号码到通讯录。",
		})
	})

	// ---------------------------------------------------------------------
	// Page phone (user self-service)
	// ---------------------------------------------------------------------
	r.GET(basePath+"/me/page-phone", func(c *gin.Context) {
		userEmail, err := service.RequestUserEmailFromContext(c)
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
			return
		}
		var eng models.Engineer
		if err := db.DB.Where("email = ?", userEmail).First(&eng).Error; err != nil {
			c.JSON(http.StatusNotFound, gin.H{"error": "engineer not found"})
			return
		}
		c.JSON(http.StatusOK, gin.H{"page_phone": eng.PagePhone})
	})

	r.PUT(basePath+"/me/page-phone", func(c *gin.Context) {
		userEmail, err := service.RequestUserEmailFromContext(c)
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
			return
		}
		var req struct {
			PagePhone string `json:"page_phone"`
		}
		if err := c.BindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
			return
		}
		result := db.DB.Model(&models.Engineer{}).Where("email = ?", userEmail).Update("page_phone", strings.TrimSpace(req.PagePhone))
		if result.Error != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
			return
		}
		if result.RowsAffected == 0 {
			c.JSON(http.StatusNotFound, gin.H{"error": "engineer not found"})
			return
		}
		c.JSON(http.StatusOK, gin.H{"page_phone": strings.TrimSpace(req.PagePhone)})
	})

	// ---------------------------------------------------------------------
	// Page (phone call + Feishu urgent)
	// ---------------------------------------------------------------------
	if pg := pager.NewFromEnv(); pg != nil {
		pg.SetPhoneLookup(func(email string) (string, string) {
			var eng models.Engineer
			if err := db.DB.Select("page_phone", "phone").Where("email = ?", email).First(&eng).Error; err != nil {
				return "", ""
			}
			return eng.PagePhone, eng.Phone
		})
		r.POST(basePath+"/page", func(c *gin.Context) {
			var req pager.PageRequest
			if err := c.BindJSON(&req); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
				return
			}
			if strings.TrimSpace(req.UserEmail) == "" {
				c.JSON(http.StatusBadRequest, gin.H{"error": "user_email is required"})
				return
			}
			result, err := pg.Page(c.Request.Context(), req)
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, result)
		})
	}

	// ---------------------------------------------------------------------
	// API keys
	// ---------------------------------------------------------------------
	apiKeys := r.Group(basePath + "/api-keys")
	{
		apiKeys.GET("", func(c *gin.Context) {
			userEmail, err := service.RequestUserEmailFromContext(c)
			if err != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
				return
			}

			keys, err := service.ListApiKeys(userEmail)
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusOK, keys)
		})

		apiKeys.POST("", func(c *gin.Context) {
			userEmail, err := service.RequestUserEmailFromContext(c)
			if err != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
				return
			}

			var req struct {
				Name string `json:"name"`
			}
			if err := c.BindJSON(&req); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
				return
			}
			if strings.TrimSpace(req.Name) == "" {
				c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
				return
			}

			plaintext, apiKey, err := service.CreateApiKey(userEmail, req.Name, models.ApiKeyTypeUser, "")
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
				return
			}

			// The plaintext key is returned exactly once; only its hash is stored.
			c.JSON(http.StatusOK, gin.H{
				"id":         apiKey.ID,
				"name":       apiKey.Name,
				"key":        plaintext,
				"key_prefix": apiKey.KeyPrefix,
				"created_at": apiKey.CreatedAt,
			})
		})

		// Service (machine-to-machine) keys are least-privilege and admin-issued.
		// Requested scopes are validated against the allowed set; default is read-only.
		apiKeys.POST("/service", func(c *gin.Context) {
			userEmail, err := service.RequestUserEmailFromContext(c)
			if err != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
				return
			}
			if !service.IsSuperAdmin(userEmail) {
				c.JSON(http.StatusForbidden, gin.H{"error": "only admins can create service keys"})
				return
			}

			var req struct {
				Name   string   `json:"name"`
				Scopes []string `json:"scopes"`
			}
			if err := c.BindJSON(&req); err != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
				return
			}
			if strings.TrimSpace(req.Name) == "" {
				c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
				return
			}

			// Default to read-only; validate every requested scope against the allowed set.
			allowed := map[string]bool{service.ScopeOncallRead: true, service.ScopeOncallWrite: true}
			scopeSet := map[string]bool{service.ScopeOncallRead: true}
			for _, sc := range req.Scopes {
				sc = strings.TrimSpace(sc)
				if sc == "" {
					continue
				}
				if !allowed[sc] {
					c.JSON(http.StatusBadRequest, gin.H{"error": "unknown scope: " + sc})
					return
				}
				scopeSet[sc] = true
			}
			scopes := make([]string, 0, len(scopeSet))
			for sc := range scopeSet {
				scopes = append(scopes, sc)
			}

			plaintext, apiKey, err := service.CreateApiKey(userEmail, req.Name, models.ApiKeyTypeService, strings.Join(scopes, " "))
			if err != nil {
				c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
				return
			}

			c.JSON(http.StatusOK, gin.H{
				"id":         apiKey.ID,
				"name":       apiKey.Name,
				"type":       apiKey.Type,
				"scopes":     apiKey.Scopes,
				"key":        plaintext,
				"key_prefix": apiKey.KeyPrefix,
				"created_at": apiKey.CreatedAt,
			})
		})

		apiKeys.DELETE("/:id", func(c *gin.Context) {
			userEmail, err := service.RequestUserEmailFromContext(c)
			if err != nil {
				c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
				return
			}

			id, _ := strconv.ParseUint(strings.TrimSpace(c.Param("id")), 10, 64)
			if id == 0 {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
				return
			}

			if err := service.DeleteApiKey(userEmail, uint(id)); err != nil {
				c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
				return
			}

			c.JSON(http.StatusOK, gin.H{"status": "deleted"})
		})
	}
}
