package usecase

import (
	"context"
	"crypto/rand"
	"errors"
	"math/big"
	"time"

	"github.com/google/uuid"
	"golang.org/x/crypto/bcrypt"

	memberconfig "gateway/internal/model/member/config"
	member "gateway/internal/model/member/domain"
	domrepo "gateway/internal/model/member/domain/repository"
	domusecase "gateway/internal/model/member/domain/usecase"
)

type otpUseCase struct {
	store  domrepo.OTPChallengeStore
	config memberconfig.Config
}

// OTPUseCaseParam wires OTPUseCase.
type OTPUseCaseParam struct {
	Store  domrepo.OTPChallengeStore
	Config memberconfig.Config
}

// MustOTPUseCase constructs OTPUseCase.
func MustOTPUseCase(param OTPUseCaseParam) domusecase.OTPUseCase {
	return &otpUseCase{
		store:  param.Store,
		config: param.Config.Defaults(),
	}
}

func (uc *otpUseCase) Generate(ctx context.Context, req *domusecase.GenerateOTPRequest) (*domusecase.OTPChallengeDTO, string, error) {
	if req == nil || req.TenantID == "" || req.Purpose == "" {
		return nil, "", errb.InputMissingRequired("tenant_id and purpose are required")
	}
	plainCode, err := generateNumericOTP(uc.config.OTP.Length)
	if err != nil {
		return nil, "", errb.SysInternal("otp generation failed").WithCause(err)
	}
	hash, err := bcrypt.GenerateFromPassword([]byte(plainCode), bcrypt.DefaultCost)
	if err != nil {
		return nil, "", errb.SysInternal("otp hash failed").WithCause(err)
	}

	challengeID := uuid.NewString()
	ch := &domrepo.OTPChallenge{
		TenantID: req.TenantID,
		UID:      req.UID,
		Purpose:  req.Purpose,
		Target:   req.Target,
		CodeHash: string(hash),
	}
	ttl := time.Duration(uc.config.OTP.TTLSeconds) * time.Second
	if err := uc.store.Save(ctx, challengeID, ch, ttl); err != nil {
		return nil, "", wrapRepoErr(err, "otp persist failed")
	}
	return &domusecase.OTPChallengeDTO{
		ChallengeID: challengeID,
		ExpiresIn:   uc.config.OTP.TTLSeconds,
	}, plainCode, nil
}

func (uc *otpUseCase) Verify(ctx context.Context, req *domusecase.VerifyOTPRequest) (string, error) {
	if req == nil || req.ChallengeID == "" || req.Code == "" || req.Purpose == "" {
		return "", errb.InputMissingRequired("challenge_id, code and purpose are required")
	}
	ch, err := uc.store.Get(ctx, req.ChallengeID)
	if err != nil {
		if errors.Is(err, member.ErrChallengeNotFound) {
			return "", errb.ResNotFound("otp challenge", req.ChallengeID).WithCause(err)
		}
		return "", wrapRepoErr(err, "otp read failed")
	}
	if ch.TenantID != req.TenantID {
		return "", errb.AuthForbidden("otp challenge tenant mismatch")
	}
	if ch.UID != "" {
		if req.UID == "" {
			return "", errb.InputMissingRequired("uid is required for this otp challenge")
		}
		if ch.UID != req.UID {
			return "", errb.AuthForbidden("otp challenge uid mismatch")
		}
	}
	if ch.Purpose != req.Purpose {
		return "", errb.AuthForbidden("otp challenge purpose mismatch")
	}
	if ch.Attempts >= uc.config.OTP.MaxAttempts {
		return "", errb.ResInvalidState("otp challenge locked").WithCause(member.ErrChallengeLocked)
	}
	if err := bcrypt.CompareHashAndPassword([]byte(ch.CodeHash), []byte(req.Code)); err != nil {
		attempts, incErr := uc.store.IncrementAttempts(ctx, req.ChallengeID)
		if incErr != nil {
			if errors.Is(incErr, member.ErrChallengeNotFound) {
				return "", errb.ResNotFound("otp challenge", req.ChallengeID).WithCause(incErr)
			}
			return "", wrapRepoErr(incErr, "otp persist failed")
		}
		if attempts >= uc.config.OTP.MaxAttempts {
			return "", errb.ResInvalidState("otp challenge locked").WithCause(member.ErrChallengeLocked)
		}
		return "", errb.AuthForbidden("invalid otp code").WithCause(member.ErrInvalidOTP)
	}
	target := ch.Target
	if delErr := uc.store.Delete(ctx, req.ChallengeID); delErr != nil {
		return "", wrapRepoErr(delErr, "otp delete failed")
	}
	return target, nil
}

func (uc *otpUseCase) Invalidate(ctx context.Context, challengeID string) error {
	if challengeID == "" {
		return errb.InputMissingRequired("challenge_id is required")
	}
	return wrapRepoErr(uc.store.Delete(ctx, challengeID), "otp delete failed")
}

func (uc *otpUseCase) GetChallenge(ctx context.Context, challengeID string) (*domusecase.OTPChallengeInfo, error) {
	if challengeID == "" {
		return nil, errb.InputMissingRequired("challenge_id is required")
	}
	ch, err := uc.store.Get(ctx, challengeID)
	if err != nil {
		if errors.Is(err, member.ErrChallengeNotFound) {
			return nil, errb.ResNotFound("otp challenge", challengeID).WithCause(err)
		}
		return nil, wrapRepoErr(err, "otp read failed")
	}
	return &domusecase.OTPChallengeInfo{
		TenantID: ch.TenantID,
		UID:      ch.UID,
		Purpose:  ch.Purpose,
		Target:   ch.Target,
	}, nil
}

func (uc *otpUseCase) MatchChallenge(ctx context.Context, req *domusecase.MatchChallengeRequest) (*domusecase.OTPChallengeInfo, error) {
	if req == nil || req.ChallengeID == "" || req.TenantID == "" || req.Purpose == "" {
		return nil, errb.InputMissingRequired("challenge_id, tenant_id and purpose are required")
	}
	ch, err := uc.GetChallenge(ctx, req.ChallengeID)
	if err != nil {
		return nil, err
	}
	if ch.TenantID != req.TenantID {
		return nil, errb.AuthForbidden("otp challenge tenant mismatch")
	}
	if ch.Purpose != req.Purpose {
		return nil, errb.AuthForbidden("otp challenge purpose mismatch")
	}
	if req.RequireUID && ch.UID == "" {
		return nil, errb.ResInvalidState("otp challenge missing uid")
	}
	if req.RequireTarget && ch.Target == "" {
		return nil, errb.ResInvalidState("otp challenge missing target")
	}
	return ch, nil
}

func generateNumericOTP(length int) (string, error) {
	if length <= 0 {
		length = 6
	}
	out := make([]byte, length)
	for i := range out {
		n, err := rand.Int(rand.Reader, big.NewInt(10))
		if err != nil {
			return "", err
		}
		out[i] = byte('0' + n.Uint64()) //nolint:gosec // digit 0-9
	}
	return string(out), nil
}
