package auth

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"slices"
	"strings"
	"time"

	"github.com/google/uuid"
	"golang.org/x/oauth2"
	"gorm.io/gorm"

	"security-center-backend/internal/config"
	"security-center-backend/internal/model"
	"security-center-backend/internal/pkg/cache"
	"security-center-backend/internal/repository"
	"security-center-backend/internal/service/audit"
	"security-center-backend/internal/service/directory"
	"security-center-backend/internal/service/jwt"
	"security-center-backend/internal/service/oauth"
)

var (
	ErrNotImplemented  = errors.New("not implemented")
	ErrSSODisabled     = errors.New("sso disabled in current environment")
	ErrUserNotFound    = errors.New("user not found")
	ErrInvalidState    = errors.New("invalid oauth state")
	ErrSessionNotFound = errors.New("session not found")
)

const (
	googleUserInfoEndpoint = "https://www.googleapis.com/oauth2/v2/userinfo"
	oauthStateKeyPattern   = "oauth:%s:state:%s"
	refreshTokenKeyPattern = "session:refresh:%s"
	accessTokenKeyPattern  = "session:access:%s"
)

var oauthStateTTL = 10 * time.Minute

type Service struct {
	cfg         config.AuthConfig
	oauthCfg    config.OAuthConfig
	jwtManager  *jwt.Manager
	userRepo    repository.UserRepository
	roleRepo    repository.RoleRepository
	employees   directory.EmployeeDirectory
	oauthClient *oauth.GoogleClient
	auth0Client *oauth.Auth0Client
	provider    string
	cache       *cache.Cache
	audit       *audit.Service
}

func New(
	cfg config.AuthConfig,
	oauthCfg config.OAuthConfig,
	jwtManager *jwt.Manager,
	userRepo repository.UserRepository,
	roleRepo repository.RoleRepository,
	cache *cache.Cache,
	auditLogger *audit.Service,
	oauthClient *oauth.GoogleClient,
	auth0Client *oauth.Auth0Client,
	employeeRepo directory.EmployeeDirectory,
) *Service {
	provider := strings.ToLower(strings.TrimSpace(oauthCfg.Provider))
	if provider == "" {
		provider = "google"
	}
	return &Service{
		cfg:         cfg,
		oauthCfg:    oauthCfg,
		jwtManager:  jwtManager,
		userRepo:    userRepo,
		roleRepo:    roleRepo,
		cache:       cache,
		audit:       auditLogger,
		oauthClient: oauthClient,
		auth0Client: auth0Client,
		provider:    provider,
		employees:   employeeRepo,
	}
}

func (s *Service) GenerateLoginURL(ctx context.Context, provider string) (string, error) {
	if s.cfg.BypassSSO || s.providerBypassEnabled() {
		return "", ErrSSODisabled
	}

	switch s.normalizeProvider(provider) {
	case "auth0":
		return s.generateAuth0AuthURL(ctx)
	default:
		return s.generateGoogleAuthURL(ctx)
	}
}

func (s *Service) HandleCallback(ctx context.Context, provider string, code string, state string) (*Session, error) {
	if s.cfg.BypassSSO || s.providerBypassEnabled() {
		return s.generateDevSession(ctx)
	}

	switch s.normalizeProvider(provider) {
	case "auth0":
		return s.handleAuth0Callback(ctx, code, state)
	default:
		return s.handleGoogleCallback(ctx, code, state)
	}
}

func (s *Service) GenerateGoogleAuthURL(ctx context.Context) (string, error) {
	return s.GenerateLoginURL(ctx, "google")
}

func (s *Service) HandleGoogleCallback(ctx context.Context, code string, state string) (*Session, error) {
	return s.HandleCallback(ctx, "google", code, state)
}

func (s *Service) GenerateAuth0AuthURL(ctx context.Context) (string, error) {
	return s.GenerateLoginURL(ctx, "auth0")
}

func (s *Service) HandleAuth0Callback(ctx context.Context, code string, state string) (*Session, error) {
	return s.HandleCallback(ctx, "auth0", code, state)
}

func (s *Service) generateGoogleAuthURL(ctx context.Context) (string, error) {
	if s.oauthClient == nil {
		return "", ErrNotImplemented
	}
	state := uuid.NewString()
	if err := s.storeOAuthState(ctx, "google", state); err != nil {
		return "", err
	}
	return s.oauthClient.AuthCodeURL(state)
}

func (s *Service) generateAuth0AuthURL(ctx context.Context) (string, error) {
	if s.auth0Client == nil {
		return "", ErrNotImplemented
	}
	state := uuid.NewString()
	if err := s.storeOAuthState(ctx, "auth0", state); err != nil {
		return "", err
	}
	return s.auth0Client.AuthCodeURL(state)
}

func (s *Service) handleGoogleCallback(ctx context.Context, code string, state string) (*Session, error) {
	if s.oauthClient == nil {
		return nil, ErrNotImplemented
	}
	if code == "" {
		return nil, errors.New("missing authorization code")
	}
	if err := s.validateOAuthState(ctx, "google", state); err != nil {
		return nil, err
	}

	token, err := s.oauthClient.Exchange(ctx, code)
	if err != nil {
		return nil, fmt.Errorf("exchange code: %w", err)
	}
	profile, err := fetchGoogleUserInfo(ctx, token)
	if err != nil {
		return nil, fmt.Errorf("fetch user info: %w", err)
	}

	user, err := s.upsertGoogleUser(ctx, profile)
	if err != nil {
		return nil, fmt.Errorf("persist user: %w", err)
	}

	session, err := s.issueSession(user)
	if err != nil {
		return nil, err
	}
	if err := s.persistSession(ctx, session); err != nil {
		return nil, err
	}
	return session, nil
}

func (s *Service) handleAuth0Callback(ctx context.Context, code string, state string) (*Session, error) {
	if s.auth0Client == nil {
		return nil, ErrNotImplemented
	}
	if code == "" {
		return nil, errors.New("missing authorization code")
	}
	if err := s.validateOAuthState(ctx, "auth0", state); err != nil {
		return nil, err
	}

	token, err := s.auth0Client.Exchange(ctx, code)
	if err != nil {
		return nil, fmt.Errorf("exchange code: %w", err)
	}
	profile, err := s.auth0Client.UserInfo(ctx, token)
	if err != nil {
		return nil, fmt.Errorf("fetch auth0 user info: %w", err)
	}

	user, err := s.upsertAuth0User(ctx, profile)
	if err != nil {
		return nil, fmt.Errorf("persist user: %w", err)
	}

	session, err := s.issueSession(user)
	if err != nil {
		return nil, err
	}
	if err := s.persistSession(ctx, session); err != nil {
		return nil, err
	}
	return session, nil
}

func (s *Service) RefreshAccessToken(ctx context.Context, refreshToken string) (*TokenPair, error) {
	if refreshToken == "" {
		return nil, errors.New("missing refresh token")
	}

	if s.cfg.BypassSSO || s.providerBypassEnabled() {
		session, err := s.generateDevSession(ctx)
		if err != nil {
			return nil, err
		}
		_ = s.removeRefreshSession(ctx, refreshToken)
		return &TokenPair{
			AccessToken:  session.AccessToken,
			RefreshToken: session.RefreshToken,
			ExpiresIn:    session.ExpiresIn,
		}, nil
	}

	if s.jwtManager == nil {
		return nil, ErrNotImplemented
	}
	if err := s.ensureRefreshSession(ctx, refreshToken); err != nil {
		return nil, err
	}

	claims, err := s.jwtManager.ValidateRefreshToken(refreshToken)
	if err != nil {
		return nil, err
	}

	user, err := s.lookupUserByID(ctx, claims.UserID)
	if err != nil {
		return nil, err
	}

	session, err := s.issueSession(user)
	if err != nil {
		return nil, err
	}
	if err := s.persistSession(ctx, session); err != nil {
		return nil, err
	}
	_ = s.removeRefreshSession(ctx, refreshToken)

	return &TokenPair{
		AccessToken:  session.AccessToken,
		RefreshToken: session.RefreshToken,
		ExpiresIn:    session.ExpiresIn,
	}, nil
}

func (s *Service) Logout(ctx context.Context, accessToken string) error {
	if accessToken == "" || s.cache == nil {
		return nil
	}
	key := accessTokenCacheKey(accessToken)
	refreshKey, err := s.cache.Get(ctx, key)
	if err != nil {
		return err
	}
	if refreshKey != "" {
		if err := s.cache.Delete(ctx, refreshKey); err != nil {
			return err
		}
	}
	return s.cache.Delete(ctx, key)
}

func (s *Service) GenerateDevSession(ctx context.Context) (*Session, error) {
	return s.generateDevSession(ctx)
}

func (s *Service) GetCurrentUserProfile(ctx context.Context, userID uint64) (*SessionUser, error) {
	if userID == 0 {
		return &SessionUser{
			ID:                 fallbackString(s.cfg.DevUser.ID, "dev-user"),
			Email:              fallbackString(s.cfg.DevUser.Email, "dev.user@example.com"),
			Name:               fallbackString(s.cfg.DevUser.Name, "Development User"),
			Roles:              fallbackRoles(s.cfg.DevUser.Roles),
			Permissions:        devDefaultPermissions(),
			Status:             model.UserStatusActive,
			EmployeeLinkStatus: model.EmployeeLinkStatusPending,
		}, nil
	}

	user, err := s.lookupUserByID(ctx, userID)
	if err != nil {
		return nil, err
	}
	if repaired, repairErr := s.repairUserEmployeeLink(ctx, user); repairErr != nil {
		return nil, repairErr
	} else if repaired {
		roles := extractRoleNames(user)
		permissions := extractPermissionNames(user, roles)
		return &SessionUser{
			ID:                 fmt.Sprintf("%d", user.ID),
			Email:              user.Email,
			Name:               user.Name,
			Roles:              roles,
			Permissions:        permissions,
			Avatar:             user.AvatarURL,
			Status:             user.Status,
			EmployeeID:         user.EmployeeID,
			EmployeeLinkStatus: user.EmployeeLinkStatus,
		}, nil
	}

	roles := extractRoleNames(user)
	permissions := extractPermissionNames(user, roles)

	return &SessionUser{
		ID:                 fmt.Sprintf("%d", user.ID),
		Email:              user.Email,
		Name:               user.Name,
		Roles:              roles,
		Permissions:        permissions,
		Avatar:             user.AvatarURL,
		Status:             user.Status,
		EmployeeID:         user.EmployeeID,
		EmployeeLinkStatus: user.EmployeeLinkStatus,
	}, nil
}

func (s *Service) IsSSOEnabled() bool {
	return s.oauthCfg.Enabled && !s.cfg.BypassSSO && !s.providerBypassEnabled()
}

// Helpers

func devDefaultPermissions() []string {
	return []string{
		"auth:manage_users",
		"system:manage_settings",
		"caasm:view_assets",
		"caasm:manage_assets",
		"caasm:manage_api_assets",
		"caasm:export_ports",
		"scan:view_batches",
		"scan:create_batches",
		"scan:manage_batches",
		"scan:view_evidence",
		"vuln:view_findings",
		"vuln:manage_findings",
		"vuln:view_sensitive_details",
		"vuln:edit_sensitive_details",
		"vuln:edit_details",
		"vuln:authorize_viewers",
		"vuln:assign_owner",
		"vuln:request_retest",
		"vuln:close_findings",
		"audit:view_logs",
	}
}

func (s *Service) generateDevSession(ctx context.Context) (*Session, error) {
	// Back the dev user by a persisted record when the DB is available so foreign keys
	// (e.g., sync_jobs.triggered_by) don't reference a non-existent user ID.
	user := &model.User{
		Base:             model.Base{ID: 0},
		ExternalID:       fallbackString(s.cfg.DevUser.ID, "dev-user"),
		Email:            fallbackString(s.cfg.DevUser.Email, "dev.user@example.com"),
		Name:             fallbackString(s.cfg.DevUser.Name, "Development User"),
		AvatarURL:        "",
		Status:           model.UserStatusActive,
		IdentityProvider: "dev",
	}

	if s.userRepo != nil {
		existing, err := s.userRepo.FindByEmail(ctx, user.Email)
		switch {
		case err == nil && existing != nil:
			user = existing
			user.Status = model.UserStatusActive
		case errors.Is(err, gorm.ErrRecordNotFound):
			if err := s.userRepo.CreateOrUpdate(ctx, user); err != nil {
				return nil, err
			}
		case err != nil:
			return nil, err
		}
	}

	// Ensure the session has the configured roles even if the DB record lacks them.
	if len(user.Roles) == 0 {
		for _, role := range fallbackRoles(s.cfg.DevUser.Roles) {
			user.Roles = append(user.Roles, model.Role{Name: role})
		}
	}
	if s.userRepo != nil && user.ID == 0 {
		return nil, errors.New("dev user has no persisted database id")
	}

	// Persist the configured dev roles (best-effort): without this, the roles only
	// existed in-memory for the freshly issued session, and later /auth/me or token
	// refresh lookups (which preload roles from user_roles) degraded the dev user
	// to "viewer" — admin menus disappeared and refreshed tokens lost admin rights.
	if s.roleRepo != nil && s.userRepo != nil {
		for _, roleName := range fallbackRoles(s.cfg.DevUser.Roles) {
			if _, err := s.roleRepo.FindByName(ctx, roleName); err != nil {
				// Fresh dev databases may not have the role row yet; create it
				// (unique races are fine — ensureRoleAssigned re-checks by name).
				_ = s.roleRepo.Create(ctx, &model.Role{Name: roleName, Description: "dev role (auto-created)"})
			}
			s.ensureRoleAssigned(ctx, user, roleName)
		}
	}

	session, err := s.issueSession(user)
	if err != nil {
		return nil, err
	}
	sessionUserID := session.User.ID
	if s.userRepo == nil {
		sessionUserID = fallbackString(s.cfg.DevUser.ID, "dev-user")
	}
	session.User = SessionUser{
		ID:                 sessionUserID,
		Email:              user.Email,
		Name:               user.Name,
		Roles:              session.User.Roles,
		Permissions:        session.User.Permissions,
		Avatar:             user.AvatarURL,
		Status:             user.Status,
		EmployeeID:         user.EmployeeID,
		EmployeeLinkStatus: user.EmployeeLinkStatus,
	}
	if err := s.persistSession(ctx, session); err != nil {
		return nil, err
	}
	return session, nil
}

func (s *Service) issueSession(user *model.User) (*Session, error) {
	if s.jwtManager == nil {
		return nil, ErrNotImplemented
	}
	roles := extractRoleNames(user)
	permissions := extractPermissionNames(user, roles)
	accessToken, err := s.jwtManager.GenerateAccessToken(user.ID, user.Email, roles, permissions)
	if err != nil {
		return nil, err
	}
	refreshToken, err := s.jwtManager.GenerateRefreshToken(user.ID)
	if err != nil {
		return nil, err
	}
	return &Session{
		User: SessionUser{
			ID:                 fmt.Sprintf("%d", user.ID),
			Email:              user.Email,
			Name:               user.Name,
			Roles:              roles,
			Permissions:        permissions,
			Avatar:             user.AvatarURL,
			Status:             user.Status,
			EmployeeID:         user.EmployeeID,
			EmployeeLinkStatus: user.EmployeeLinkStatus,
		},
		AccessToken:  accessToken,
		RefreshToken: refreshToken,
		ExpiresIn:    int64(s.jwtManager.AccessTTL().Seconds()),
	}, nil
}

func (s *Service) repairUserEmployeeLink(ctx context.Context, user *model.User) (bool, error) {
	if user == nil || s.userRepo == nil || s.employees == nil {
		return false, nil
	}
	email := strings.TrimSpace(user.Email)
	if email == "" {
		return false, nil
	}

	originalEmployeeID := user.EmployeeID
	originalStatus := user.EmployeeLinkStatus
	originalName := user.Name
	originalAvatar := user.AvatarURL

	employee, err := s.employees.FindByEmail(ctx, email)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return false, nil
		}
		return false, err
	}
	if employee == nil {
		return false, nil
	}

	user.EmployeeID = &employee.ID
	user.EmployeeLinkStatus = model.EmployeeLinkStatusLinked
	if employee.Name != "" {
		user.Name = employee.Name
	}
	if employee.AvatarURL != "" {
		user.AvatarURL = employee.AvatarURL
	}

	changed := originalEmployeeID == nil || *originalEmployeeID != employee.ID ||
		originalStatus != model.EmployeeLinkStatusLinked ||
		originalName != user.Name ||
		originalAvatar != user.AvatarURL
	if !changed {
		return false, nil
	}

	if err := s.userRepo.CreateOrUpdate(ctx, user); err != nil {
		return false, err
	}
	s.applyEmployeeRoleHints(ctx, user, employee)
	s.ensureDefaultRole(ctx, user)
	return true, nil
}

func extractPermissionNames(user *model.User, roles []string) []string {
	permsMap := map[string]struct{}{}
	for _, role := range user.Roles {
		for _, perm := range role.Permissions {
			if perm.Name == "" {
				continue
			}
			permsMap[perm.Name] = struct{}{}
		}
	}
	if len(permsMap) == 0 {
		for _, role := range roles {
			for _, perm := range fallbackPermissions(role) {
				permsMap[perm] = struct{}{}
			}
		}
	}
	perms := make([]string, 0, len(permsMap))
	for name := range permsMap {
		perms = append(perms, name)
	}
	slices.Sort(perms)
	return perms
}

func fallbackPermissions(role string) []string {
	switch role {
	case "admin":
		return []string{
			"auth:manage_users",
			"system:manage_settings",
			"caasm:view_assets",
			"caasm:manage_assets",
			"caasm:manage_api_assets",
			"caasm:export_ports",
			"scan:view_batches",
			"scan:create_batches",
			"scan:manage_batches",
			"scan:view_evidence",
			"vuln:view_findings",
			"vuln:manage_findings",
			"vuln:submit_findings",
			"vuln:view_sensitive_details",
			"vuln:edit_sensitive_details",
			"vuln:edit_details",
			"vuln:authorize_viewers",
			"vuln:assign_owner",
			"vuln:request_retest",
			"vuln:close_findings",
			"audit:view_logs",
		}
	case "security_operator":
		return []string{
			"caasm:view_assets",
			"caasm:manage_assets",
			"caasm:manage_api_assets",
			"caasm:export_ports",
			"scan:view_batches",
			"scan:create_batches",
			"scan:manage_batches",
			"scan:view_evidence",
			"vuln:view_findings",
			"vuln:manage_findings",
			"vuln:submit_findings",
			"vuln:view_sensitive_details",
			"vuln:edit_sensitive_details",
			"vuln:authorize_viewers",
			"vuln:assign_owner",
			"vuln:request_retest",
			"vuln:close_findings",
			"audit:view_logs",
		}
	case "viewer":
		return []string{
			"caasm:view_assets",
			"vuln:view_findings",
			"audit:view_logs",
		}
	default:
		return []string{"caasm:view_assets"}
	}
}

func (s *Service) upsertGoogleUser(ctx context.Context, profile *googleUserInfo) (*model.User, error) {
	if s.userRepo == nil {
		return &model.User{
			Base:             model.Base{ID: 0},
			ExternalID:       profile.ID,
			Email:            profile.Email,
			Name:             profile.Name,
			AvatarURL:        profile.Picture,
			Status:           model.UserStatusActive,
			IdentityProvider: "google",
			MigratedToAuth0:  false,
			MigrationDate:    nil,
			Auth0UserID:      "",
		}, nil
	}

	var user *model.User
	if profile.ID != "" {
		if existing, err := s.userRepo.FindByExternalID(ctx, profile.ID); err == nil {
			user = existing
		} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, err
		}
	}
	if user == nil && profile.Email != "" {
		if existing, err := s.userRepo.FindByEmail(ctx, profile.Email); err == nil {
			user = existing
		} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, err
		}
	}
	if user == nil {
		user = &model.User{
			ExternalID:       profile.ID,
			Email:            profile.Email,
			Name:             profile.Name,
			AvatarURL:        profile.Picture,
			Status:           model.UserStatusActive,
			IdentityProvider: "google",
		}
	}

	user.ExternalID = profile.ID
	user.Email = profile.Email
	user.Name = profile.Name
	user.AvatarURL = profile.Picture
	user.Status = model.UserStatusActive
	user.IdentityProvider = "google"
	now := time.Now()
	firstLogin := user.FirstLoginAt == nil
	if firstLogin {
		user.FirstLoginAt = &now
	}
	user.LastLoginAt = &now

	var linkedEmployee *directory.Employee
	if s.employees != nil && user.Email != "" {
		employee, err := s.employees.FindByEmail(ctx, user.Email)
		if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, err
		}
		if err == nil && employee != nil {
			linkedEmployee = employee
			user.EmployeeID = &employee.ID
			if employee.Name != "" {
				user.Name = employee.Name
			}
			if employee.AvatarURL != "" {
				user.AvatarURL = employee.AvatarURL
			}
		}
	}
	if user.EmployeeID != nil {
		user.EmployeeLinkStatus = model.EmployeeLinkStatusLinked
	} else if linkedEmployee != nil {
		user.EmployeeLinkStatus = model.EmployeeLinkStatusLinked
	} else if firstLogin && user.EmployeeLinkStatus != model.EmployeeLinkStatusLinked {
		user.EmployeeLinkStatus = model.EmployeeLinkStatusFailed
	} else if user.EmployeeLinkStatus == "" {
		user.EmployeeLinkStatus = model.EmployeeLinkStatusPending
	}

	if err := s.userRepo.CreateOrUpdate(ctx, user); err != nil {
		return nil, err
	}
	if linkedEmployee != nil {
		s.applyEmployeeRoleHints(ctx, user, linkedEmployee)
	}
	if firstLogin {
		// 仅新用户首登开通时授予 agent_user（W0-①=b 决策）；存量用户不追溯。
		s.ensureRoleAssigned(ctx, user, "agent_user")
	}
	s.ensureDefaultRole(ctx, user)
	return user, nil
}

func (s *Service) upsertAuth0User(ctx context.Context, profile *oauth.Auth0UserInfo) (*model.User, error) {
	if s.userRepo == nil {
		now := time.Now()
		return &model.User{
			Base:             model.Base{ID: 0},
			ExternalID:       profile.Sub,
			Auth0UserID:      profile.Sub,
			Email:            profile.Email,
			Name:             profile.Name,
			AvatarURL:        profile.Picture,
			Status:           model.UserStatusActive,
			IdentityProvider: "auth0",
			MigratedToAuth0:  true,
			MigrationDate:    &now,
			LastLoginAt:      &now,
		}, nil
	}

	var user *model.User
	if profile.Sub != "" {
		if existing, err := s.userRepo.FindByAuth0ID(ctx, profile.Sub); err == nil {
			user = existing
		} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, err
		}
	}
	if user == nil && profile.Email != "" {
		if existing, err := s.userRepo.FindByEmail(ctx, profile.Email); err == nil {
			user = existing
		} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, err
		}
	}
	if user == nil && profile.Sub != "" {
		if existing, err := s.userRepo.FindByExternalID(ctx, profile.Sub); err == nil {
			user = existing
		} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, err
		}
	}
	if user == nil {
		user = &model.User{
			ExternalID:       profile.Sub,
			Auth0UserID:      profile.Sub,
			Email:            profile.Email,
			Name:             profile.Name,
			AvatarURL:        profile.Picture,
			Status:           model.UserStatusActive,
			IdentityProvider: "auth0",
			MigratedToAuth0:  true,
		}
	}

	now := time.Now()
	if profile.Sub != "" {
		user.ExternalID = profile.Sub
		user.Auth0UserID = profile.Sub
	}
	if profile.Email != "" {
		user.Email = profile.Email
	}
	if profile.Name != "" {
		user.Name = profile.Name
	}
	if profile.Picture != "" {
		user.AvatarURL = profile.Picture
	}
	user.Status = model.UserStatusActive
	user.IdentityProvider = "auth0"
	if !user.MigratedToAuth0 {
		user.MigratedToAuth0 = true
		user.MigrationDate = &now
	} else if user.MigrationDate == nil {
		user.MigrationDate = &now
	}
	firstLogin := user.FirstLoginAt == nil
	if firstLogin {
		user.FirstLoginAt = &now
	}
	user.LastLoginAt = &now

	var linkedEmployee *directory.Employee
	if s.employees != nil && user.Email != "" {
		employee, err := s.employees.FindByEmail(ctx, user.Email)
		if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, err
		}
		if err == nil && employee != nil {
			linkedEmployee = employee
			user.EmployeeID = &employee.ID
			if employee.Name != "" {
				user.Name = employee.Name
			}
			if employee.AvatarURL != "" {
				user.AvatarURL = employee.AvatarURL
			}
		}
	}
	if user.EmployeeID != nil {
		user.EmployeeLinkStatus = model.EmployeeLinkStatusLinked
	} else if linkedEmployee != nil {
		user.EmployeeLinkStatus = model.EmployeeLinkStatusLinked
	} else if firstLogin && user.EmployeeLinkStatus != model.EmployeeLinkStatusLinked {
		user.EmployeeLinkStatus = model.EmployeeLinkStatusFailed
	} else if user.EmployeeLinkStatus == "" {
		user.EmployeeLinkStatus = model.EmployeeLinkStatusPending
	}

	if err := s.userRepo.CreateOrUpdate(ctx, user); err != nil {
		return nil, err
	}
	if linkedEmployee != nil {
		s.applyEmployeeRoleHints(ctx, user, linkedEmployee)
	}
	if firstLogin {
		// 仅新用户首登开通时授予 agent_user（W0-①=b 决策）；存量用户不追溯。
		s.ensureRoleAssigned(ctx, user, "agent_user")
	}
	s.ensureDefaultRole(ctx, user)
	return user, nil
}

func (s *Service) ensureDefaultRole(ctx context.Context, user *model.User) {
	if s.roleRepo == nil || s.userRepo == nil || user == nil {
		return
	}
	roles, _ := s.userRepo.GetUserRoles(ctx, user.ID)
	user.Roles = roles
	if len(roles) == 0 {
		if defaultRole, err := s.resolveDefaultRole(ctx); err == nil && defaultRole != nil {
			_ = s.userRepo.AssignRoles(ctx, user.ID, []uint64{defaultRole.ID})
			user.Roles = []model.Role{*defaultRole}
			user.DefaultRoleID = &defaultRole.ID
			_ = s.userRepo.UpdateDefaultRole(ctx, user.ID, user.DefaultRoleID)
		}
		return
	}
	if user.DefaultRoleID == nil {
		roleID := roles[0].ID
		user.DefaultRoleID = &roleID
		_ = s.userRepo.UpdateDefaultRole(ctx, user.ID, user.DefaultRoleID)
	}
}

func (s *Service) applyEmployeeRoleHints(ctx context.Context, user *model.User, employee *directory.Employee) {
	if user == nil || employee == nil {
		return
	}
	title := strings.ToLower(strings.TrimSpace(employee.JobTitle))
	if strings.Contains(title, "security") {
		s.ensureRoleAssigned(ctx, user, "security_operator")
	}
}

func (s *Service) ensureRoleAssigned(ctx context.Context, user *model.User, roleName string) {
	if s.roleRepo == nil || s.userRepo == nil || user == nil || roleName == "" {
		return
	}
	role, err := s.roleRepo.FindByName(ctx, roleName)
	if err != nil || role == nil {
		return
	}
	roles, err := s.userRepo.GetUserRoles(ctx, user.ID)
	if err != nil {
		return
	}
	for _, existing := range roles {
		if existing.ID == role.ID {
			return
		}
	}
	roleIDs := make([]uint64, 0, len(roles)+1)
	for _, existing := range roles {
		roleIDs = append(roleIDs, existing.ID)
	}
	roleIDs = append(roleIDs, role.ID)
	_ = s.userRepo.AssignRoles(ctx, user.ID, roleIDs)
}

func (s *Service) resolveDefaultRole(ctx context.Context) (*model.Role, error) {
	if s.roleRepo == nil {
		return nil, errors.New("role repository not configured")
	}
	role, err := s.roleRepo.FindDefault(ctx)
	if err == nil {
		return role, nil
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}
	role, err = s.roleRepo.FindByName(ctx, "viewer")
	if err != nil {
		return nil, err
	}
	return role, nil
}

func (s *Service) lookupUserByID(ctx context.Context, id uint64) (*model.User, error) {
	if s.userRepo == nil {
		return nil, ErrUserNotFound
	}
	return s.userRepo.FindByID(ctx, id)
}

func extractRoleNames(user *model.User) []string {
	roles := make([]string, 0, len(user.Roles))
	for _, role := range user.Roles {
		if role.Name != "" {
			roles = append(roles, role.Name)
		}
	}
	if len(roles) == 0 {
		roles = append(roles, "viewer")
	}
	return roles
}

func fallbackString(value string, fallback string) string {
	if value == "" {
		return fallback
	}
	return value
}

func fallbackRoles(roles []string) []string {
	if len(roles) == 0 {
		return []string{"admin"}
	}
	return roles
}

func (s *Service) normalizeProvider(provider string) string {
	value := strings.ToLower(strings.TrimSpace(provider))
	if value == "" {
		value = s.provider
	}
	if value != "auth0" {
		return "google"
	}
	return value
}

func (s *Service) providerBypassEnabled() bool {
	return s.oauthCfg.BypassProvider || s.oauthCfg.BypassGoogle
}

func (s *Service) storeOAuthState(ctx context.Context, provider string, state string) error {
	if s.cache == nil || state == "" {
		return nil
	}
	return s.cache.Set(ctx, oauthStateCacheKey(s.normalizeProvider(provider), state), "1", oauthStateTTL)
}

func (s *Service) validateOAuthState(ctx context.Context, provider string, state string) error {
	if state == "" {
		return ErrInvalidState
	}
	if s.cache == nil {
		return nil
	}
	key := oauthStateCacheKey(s.normalizeProvider(provider), state)
	val, err := s.cache.Get(ctx, key)
	if err != nil {
		return err
	}
	if val == "" {
		return ErrInvalidState
	}
	return s.cache.Delete(ctx, key)
}

func (s *Service) persistSession(ctx context.Context, session *Session) error {
	if s.cache == nil || session == nil || s.jwtManager == nil {
		return nil
	}
	record := sessionRecord{
		UserID:      session.User.ID,
		Email:       session.User.Email,
		Roles:       session.User.Roles,
		Permissions: session.User.Permissions,
	}
	payload, err := json.Marshal(record)
	if err != nil {
		return err
	}
	refreshKey := refreshTokenCacheKey(session.RefreshToken)
	if err := s.cache.Set(ctx, refreshKey, payload, s.jwtManager.RefreshTTL()); err != nil {
		return err
	}
	return s.cache.Set(ctx, accessTokenCacheKey(session.AccessToken), refreshKey, s.jwtManager.AccessTTL())
}

func (s *Service) ensureRefreshSession(ctx context.Context, refreshToken string) error {
	if s.cache == nil {
		return nil
	}
	if refreshToken == "" {
		return ErrSessionNotFound
	}
	val, err := s.cache.Get(ctx, refreshTokenCacheKey(refreshToken))
	if err != nil {
		return err
	}
	if val == "" {
		return ErrSessionNotFound
	}
	return nil
}

func (s *Service) removeRefreshSession(ctx context.Context, refreshToken string) error {
	if s.cache == nil || refreshToken == "" {
		return nil
	}
	return s.cache.Delete(ctx, refreshTokenCacheKey(refreshToken))
}

type sessionRecord struct {
	UserID      string   `json:"user_id"`
	Email       string   `json:"email"`
	Roles       []string `json:"roles"`
	Permissions []string `json:"permissions"`
}

func oauthStateCacheKey(provider string, state string) string {
	p := strings.TrimSpace(provider)
	if p == "" {
		p = "google"
	}
	return fmt.Sprintf(oauthStateKeyPattern, p, state)
}

func refreshTokenCacheKey(token string) string {
	return fmt.Sprintf(refreshTokenKeyPattern, hashToken(token))
}

func accessTokenCacheKey(token string) string {
	return fmt.Sprintf(accessTokenKeyPattern, hashToken(token))
}

func hashToken(token string) string {
	if token == "" {
		return ""
	}
	sum := sha256.Sum256([]byte(token))
	return hex.EncodeToString(sum[:])
}

type googleUserInfo struct {
	ID      string `json:"id"`
	Email   string `json:"email"`
	Name    string `json:"name"`
	Picture string `json:"picture"`
}

func fetchGoogleUserInfo(ctx context.Context, token *oauth2.Token) (*googleUserInfo, error) {
	client := oauth2.NewClient(ctx, oauth2.StaticTokenSource(token))
	resp, err := client.Get(googleUserInfoEndpoint)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("google userinfo status %d", resp.StatusCode)
	}
	var profile googleUserInfo
	if err := json.NewDecoder(resp.Body).Decode(&profile); err != nil {
		return nil, err
	}
	return &profile, nil
}

type Session struct {
	User         SessionUser `json:"user"`
	AccessToken  string      `json:"access_token"`
	RefreshToken string      `json:"refresh_token"`
	ExpiresIn    int64       `json:"expires_in"`
}

type SessionUser struct {
	ID                 string                   `json:"id"`
	Email              string                   `json:"email"`
	Name               string                   `json:"name"`
	Roles              []string                 `json:"roles"`
	Permissions        []string                 `json:"permissions"`
	Avatar             string                   `json:"avatar,omitempty"`
	Status             model.UserStatus         `json:"status,omitempty"`
	EmployeeID         *uint64                  `json:"employee_id,omitempty"`
	EmployeeLinkStatus model.EmployeeLinkStatus `json:"employee_link_status,omitempty"`
}

type TokenPair struct {
	AccessToken  string `json:"access_token"`
	RefreshToken string `json:"refresh_token"`
	ExpiresIn    int64  `json:"expires_in"`
}
