package services

import (
	"agent-desk/internal/models"
	"agent-desk/internal/pkg/config"
	"agent-desk/internal/pkg/constants"
	"agent-desk/internal/pkg/dto"
	"agent-desk/internal/pkg/dto/request"
	"agent-desk/internal/pkg/dto/response"
	"agent-desk/internal/pkg/enums"
	"agent-desk/internal/pkg/errorsx"
	"agent-desk/internal/repositories"
	"crypto/rand"
	"encoding/hex"
	"slices"
	"sort"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/mlogclub/simple/common/strs"
	"github.com/mlogclub/simple/sqls"
	"golang.org/x/crypto/bcrypt"
	"gorm.io/gorm"
)

const (
	authPrincipalContextKey = "authPrincipal"
)

var AuthService = newAuthService()

func newAuthService() *authService {
	return &authService{}
}

type authService struct {
}

func (s *authService) GetAuthPrincipal(ctx *gin.Context) *dto.AuthPrincipal {
	if ctx == nil {
		return nil
	}
	v, _ := ctx.Get(authPrincipalContextKey)
	if principal, ok := v.(*dto.AuthPrincipal); ok {
		return principal
	}
	return nil
}

func (s *authService) setAuthPrincipal(ctx *gin.Context, user *models.User, roles, permissions []string) *dto.AuthPrincipal {
	principal := &dto.AuthPrincipal{
		UserID:      user.ID,
		Username:    user.Username,
		Nickname:    user.Nickname,
		Avatar:      user.Avatar,
		Status:      user.Status,
		Roles:       roles,
		Permissions: permissions,
	}
	ctx.Set(authPrincipalContextKey, principal)
	return principal
}

func (s *authService) RequirePermission(ctx *gin.Context, permission constants.Permission) (principal *dto.AuthPrincipal, err error) {
	if principal = s.GetAuthPrincipal(ctx); principal == nil {
		if principal, err = s.Authenticate(ctx); err != nil {
			return nil, err
		}
	}

	if principal == nil {
		return nil, errorsx.ForbiddenI18n("error.e0225")
	}

	if !s.HasPermission(ctx, permission.Code) {
		return principal, errorsx.ForbiddenI18n("error.e0225")
	}
	return principal, nil
}

func (s *authService) Login(req request.LoginRequest, authCfg config.AuthConfig, clientIP, userAgent string) (*response.LoginResponse, error) {
	username := strings.TrimSpace(req.Username)
	principal := normalizeLoginPrincipal(username)
	password := req.Password
	if username == "" || strings.TrimSpace(password) == "" {
		return nil, errorsx.InvalidParamI18n("error.e0258")
	}

	if s.isCredentialLocked(principal, authCfg) {
		_ = s.createLoginCredentialLog(principal, 0, false, clientIP, userAgent, "credential locked")
		return nil, errorsx.CredentialLockedI18n("error.e0270")
	}

	user := UserService.GetByUsername(username)
	if user == nil || user.Status != enums.StatusOk {
		_ = s.createLoginCredentialLog(principal, 0, false, clientIP, userAgent, "user not found")
		return nil, errorsx.InvalidAccountI18n("error.e0260")
	}
	if strs.IsBlank(user.Password) || bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) != nil {
		_ = s.createLoginCredentialLog(principal, user.ID, false, clientIP, userAgent, "password mismatch")
		return nil, errorsx.InvalidAccountI18n("error.e0260")
	}

	var ret *response.LoginResponse
	if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
		var dbErr error
		ret, dbErr = s.issueTokens(ctx, user, clientIP, userAgent, authCfg)
		if dbErr != nil {
			return dbErr
		}
		if dbErr = repositories.UserRepository.Updates(ctx.Tx, user.ID, map[string]any{
			"last_login_at":    time.Now(),
			"last_login_ip":    clientIP,
			"update_user_id":   user.ID,
			"update_user_name": user.Username,
			"updated_at":       time.Now(),
		}); dbErr != nil {
			return dbErr
		}
		return nil
	}); err != nil {
		return nil, err
	}

	_ = s.createLoginCredentialLog(principal, user.ID, true, clientIP, userAgent, "")
	return ret, nil
}

func (s *authService) Logout(accessToken string) error {
	accessToken = s.extractBearerToken(accessToken)
	now := time.Now()
	if accessToken != "" {
		if session := LoginSessionService.FindOne(sqls.NewCnd().Eq("token", accessToken)); session != nil && session.RevokedAt == nil {
			if err := LoginSessionService.Updates(session.ID, map[string]any{
				"revoked_at": now,
				"updated_at": now,
			}); err != nil {
				return err
			}
		}
	}
	return nil
}

func (s *authService) Authenticate(ctx *gin.Context) (*dto.AuthPrincipal, error) {
	if principal := s.GetAuthPrincipal(ctx); principal != nil {
		return principal, nil
	}

	token := s.extractBearerToken(ctx.GetHeader("Authorization"))
	if token == "" {
		token = strings.TrimSpace(ctx.Query("accessToken"))
	}
	if token == "" {
		return nil, errorsx.UnauthorizedI18n("error.auth.expired")
	}

	session, err := s.validateSessionToken(token)
	if err != nil {
		return nil, err
	}

	user := UserService.Get(session.UserID)
	if user == nil || user.Status != enums.StatusOk {
		return nil, errorsx.UnauthorizedI18n("error.e0256")
	}

	roles, permissions, err := s.loadUserAuthScope(sqls.DB(), user.ID)
	if err != nil {
		return nil, err
	}
	principal := s.setAuthPrincipal(ctx, user, roles, permissions)

	now := time.Now()
	_ = LoginSessionService.Updates(session.ID, map[string]any{
		"last_seen_at": now,
		"updated_at":   now,
	})

	return principal, nil
}

func (s *authService) HasPermission(ctx *gin.Context, permissionCode string) bool {
	principal := s.GetAuthPrincipal(ctx)
	if principal == nil {
		return false
	}
	return slices.Contains(principal.Permissions, permissionCode)
}

func (s *authService) CurrentProfile(ctx *gin.Context) (*response.LoginResponse, error) {
	principal, err := s.Authenticate(ctx)
	if err != nil {
		return nil, err
	}

	return &response.LoginResponse{
		User: &response.AuthUserResponse{
			ID:       principal.UserID,
			Username: principal.Username,
			Nickname: principal.Nickname,
			Avatar:   principal.Avatar,
			Status:   principal.Status,
			Roles:    principal.Roles,
		},
		Permissions: principal.Permissions,
		Roles:       principal.Roles,
	}, nil
}

func (s *authService) GetUserRoles(userID int64) ([]models.Role, error) {
	return s.loadUserRoles(sqls.DB(), userID)
}

func (s *authService) GetUserPermissions(userID int64) ([]string, error) {
	return s.loadUserPermissionCodes(sqls.DB(), userID)
}

func (s *authService) issueTokens(ctx *sqls.TxContext, user *models.User, clientIP, userAgent string, authCfg config.AuthConfig) (*response.LoginResponse, error) {
	roles, permissions, err := s.loadUserAuthScope(ctx.Tx, user.ID)
	if err != nil {
		return nil, err
	}

	tokenTTL := s.resolveTokenTTL(authCfg)
	accessToken, err := randomToken(constants.AuthTokenPrefix)
	if err != nil {
		return nil, err
	}

	now := time.Now()
	if err := repositories.LoginSessionRepository.Create(ctx.Tx, &models.LoginSession{
		UserID:     user.ID,
		Token:      accessToken,
		ClientType: constants.ClientTypeAdminWeb,
		ClientIP:   clientIP,
		UserAgent:  userAgent,
		ExpiredAt:  now.Add(tokenTTL),
		LastSeenAt: &now,
		AuditFields: models.AuditFields{
			CreatedAt:      now,
			CreateUserID:   user.ID,
			CreateUserName: user.Username,
			UpdatedAt:      now,
			UpdateUserID:   user.ID,
			UpdateUserName: user.Username,
		},
	}); err != nil {
		return nil, err
	}

	return &response.LoginResponse{
		AccessToken: accessToken,
		ExpiresAt:   now.Add(tokenTTL).Format(time.DateTime),
		User: &response.AuthUserResponse{
			ID:       user.ID,
			Username: user.Username,
			Nickname: user.Nickname,
			Avatar:   user.Avatar,
			Status:   user.Status,
			Roles:    roles,
		},
		Permissions: permissions,
		Roles:       roles,
	}, nil
}

func (s *authService) resolveTokenTTL(authCfg config.AuthConfig) time.Duration {
	tokenTTL := 12 * time.Hour
	if authCfg.TokenTTLHours > 0 {
		tokenTTL = time.Duration(authCfg.TokenTTLHours) * time.Hour
	}
	return tokenTTL
}

func (s *authService) validateSessionToken(token string) (*models.LoginSession, error) {
	if strings.TrimSpace(token) == "" {
		return nil, errorsx.UnauthorizedI18n("error.auth.expired")
	}
	session := LoginSessionService.FindOne(sqls.NewCnd().Eq("token", token))
	if session == nil {
		return nil, errorsx.InvalidTokenI18n("error.e0269")
	}
	if session.RevokedAt != nil {
		return nil, errorsx.InvalidTokenI18n("error.e0267")
	}
	if time.Now().After(session.ExpiredAt) {
		return nil, errorsx.InvalidTokenI18n("error.e0268")
	}
	return session, nil
}

func (s *authService) loadUserAuthScope(tx *gorm.DB, userID int64) ([]string, []string, error) {
	roleCodes, err := s.loadUserRoleCodes(tx, userID)
	if err != nil {
		return nil, nil, err
	}
	permissionCodes, err := s.loadUserPermissionCodes(tx, userID)
	if err != nil {
		return nil, nil, err
	}
	return roleCodes, permissionCodes, nil
}

func (s *authService) loadUserRoleCodes(tx *gorm.DB, userID int64) ([]string, error) {
	roles, err := s.loadUserRoles(tx, userID)
	if err != nil {
		return nil, err
	}
	roleCodes := make([]string, 0, len(roles))
	for _, role := range roles {
		roleCodes = append(roleCodes, role.Code)
	}
	return roleCodes, nil
}

func (s *authService) loadUserRoles(tx *gorm.DB, userID int64) ([]models.Role, error) {
	roles := make([]models.Role, 0)
	if err := tx.
		Table("t_role AS r").
		Select("r.*").
		Joins("JOIN t_user_role AS ur ON ur.role_id = r.id").
		Where("ur.user_id = ? AND r.status = ?", userID, enums.StatusOk).
		Order("r.sort_no ASC, r.id ASC").
		Scan(&roles).Error; err != nil {
		return nil, err
	}

	return roles, nil
}

func (s *authService) loadUserPermissionCodes(tx *gorm.DB, userID int64) ([]string, error) {
	permissionRows := make([]struct {
		Code string
	}, 0)
	db := tx.Table("t_permission AS p").
		Select("DISTINCT p.code").
		Joins("JOIN t_role_permission AS rp ON rp.permission_id = p.id").
		Joins("JOIN t_user_role AS ur ON ur.role_id = rp.role_id").
		Where("ur.user_id = ?", userID).
		Where("p.status = ?", enums.StatusOk)
	if err := db.Order("p.sort_no ASC, p.id ASC").Scan(&permissionRows).Error; err != nil {
		return nil, err
	}

	permissionCodes := make([]string, 0, len(permissionRows))
	for _, permission := range permissionRows {
		permissionCodes = append(permissionCodes, permission.Code)
	}

	overrideRows := make([]struct {
		Code   string
		Effect int
	}, 0)
	if err := tx.
		Table("t_user_permission AS up").
		Select("p.code, up.effect").
		Joins("JOIN t_permission AS p ON p.id = up.permission_id").
		Where("up.user_id = ? AND (up.expired_at IS NULL OR up.expired_at > ?)", userID, time.Now()).
		Scan(&overrideRows).Error; err != nil {
		return nil, err
	}

	permissionSet := make(map[string]bool, len(permissionCodes))
	for _, code := range permissionCodes {
		permissionSet[code] = true
	}
	for _, override := range overrideRows {
		if override.Effect < 0 {
			delete(permissionSet, override.Code)
			continue
		}
		permissionSet[override.Code] = true
	}

	permissionCodes = permissionCodes[:0]
	for code := range permissionSet {
		permissionCodes = append(permissionCodes, code)
	}
	sort.Strings(permissionCodes)
	return permissionCodes, nil
}

func (s *authService) extractBearerToken(header string) string {
	header = strings.TrimSpace(header)
	if header == "" {
		return ""
	}
	parts := strings.SplitN(header, " ", 2)
	if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
		return ""
	}
	return strings.TrimSpace(parts[1])
}

func (s *authService) createLoginCredentialLog(principal string, userID int64, success bool, clientIP, userAgent, reason string) error {
	return LoginCredentialLogService.Create(&models.LoginCredentialLog{
		Principal: principal,
		UserID:    userID,
		Success:   success,
		ClientIP:  clientIP,
		UserAgent: userAgent,
		Reason:    reason,
		CreatedAt: time.Now(),
	})
}

func (s *authService) isCredentialLocked(principal string, authCfg config.AuthConfig) bool {
	maxFailedAttempts := authCfg.MaxFailedAttempts
	if maxFailedAttempts <= 0 {
		return false
	}
	lockMinute := authCfg.CredentialLockMinute
	if lockMinute <= 0 {
		lockMinute = 15
	}
	since := time.Now().Add(-time.Duration(lockMinute) * time.Minute)
	return LoginCredentialLogService.Count(sqls.NewCnd().
		Eq("principal", normalizeLoginPrincipal(principal)).
		Eq("success", false).
		NotEq("reason", "credential locked").
		Where("created_at >= ?", since)) >= int64(maxFailedAttempts)
}

func normalizeLoginPrincipal(principal string) string {
	return strings.ToLower(strings.TrimSpace(principal))
}

func randomToken(prefix string) (string, error) {
	buf := make([]byte, 24)
	if _, err := rand.Read(buf); err != nil {
		return "", err
	}
	return prefix + hex.EncodeToString(buf), nil
}
