package services

import (
	"agent-desk/internal/models"
	"agent-desk/internal/oidcclient"
	"agent-desk/internal/pkg/config"
	"agent-desk/internal/pkg/dto/response"
	"agent-desk/internal/pkg/enums"
	"agent-desk/internal/pkg/errorsx"
	"agent-desk/internal/repositories"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"strconv"
	"strings"
	"time"
	"unicode"

	"github.com/mlogclub/simple/sqls"
	"gorm.io/gorm"
)

var OIDCLoginService = newOIDCLoginService()

type oidcLoginService struct {
}

type oidcLoginProfile = oidcclient.Profile

func newOIDCLoginService() *oidcLoginService {
	return &oidcLoginService{}
}

func (s *oidcLoginService) BuildOIDCLoginURL(next string) (string, error) {
	return oidcclient.BuildAuthCodeURL(next)
}

func (s *oidcLoginService) LoginByOIDC(ctx context.Context, code, state string, authCfg config.AuthConfig, clientIP, userAgent string) (string, string, error) {
	next, err := oidcclient.ParseState(state)
	if err != nil {
		return "", "", err
	}
	profile, err := oidcclient.ExchangeCode(ctx, code)
	if err != nil {
		return "", "", err
	}
	loginResp, err := s.loginWithOIDCProfile(profile, authCfg, clientIP, userAgent)
	if err != nil {
		return "", "", err
	}
	ticket, err := oidcclient.IssueLoginTicket(loginResp)
	if err != nil {
		return "", "", err
	}
	return ticket, next, nil
}

func (s *oidcLoginService) ExchangeOIDCLoginTicket(ticket string) (*response.LoginResponse, error) {
	return oidcclient.ConsumeLoginTicket(ticket)
}

func (s *oidcLoginService) loginWithOIDCProfile(profile *oidcLoginProfile, authCfg config.AuthConfig, clientIP, userAgent string) (*response.LoginResponse, error) {
	if profile == nil || strings.TrimSpace(profile.Subject) == "" {
		return nil, errorsx.BusinessErrorI18n(2, "error.oidc.profileMissing")
	}

	var ret *response.LoginResponse
	err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
		var (
			identity = repositories.UserIdentityRepository.GetBy(ctx.Tx, enums.ThirdProviderOIDC, "", profile.Subject)
			user     *models.User
			err      error
		)
		if identity == nil {
			user, identity, err = s.createOIDCUser(ctx, profile)
			if err != nil {
				return err
			}
		} else {
			if identity.Status != enums.StatusOk {
				return errorsx.BusinessErrorI18n(3, "error.oidc.bindingDisabled")
			}
			user = repositories.UserRepository.Get(ctx.Tx, identity.UserID)
			if user == nil {
				return errorsx.BusinessErrorI18n(4, "error.oidc.boundUserMissing")
			}
		}

		if user.Status != enums.StatusOk {
			return errorsx.UnauthorizedI18n("error.e0200")
		}

		if err = repositories.UserRepository.Updates(ctx.Tx, user.ID, map[string]any{
			"nickname":         s.resolveOIDCNickname(user.Nickname, profile),
			"avatar":           s.resolveOIDCAvatar(user.Avatar, profile),
			"last_login_at":    time.Now(),
			"last_login_ip":    clientIP,
			"update_user_id":   user.ID,
			"update_user_name": user.Username,
			"updated_at":       time.Now(),
		}); err != nil {
			return err
		}

		if err = repositories.UserIdentityRepository.Updates(ctx.Tx, identity.ID, map[string]any{
			"provider_name":    enums.GetThirdProviderLabel(enums.ThirdProviderOIDC),
			"raw_profile":      profile.RawProfile,
			"last_auth_at":     time.Now(),
			"status":           enums.StatusOk,
			"update_user_id":   user.ID,
			"update_user_name": user.Username,
			"updated_at":       time.Now(),
		}); err != nil {
			return err
		}

		ret, err = AuthService.issueTokens(ctx, user, clientIP, userAgent, authCfg)
		return err
	})
	if err != nil {
		return nil, err
	}
	return ret, nil
}

func (s *oidcLoginService) createOIDCUser(ctx *sqls.TxContext, profile *oidcLoginProfile) (*models.User, *models.UserIdentity, error) {
	now := time.Now()
	email := s.availableEmail(ctx.Tx, profile.Email)
	username := s.availableUsername(ctx.Tx, profile)

	user := &models.User{
		Username:     username,
		Nickname:     s.resolveOIDCNickname("", profile),
		Avatar:       s.resolveOIDCAvatar("", profile),
		Email:        email,
		Password:     "",
		PasswordSalt: "",
		Status:       enums.StatusOk,
		AuditFields: models.AuditFields{
			CreatedAt:      now,
			CreateUserID:   0,
			CreateUserName: enums.GetThirdProviderLabel(enums.ThirdProviderOIDC),
			UpdatedAt:      now,
			UpdateUserID:   0,
			UpdateUserName: enums.GetThirdProviderLabel(enums.ThirdProviderOIDC),
		},
	}
	if err := repositories.UserRepository.Create(ctx.Tx, user); err != nil {
		return nil, nil, err
	}

	identity := &models.UserIdentity{
		UserID:         user.ID,
		Provider:       enums.ThirdProviderOIDC,
		ProviderUserID: strings.TrimSpace(profile.Subject),
		ProviderCorpID: "",
		ProviderName:   enums.GetThirdProviderLabel(enums.ThirdProviderOIDC),
		RawProfile:     profile.RawProfile,
		Status:         enums.StatusOk,
		LastAuthAt:     &now,
		AuditFields: models.AuditFields{
			CreatedAt:      now,
			CreateUserID:   user.ID,
			CreateUserName: user.Username,
			UpdatedAt:      now,
			UpdateUserID:   user.ID,
			UpdateUserName: user.Username,
		},
	}
	if err := repositories.UserIdentityRepository.Create(ctx.Tx, identity); err != nil {
		return nil, nil, err
	}
	return user, identity, nil
}

func (s *oidcLoginService) availableEmail(tx *gorm.DB, email string) *string {
	email = strings.TrimSpace(strings.ToLower(email))
	if email == "" || repositories.UserRepository.GetByEmail(tx, email) != nil {
		return nil
	}
	return &email
}

func (s *oidcLoginService) availableUsername(tx *gorm.DB, profile *oidcLoginProfile) string {
	for _, candidate := range []string{
		profile.PreferredUsername,
		strings.Split(strings.TrimSpace(profile.Email), "@")[0],
	} {
		username := normalizeOIDCUsername(candidate)
		if username != "" && repositories.UserRepository.GetByUsername(tx, username) == nil {
			return username
		}
	}
	base := "oidc_" + shortSubjectHash(profile.Subject)
	if repositories.UserRepository.GetByUsername(tx, base) == nil {
		return base
	}
	for i := 1; i < 100; i++ {
		username := base + "_" + strconv.Itoa(i)
		if repositories.UserRepository.GetByUsername(tx, username) == nil {
			return username
		}
	}
	return base + "_" + shortSubjectHash(time.Now().String())
}

func (s *oidcLoginService) resolveOIDCNickname(current string, profile *oidcLoginProfile) string {
	if profile != nil {
		for _, candidate := range []string{profile.Name, profile.PreferredUsername, profile.Email, profile.Subject} {
			if candidate = strings.TrimSpace(candidate); candidate != "" {
				return candidate
			}
		}
	}
	return strings.TrimSpace(current)
}

func (s *oidcLoginService) resolveOIDCAvatar(current string, profile *oidcLoginProfile) string {
	if profile != nil {
		if picture := strings.TrimSpace(profile.Picture); picture != "" {
			return picture
		}
	}
	return strings.TrimSpace(current)
}

func normalizeOIDCUsername(value string) string {
	value = strings.TrimSpace(strings.ToLower(value))
	var b strings.Builder
	for _, r := range value {
		if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' || r == '.' {
			b.WriteRune(r)
		}
	}
	ret := strings.Trim(b.String(), "._-")
	if len(ret) > 100 {
		ret = ret[:100]
	}
	return ret
}

func shortSubjectHash(subject string) string {
	sum := sha256.Sum256([]byte(strings.TrimSpace(subject)))
	return hex.EncodeToString(sum[:])[:16]
}
