package member

import (
	"context"
	"fmt"
	"os"
	"time"

	memberdom "gateway/internal/model/member/domain"
	"gateway/internal/model/member/domain/enum"
	domusecase "gateway/internal/model/member/domain/usecase"
	notifenum "gateway/internal/model/notification/domain/enum"
	notifuc "gateway/internal/model/notification/domain/usecase"
	"gateway/internal/svc"
	"gateway/internal/types"

	"github.com/zeromicro/go-zero/core/logx"
)

func startVerification(
	ctx context.Context,
	sc *svc.ServiceContext,
	actor Actor,
	purpose enum.OTPPurpose,
	channel notifenum.Channel,
	kind notifenum.NotifyKind,
	target string,
) (*types.VerificationStartData, error) {
	if sc.MemberOTP == nil {
		return nil, errb.SysNotImplemented("member OTP not configured")
	}
	if sc.Notifier == nil {
		return nil, errb.SysNotImplemented("notifier not configured")
	}
	if sc.MemberVerifyRate == nil {
		return nil, errb.SysNotImplemented("member verify rate not configured")
	}
	if target == "" {
		return nil, errb.InputMissingRequired("target is required")
	}

	cfg := sc.Config.Member.Defaults()
	rateKey := memberdom.GetVerifyRateRedisKey(actor.TenantID, actor.UID, string(purpose))
	if err := sc.MemberVerifyRate.AssertResendAllowed(ctx, rateKey, time.Duration(cfg.OTP.ResendCooldownSeconds)*time.Second); err != nil {
		return nil, err
	}
	dailyKey := memberdom.GetVerifyDailyRedisKey(actor.TenantID, actor.UID, string(purpose))
	if err := sc.MemberVerifyRate.AssertDailyAllowed(ctx, dailyKey, 24*time.Hour, cfg.OTP.DailyVerifyLimit); err != nil {
		return nil, err
	}

	dto, plainCode, err := sc.MemberOTP.Generate(ctx, &domusecase.GenerateOTPRequest{
		TenantID: actor.TenantID,
		UID:      actor.UID,
		Purpose:  purpose,
		Target:   target,
	})
	if err != nil {
		return nil, err
	}
	locale := sc.Config.Notification.DefaultLocale
	if _, sendErr := sc.Notifier.Send(ctx, &notifuc.SendRequest{
		TenantID:         actor.TenantID,
		UID:              actor.UID,
		Channel:          channel,
		Kind:             kind,
		Target:           target,
		Locale:           locale,
		Data:             map[string]any{"code": plainCode, "expires_in": dto.ExpiresIn},
		IdempotencyKey:   dto.ChallengeID,
		DoNotPersistBody: true,
		Severity:         notifenum.SeverityInfo,
	}); sendErr != nil {
		if invErr := sc.MemberOTP.Invalidate(ctx, dto.ChallengeID); invErr != nil {
			return nil, invErr
		}
		return nil, sendErr
	}
	if os.Getenv("GATEWAY_E2E") == "1" && sc.Redis != nil && sc.Redis.Zero() != nil {
		key := fmt.Sprintf("e2e:otp:%s", dto.ChallengeID)
		if setErr := sc.Redis.Zero().SetexCtx(ctx, key, plainCode, dto.ExpiresIn); setErr != nil {
			logx.WithContext(ctx).Infof("e2e otp mirror skipped: %v", setErr)
		}
	}
	return &types.VerificationStartData{
		ChallengeID: dto.ChallengeID,
		ExpiresIn:   dto.ExpiresIn,
	}, nil
}

func confirmVerification(
	ctx context.Context,
	sc *svc.ServiceContext,
	actor Actor,
	req *types.VerificationConfirmReq,
	purpose enum.OTPPurpose,
	setVerified func(context.Context, string, string, string) error,
) error {
	if sc.MemberOTP == nil || sc.MemberProfile == nil {
		return errb.SysNotImplemented("member module not configured")
	}
	if req == nil || req.ChallengeID == "" || req.Code == "" {
		return errb.InputMissingRequired("challenge_id and code are required")
	}
	target, err := sc.MemberOTP.Verify(ctx, &domusecase.VerifyOTPRequest{
		TenantID:    actor.TenantID,
		UID:         actor.UID,
		ChallengeID: req.ChallengeID,
		Code:        req.Code,
		Purpose:     purpose,
	})
	if err != nil {
		return err
	}
	return setVerified(ctx, actor.TenantID, actor.UID, target)
}

func requireTOTP(sc *svc.ServiceContext) error {
	if sc.MemberTOTP == nil {
		return errb.SysNotImplemented("member TOTP not configured")
	}
	return nil
}

func actorOrErr(ctx context.Context) (Actor, error) {
	actor, err := ActorFromContext(ctx)
	if err != nil {
		return Actor{}, errb.AuthUnauthorized("missing bearer token or X-Tenant-ID/X-UID headers")
	}
	return actor, nil
}
