package service

import (
	"errors"
	"fmt"
	"log/slog"
	"regexp"
	"sort"
	"strings"
	"time"

	"gorm.io/gorm"

	"github.com/tidbcloud/aisupport/services/oncall/db"
	"github.com/tidbcloud/aisupport/services/oncall/models"
)

type ScheduleService struct{}

func NewScheduleService() *ScheduleService {
	return &ScheduleService{}
}

func normalizeEmail(email string) string {
	return strings.ToLower(strings.TrimSpace(email))
}

func normalizeTeamKeyName(name string) string {
	return strings.TrimSpace(name)
}

var teamKeyNameRe = regexp.MustCompile(`^[a-z][a-z0-9_]*$`)

func validateTeamKeyName(name string) error {
	if name == "" {
		return errors.New("name is required")
	}
	if strings.ToLower(name) != name {
		return errors.New("name must be lowercase")
	}
	if !teamKeyNameRe.MatchString(name) {
		return errors.New("name must start with a lowercase letter and contain only lowercase letters, digits, and underscore")
	}
	return nil
}

func uniqEmails(emails []string) []string {
	res := make([]string, 0)
	seen := make(map[string]bool)
	for _, raw := range emails {
		email := normalizeEmail(raw)
		if email == "" || seen[email] {
			continue
		}
		seen[email] = true
		res = append(res, email)
	}
	return res
}

func validateManagedEngineerEmail(email string) error {
	email = normalizeEmail(email)
	if email == "" {
		return errors.New("missing engineer_email")
	}
	if !IsEmailAllowed(email) {
		return fmt.Errorf("email domain not allowed: %s", email)
	}

	exists, err := CheckFeishuUserExists(email)
	if err != nil {
		return fmt.Errorf("failed to check feishu user: %w", err)
	}
	if !exists {
		return fmt.Errorf("user with email %s not found in Feishu", email)
	}
	return nil
}

// ErrLastManager is returned when a change would leave a team with no manager
// at all. Handlers map it to 400 — it is a rejected request, not a failure.
var ErrLastManager = errors.New("cannot remove the last manager from team")

// TeamRoleOf returns a user's effective role in a team: manager for super
// admins, the stored role for members, and "" for everyone else. It is the
// single place team privileges are resolved from.
func (s *ScheduleService) TeamRoleOf(teamID uint, userEmail string) (string, error) {
	email := normalizeEmail(userEmail)
	if email == "" {
		return "", errors.New("missing user email")
	}
	if IsSuperAdmin(email) {
		return models.TeamRoleManager, nil
	}

	var roles []string
	if err := db.DB.Table("engineer_teams").
		Joins("JOIN engineers ON engineers.id = engineer_teams.engineer_id").
		Where("engineer_teams.team_id = ? AND lower(engineers.email) = ?", teamID, email).
		Pluck("engineer_teams.role", &roles).Error; err != nil {
		return "", err
	}
	if len(roles) == 0 {
		return "", nil
	}

	// A user should have a single link per team, but duplicates exist in older
	// data; the most privileged one wins.
	best := models.TeamRoleMember
	for _, role := range roles {
		if models.TeamRoleRank(role) > models.TeamRoleRank(best) {
			best = role
		}
	}
	return best, nil
}

func (s *ScheduleService) hasTeamRole(teamID uint, userEmail, min string) (bool, error) {
	role, err := s.TeamRoleOf(teamID, userEmail)
	if err != nil {
		return false, err
	}
	if role == "" {
		return false, nil
	}
	return models.TeamRoleRank(role) >= models.TeamRoleRank(min), nil
}

// IsTeamManager reports whether the user may administer the team (grant roles,
// disable it). Super admins always may.
func (s *ScheduleService) IsTeamManager(teamID uint, userEmail string) (bool, error) {
	return s.hasTeamRole(teamID, userEmail, models.TeamRoleManager)
}

// IsTeamEditor reports whether the user may change the team's schedule and
// membership — true for editors and managers alike.
func (s *ScheduleService) IsTeamEditor(teamID uint, userEmail string) (bool, error) {
	return s.hasTeamRole(teamID, userEmail, models.TeamRoleEditor)
}

// EnsureTeamWritePermission gates the day-to-day writes: layers, backup lane,
// overrides, and adding or removing plain members. Editors and managers pass.
func (s *ScheduleService) EnsureTeamWritePermission(teamID uint, userEmail string) error {
	if ShouldBypassAuth() {
		return nil
	}

	ok, err := s.IsTeamEditor(teamID, userEmail)
	if err != nil {
		return err
	}
	if !ok {
		return errors.New("forbidden: you do not have permission to modify this team")
	}
	return nil
}

// EnsureTeamAdminPermission gates the manager-only writes: handing out roles
// and disabling the team. An editor is deliberately not enough — that is the
// line between the two roles.
func (s *ScheduleService) EnsureTeamAdminPermission(teamID uint, userEmail string) error {
	if ShouldBypassAuth() {
		return nil
	}

	ok, err := s.IsTeamManager(teamID, userEmail)
	if err != nil {
		return err
	}
	if !ok {
		return errors.New("forbidden: only a team manager can change roles or disable this team")
	}
	return nil
}

// EnsureCanManageMember gates membership changes on the *target's* role: a
// manager may remove anyone, while an editor may only touch plain members.
// Without this an editor could demote a manager by deleting their membership
// outright, which would sidestep the manager-only role endpoints.
func (s *ScheduleService) EnsureCanManageMember(teamID uint, actorEmail string, targetEngineerID uint) error {
	if ShouldBypassAuth() {
		return nil
	}

	var link models.EngineerTeam
	err := db.DB.Where("team_id = ? AND engineer_id = ?", teamID, targetEngineerID).
		Order("id").First(&link).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil // not a member: nothing privileged to protect
	}
	if err != nil {
		return err
	}
	if models.TeamRoleRank(link.Role) <= models.TeamRoleRank(models.TeamRoleMember) {
		return nil
	}
	return s.EnsureTeamAdminPermission(teamID, actorEmail)
}

// IsTeamMember reports whether userEmail belongs to teamID at all (manager or
// not); super admins always count as members.
func (s *ScheduleService) IsTeamMember(teamID uint, userEmail string) (bool, error) {
	email := normalizeEmail(userEmail)
	if email == "" {
		return false, errors.New("missing user email")
	}
	if IsSuperAdmin(email) {
		return true, nil
	}

	var count int64
	err := db.DB.Table("engineer_teams").
		Joins("JOIN engineers ON engineers.id = engineer_teams.engineer_id").
		Where("engineer_teams.team_id = ? AND lower(engineers.email) = ?", teamID, email).
		Count(&count).Error
	if err != nil {
		return false, err
	}
	return count > 0, nil
}

// EnsureTeamOverridePermission gates manual on-call overrides: anyone with
// write access to the team (editor, manager or super admin) may reassign the
// slot to anyone, while a plain team member may only reassign it to
// themselves — every email in targetEmails must be their own, and clearing the
// slot to no one (empty targetEmails) needs write access.
func (s *ScheduleService) EnsureTeamOverridePermission(teamID uint, userEmail string, targetEmails []string) error {
	if err := s.EnsureTeamWritePermission(teamID, userEmail); err == nil {
		return nil // editor, manager, super admin, or auth bypass — full write access
	}

	isMember, err := s.IsTeamMember(teamID, userEmail)
	if err != nil {
		return err
	}
	if !isMember {
		return errors.New("forbidden: you do not have permission to modify this team")
	}
	if len(targetEmails) == 0 {
		return errors.New("forbidden: only a team editor or manager can clear on-call to no one")
	}

	self := normalizeEmail(userEmail)
	for _, e := range targetEmails {
		if normalizeEmail(e) != self {
			return errors.New("forbidden: team members may only reassign on-call to themselves")
		}
	}
	return nil
}

func (s *ScheduleService) resolveEngineerID(engineerID uint, engineerEmail string, createIfMissing bool) (uint, error) {
	email := strings.TrimSpace(strings.ToLower(engineerEmail))
	if email != "" {
		var eng models.Engineer
		err := db.DB.Where("email = ?", email).First(&eng).Error
		if err == nil {
			return eng.ID, nil
		}
		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return 0, err
		}
		if !createIfMissing {
			return 0, fmt.Errorf("engineer not found by email: %s", email)
		}
		eng = models.Engineer{Email: email, Name: strings.Split(email, "@")[0]}
		if profile, ferr := FetchFeishuUserProfile(email); ferr == nil {
			if profile.Name != "" {
				eng.Name = profile.Name
			}
			eng.EnName = profile.EnName
			eng.Phone = profile.Phone
		} else {
			// Soft failure: keep the email-derived name but log why the Feishu
			// profile (name/en_name/phone) could not be filled in, so missing
			// names are diagnosable instead of silently swallowed.
			slog.Warn("feishu profile fetch failed; using email-derived name",
				"email", email, "error", ferr)
		}
		if err := db.DB.Create(&eng).Error; err != nil {
			return 0, err
		}
		return eng.ID, nil
	}

	if engineerID == 0 {
		return 0, errors.New("missing engineer identifier")
	}

	var eng models.Engineer
	if err := db.DB.First(&eng, engineerID).Error; err != nil {
		return 0, err
	}
	return eng.ID, nil
}

// setMemberRole is the only writer of a membership's privileges: it upserts the
// link at the given role and keeps the legacy is_manager mirror in sync (see
// models.EngineerTeam.IsManager).
func setMemberRole(tx *gorm.DB, engineerID, teamID uint, role string) error {
	role, err := models.NormalizeTeamRole(role)
	if err != nil {
		return err
	}
	updates := map[string]interface{}{
		"role":       role,
		"is_manager": role == models.TeamRoleManager,
	}

	var count int64
	if err := tx.Model(&models.EngineerTeam{}).
		Where("engineer_id = ? AND team_id = ?", engineerID, teamID).Count(&count).Error; err != nil {
		return err
	}
	if count == 0 {
		return tx.Create(&models.EngineerTeam{
			EngineerID: engineerID,
			TeamID:     teamID,
			Role:       role,
			IsManager:  role == models.TeamRoleManager,
		}).Error
	}
	return tx.Model(&models.EngineerTeam{}).
		Where("engineer_id = ? AND team_id = ?", engineerID, teamID).Updates(updates).Error
}

// ensureNotLastManager rejects a change that would strip a team of its last
// manager, which would leave nobody able to hand roles out again.
func ensureNotLastManager(tx *gorm.DB, engineerID, teamID uint) error {
	var managerIDs []uint
	if err := tx.Model(&models.EngineerTeam{}).
		Distinct("engineer_id").
		Where("team_id = ? AND role = ?", teamID, models.TeamRoleManager).
		Pluck("engineer_id", &managerIDs).Error; err != nil {
		return err
	}
	isManager, others := false, 0
	for _, id := range managerIDs {
		if id == engineerID {
			isManager = true
		} else {
			others++
		}
	}
	if isManager && others == 0 {
		return ErrLastManager
	}
	return nil
}

// FindEngineerID resolves an existing engineer by numeric id or email. Unlike
// the grant paths it never creates one.
func (s *ScheduleService) FindEngineerID(engineerID uint, engineerEmail string) (uint, error) {
	return s.resolveEngineerID(engineerID, engineerEmail, false)
}

func (s *ScheduleService) DeleteEngineerFromTeam(engineerID, teamID uint) error {
	if engineerID == 0 || teamID == 0 {
		return errors.New("missing engineer_id or team_id")
	}

	return db.DB.Transaction(func(tx *gorm.DB) error {
		// Removing a membership is also a way to strip someone of their role,
		// so it answers to the same last-manager rule as an explicit demotion.
		if err := ensureNotLastManager(tx, engineerID, teamID); err != nil {
			return err
		}
		return tx.Where("engineer_id = ? AND team_id = ?", engineerID, teamID).Delete(&models.EngineerTeam{}).Error
	})
}

func (s *ScheduleService) AddEngineerToTeam(engineerID, teamID uint) error {
	if engineerID == 0 || teamID == 0 {
		return errors.New("missing engineer_id or team_id")
	}

	var count int64
	db.DB.Model(&models.EngineerTeam{}).Where("engineer_id = ? AND team_id = ?", engineerID, teamID).Count(&count)
	if count > 0 {
		return nil // already a member; never silently changes an existing role
	}
	return setMemberRole(db.DB, engineerID, teamID, models.TeamRoleMember)
}

func (s *ScheduleService) AddEngineerToTeamByEmail(engineerEmail string, teamID uint) error {
	email := normalizeEmail(engineerEmail)
	if err := validateManagedEngineerEmail(email); err != nil {
		return err
	}

	engID, err := s.resolveEngineerID(0, email, true)
	if err != nil {
		return err
	}
	if err := s.AddEngineerToTeam(engID, teamID); err != nil {
		return err
	}

	var team models.Team
	if err := db.DB.Select("feishu_chat_id").First(&team, teamID).Error; err == nil && team.FeishuChatID != "" {
		go InviteToFeishuChat(team.FeishuChatID, []string{email})
	}
	return nil
}

func (s *ScheduleService) AddTeam(name, displayName string, managers []string, groupNames []string, sortPriority int, description string, validateFeishu bool, validateDomain bool) error {
	name = normalizeTeamKeyName(name)
	if err := validateTeamKeyName(name); err != nil {
		return err
	}
	if strings.TrimSpace(displayName) == "" {
		displayName = name
	}
	groups := normalizeGroups(groupNames)
	cat := models.Team{
		Name:         name,
		DisplayName:  strings.TrimSpace(displayName),
		GroupNames:   groups,
		GroupName:    groups[0], // keep legacy column populated
		SortPriority: sortPriority,
		Description:  strings.TrimSpace(description),
		IsDisabled:   false,
	}

	managerList := uniqEmails(managers)
	if len(managerList) == 0 {
		return db.DB.Create(&cat).Error
	}

	return db.DB.Transaction(func(tx *gorm.DB) error {
		if err := tx.Create(&cat).Error; err != nil {
			return err
		}
		for _, email := range managerList {
			if validateDomain && !IsEmailAllowed(email) {
				return fmt.Errorf("email domain not allowed: %s", email)
			}
			if validateFeishu {
				if err := validateManagedEngineerEmail(email); err != nil {
					return err
				}
			}
			engID, err := s.resolveEngineerID(0, email, true)
			if err != nil {
				return err
			}
			if err := setMemberRole(tx, engID, cat.ID, models.TeamRoleManager); err != nil {
				return err
			}
		}
		return nil
	})
}

func (s *ScheduleService) DisableTeam(teamID uint) error {
	return db.DB.Model(&models.Team{}).Where("id = ?", teamID).Update("is_disabled", true).Error
}

func (s *ScheduleService) AddManagerToTeam(engineerID, teamID uint) error {
	return s.SetTeamMemberRole(engineerID, teamID, models.TeamRoleManager)
}

func (s *ScheduleService) AddManagerToTeamByEmail(engineerEmail string, teamID uint) error {
	return s.SetTeamMemberRoleByEmail(engineerEmail, teamID, models.TeamRoleManager)
}

// SetTeamMemberRole moves an engineer to the given rung of the role ladder,
// adding them to the team first if they are not a member yet.
func (s *ScheduleService) SetTeamMemberRole(engineerID, teamID uint, role string) error {
	if engineerID == 0 || teamID == 0 {
		return errors.New("missing engineer_id or team_id")
	}
	if _, err := models.NormalizeTeamRole(role); err != nil {
		return err
	}

	return db.DB.Transaction(func(tx *gorm.DB) error {
		if models.TeamRoleRank(role) < models.TeamRoleRank(models.TeamRoleManager) {
			if err := ensureNotLastManager(tx, engineerID, teamID); err != nil {
				return err
			}
		}
		return setMemberRole(tx, engineerID, teamID, role)
	})
}

// SetTeamMemberRoleByEmail is SetTeamMemberRole keyed by email; the engineer is
// created on demand (and validated against Feishu) when granting a role, the
// same way manager grants have always worked.
func (s *ScheduleService) SetTeamMemberRoleByEmail(engineerEmail string, teamID uint, role string) error {
	email := normalizeEmail(engineerEmail)
	if err := validateManagedEngineerEmail(email); err != nil {
		return err
	}
	engID, err := s.resolveEngineerID(0, email, true)
	if err != nil {
		return err
	}
	return s.SetTeamMemberRole(engID, teamID, role)
}

// TeamManagerEmails returns a team's manager emails, normalized, de-duplicated
// and sorted. It is the lean form of GetTeamManagers for callers that only need
// addresses (e.g. the current-oncall escalation target).
func (s *ScheduleService) TeamManagerEmails(teamID uint) ([]string, error) {
	if teamID == 0 {
		return nil, errors.New("missing team_id")
	}

	var raw []string
	if err := db.DB.Model(&models.EngineerTeam{}).
		Joins("JOIN engineers ON engineers.id = engineer_teams.engineer_id").
		Where("engineer_teams.team_id = ? AND engineer_teams.role = ?", teamID, models.TeamRoleManager).
		Pluck("engineers.email", &raw).Error; err != nil {
		return nil, err
	}

	seen := make(map[string]struct{}, len(raw))
	out := make([]string, 0, len(raw))
	for _, email := range raw {
		email = normalizeEmail(email)
		if email == "" {
			continue
		}
		if _, ok := seen[email]; ok {
			continue
		}
		seen[email] = struct{}{}
		out = append(out, email)
	}
	sort.Strings(out)
	return out, nil
}

func (s *ScheduleService) GetTeamManagers(teamID uint) ([]map[string]interface{}, error) {
	if teamID == 0 {
		return nil, errors.New("missing team_id")
	}

	resMap := make(map[string]map[string]interface{})

	var links []models.EngineerTeam
	if err := db.DB.Preload("Engineer").Where("team_id = ? AND role = ?", teamID, models.TeamRoleManager).Find(&links).Error; err != nil {
		return nil, err
	}
	for _, link := range links {
		email := normalizeEmail(link.Engineer.Email)
		if email == "" {
			continue
		}
		resMap[email] = map[string]interface{}{
			"engineer_id":    link.Engineer.ID,
			"engineer_name":  link.Engineer.Name,
			"engineer_en":    link.Engineer.EnName,
			"engineer_email": email,
		}
	}

	res := make([]map[string]interface{}, 0, len(resMap))
	for _, item := range resMap {
		res = append(res, item)
	}
	sort.Slice(res, func(i, j int) bool {
		left := normalizeEmail(fmt.Sprintf("%v", res[i]["engineer_email"]))
		right := normalizeEmail(fmt.Sprintf("%v", res[j]["engineer_email"]))
		return left < right
	})
	return res, nil
}

func (s *ScheduleService) RemoveManagerFromTeamByEmail(engineerEmail string, teamID uint) error {
	if teamID == 0 {
		return errors.New("missing team_id")
	}
	email := normalizeEmail(engineerEmail)
	if email == "" {
		return errors.New("missing engineer_email")
	}

	var eng models.Engineer
	if err := db.DB.Where("email = ?", email).First(&eng).Error; err != nil {
		return nil
	}

	return db.DB.Transaction(func(tx *gorm.DB) error {
		var rels []models.EngineerTeam
		if err := tx.Where("team_id = ? AND engineer_id = ?", teamID, eng.ID).Find(&rels).Error; err != nil {
			return err
		}
		if len(rels) == 0 {
			return errors.New("engineer not found in team")
		}

		targetIsManager := false
		for _, rel := range rels {
			if rel.HasRole(models.TeamRoleManager) {
				targetIsManager = true
				break
			}
		}
		if !targetIsManager {
			return errors.New("engineer is not a manager of team")
		}

		if err := ensureNotLastManager(tx, eng.ID, teamID); err != nil {
			return err
		}

		// Revoking manager keeps the person on the team as a plain member.
		return setMemberRole(tx, eng.ID, teamID, models.TeamRoleMember)
	})
}

func (s *ScheduleService) GetCurrentOverview(groupName string) ([]map[string]interface{}, error) {
	var teams []models.Team
	if err := db.DB.Where("is_disabled = ?", false).Find(&teams).Error; err != nil {
		return nil, err
	}
	teams = filterTeamsByGroup(teams, groupName)
	sortTeamsByPriority(teams)
	if len(teams) == 0 {
		return []map[string]interface{}{}, nil
	}

	now := time.Now().Unix()
	ids := make([]uint, 0, len(teams))
	for _, c := range teams {
		ids = append(ids, c.ID)
	}

	primaryRows, err := s.GetEffectiveScheduleV2(ids, now, now, false, false)
	if err != nil {
		return nil, err
	}
	backupRows, err := s.GetEffectiveScheduleV2(ids, now, now, true, false)
	if err != nil {
		return nil, err
	}

	shiftsByTeam := make(map[uint][]map[string]interface{}, len(ids))
	for _, r := range primaryRows {
		cid, _ := r["team_id"].(uint)
		if cid == 0 {
			continue
		}
		shiftsByTeam[cid] = append(shiftsByTeam[cid], map[string]interface{}{
			"engineer_name": r["engineer_name"],
			"en_name":       r["engineer_en_name"],
			"engineer_id":   r["engineer_id"],
			"email":         r["engineer_email"],
		})
	}

	backupByTeam := make(map[uint][]map[string]interface{}, len(ids))
	for _, r := range backupRows {
		cid, _ := r["team_id"].(uint)
		if cid == 0 {
			continue
		}
		backupByTeam[cid] = append(backupByTeam[cid], map[string]interface{}{
			"engineer_name": r["engineer_name"],
			"en_name":       r["engineer_en_name"],
			"engineer_id":   r["engineer_id"],
			"email":         r["engineer_email"],
		})
	}

	res := make([]map[string]interface{}, 0, len(teams))
	for _, cat := range teams {
		displayName := strings.TrimSpace(cat.DisplayName)
		if displayName == "" {
			displayName = cat.Name
		}
		res = append(res, map[string]interface{}{
			"team_id":       cat.ID,
			"team_name":     displayName,
			"name":          cat.Name,
			"display_name":  displayName,
			"description":   cat.Description,
			"group_names":   []string(cat.GroupNames),
			"sort_priority": cat.SortPriority,
			"shifts":        shiftsByTeam[cat.ID],
			"backup":        backupByTeam[cat.ID],
		})
	}
	return res, nil
}

func (s *ScheduleService) ListTeams(groupName string) ([]map[string]interface{}, error) {
	var teams []models.Team
	if err := db.DB.Where("is_disabled = ?", false).Find(&teams).Error; err != nil {
		return nil, err
	}
	teams = filterTeamsByGroup(teams, groupName)
	sortTeamsByPriority(teams)
	out := make([]map[string]interface{}, 0, len(teams))
	for _, c := range teams {
		displayName := strings.TrimSpace(c.DisplayName)
		if displayName == "" {
			displayName = c.Name
		}
		out = append(out, map[string]interface{}{
			"team_id":       c.ID,
			"team_name":     displayName,
			"team_key":      c.Name,
			"display_name":  displayName,
			"description":   c.Description,
			"group_names":   []string(c.GroupNames),
			"sort_priority": c.SortPriority,
		})
	}
	return out, nil
}
