package auth

import (
	"context"
	"strings"
	"time"

	errs "gateway/internal/library/errors"
	"gateway/internal/library/errors/code"
	"gateway/internal/library/zitadel"
	domauth "gateway/internal/model/auth/domain/usecase"
	memberdom "gateway/internal/model/member/domain"
	memberenum "gateway/internal/model/member/domain/enum"
	dommember "gateway/internal/model/member/domain/usecase"
	"gateway/internal/svc"
	"gateway/internal/types"
)

func issueAuthToken(ctx context.Context, sc *svc.ServiceContext, tenantID, uid string) (*types.AuthTokenData, error) {
	if sc.AuthToken == nil {
		return nil, errb.SysNotImplemented("auth token not configured")
	}
	pair, err := sc.AuthToken.IssuePair(ctx, &domauth.IssuePairRequest{
		TenantID: tenantID,
		UID:      uid,
	})
	if err != nil {
		return nil, err
	}
	return &types.AuthTokenData{
		AccessToken:  pair.AccessToken,
		RefreshToken: pair.RefreshToken,
		ExpiresIn:    pair.ExpiresIn,
		UID:          uid,
		TokenType:    pair.TokenType,
	}, nil
}

func tokenDataFromRefresh(ctx context.Context, sc *svc.ServiceContext, refreshToken string) (*types.AuthTokenData, error) {
	if sc.AuthToken == nil {
		return nil, errb.SysNotImplemented("auth token not configured")
	}
	pair, err := sc.AuthToken.Refresh(ctx, refreshToken)
	if err != nil {
		return nil, err
	}
	claims, err := sc.AuthToken.ParseAccessToken(ctx, pair.AccessToken)
	if err != nil {
		return nil, err
	}
	return &types.AuthTokenData{
		AccessToken:  pair.AccessToken,
		RefreshToken: pair.RefreshToken,
		ExpiresIn:    pair.ExpiresIn,
		UID:          claims.UID,
		TokenType:    pair.TokenType,
	}, nil
}

func zitadelIdentityFromToken(ctx context.Context, client *zitadel.Client, tok *zitadel.TokenResult) (*zitadel.IDTokenClaims, error) {
	if tok == nil {
		return nil, errb.SvcThirdParty("empty token result")
	}
	if tok.Subject != "" {
		return &zitadel.IDTokenClaims{
			Sub:   tok.Subject,
			Email: tok.Email,
		}, nil
	}
	if tok.IDToken != "" {
		claims, err := zitadel.ParseIDTokenClaims(tok.IDToken)
		if err != nil {
			return nil, errb.SvcThirdParty("parse id_token failed").WithCause(err)
		}
		return claims, nil
	}
	claims, err := client.FetchUserInfo(ctx, tok.AccessToken)
	if err != nil {
		return nil, wrapZitadelErr(err)
	}
	return claims, nil
}

func memberForLogin(ctx context.Context, sc *svc.ServiceContext, tenantID, zitadelSub string) (*dommember.MemberDTO, error) {
	if sc.MemberProfile == nil {
		return nil, errb.SysNotImplemented("member profile not configured")
	}
	dto, err := sc.MemberProfile.GetByZitadelUserID(ctx, tenantID, zitadelSub)
	if err != nil {
		if e := errs.FromError(err); e != nil && e.Category() == code.ResNotFound {
			return nil, errb.AuthUnauthorized("invalid credentials").WithCause(memberdom.ErrNotFound)
		}
		return nil, err
	}
	if err := ensureLoginEligible(dto.Status); err != nil {
		return nil, err
	}
	return dto, nil
}

func ensureLoginEligible(status memberenum.MemberStatus) error {
	switch status {
	case memberenum.MemberStatusActive:
		return nil
	case memberenum.MemberStatusUnverified:
		return errb.AuthForbidden("account is not verified")
	case memberenum.MemberStatusSuspended:
		return errb.AuthForbidden("account is suspended")
	case memberenum.MemberStatusDeleted:
		return errb.AuthUnauthorized("invalid credentials")
	default:
		return errb.AuthForbidden("account is not allowed to login")
	}
}

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

func requireLoginDeps(sc *svc.ServiceContext) error {
	if sc.Zitadel == nil {
		return errb.SysNotImplemented("zitadel not configured")
	}
	if sc.MemberProfile == nil {
		return errb.SysNotImplemented("member profile not configured")
	}
	return nil
}

func isMemberNotFound(err error) bool {
	e := errs.FromError(err)
	return e != nil && e.Category() == code.ResNotFound
}

func loginDataFromTokens(tokens *types.AuthTokenData) *types.LoginData {
	if tokens == nil {
		return nil
	}
	return &types.LoginData{
		AccessToken:  tokens.AccessToken,
		RefreshToken: tokens.RefreshToken,
		ExpiresIn:    tokens.ExpiresIn,
		UID:          tokens.UID,
		TokenType:    tokens.TokenType,
	}
}

func beginLoginMFA(ctx context.Context, sc *svc.ServiceContext, tenantID, tenantSlug, uid string) (*types.LoginData, error) {
	if sc.AuthLoginMFAChallenge == nil {
		return nil, errb.SysNotImplemented("login mfa challenge not configured")
	}
	if sc.MemberTOTP == nil {
		return nil, errb.SysNotImplemented("member TOTP not configured")
	}
	ttl := time.Duration(sc.Config.Auth.Defaults().RegistrationSessionTTLSeconds) * time.Second
	challenge, err := sc.AuthLoginMFAChallenge.Create(ctx, &domauth.CreateLoginMFAChallengeRequest{
		TenantID:   tenantID,
		TenantSlug: tenantSlug,
		UID:        uid,
		TTL:        ttl,
	})
	if err != nil {
		return nil, err
	}
	return &types.LoginData{
		MFARequired:    true,
		MFAChallengeID: challenge.ChallengeID,
		MFAExpiresIn:   challenge.ExpiresIn,
	}, nil
}

func confirmLoginMFA(ctx context.Context, sc *svc.ServiceContext, tenantSlug, challengeID, totpCode string) (*types.AuthTokenData, error) {
	if sc.AuthLoginMFAChallenge == nil {
		return nil, errb.SysNotImplemented("login mfa challenge not configured")
	}
	if sc.MemberTOTP == nil {
		return nil, errb.SysNotImplemented("member TOTP not configured")
	}

	tenant, err := resolveTenant(ctx, sc, tenantSlug)
	if err != nil {
		return nil, err
	}

	challenge, err := sc.AuthLoginMFAChallenge.Get(ctx, challengeID)
	if err != nil {
		return nil, err
	}
	if challenge.TenantID != tenant.TenantID {
		return nil, errb.AuthForbidden("login mfa challenge tenant mismatch")
	}
	if !strings.EqualFold(strings.TrimSpace(challenge.TenantSlug), strings.TrimSpace(tenantSlug)) {
		return nil, errb.AuthForbidden("login mfa challenge tenant mismatch")
	}

	member, err := sc.MemberProfile.GetByUID(ctx, &dommember.GetMemberRequest{
		TenantID: challenge.TenantID,
		UID:      challenge.UID,
	})
	if err != nil {
		return nil, err
	}
	if err := ensureLoginEligible(member.Status); err != nil {
		return nil, err
	}
	if !member.TOTPEnrolled {
		return nil, errb.ResInvalidState("totp not enrolled").WithCause(memberdom.ErrTOTPNotEnrolled)
	}

	if err := sc.MemberTOTP.VerifyCode(ctx, challenge.TenantID, challenge.UID, strings.TrimSpace(totpCode)); err != nil {
		return nil, err
	}

	if err := sc.AuthLoginMFAChallenge.Delete(ctx, challengeID); err != nil {
		return nil, err
	}

	return issueAuthToken(ctx, sc, challenge.TenantID, challenge.UID)
}
