package confirmationimp

import (
	"context"
	"crypto/rand"
	"fmt"
	"strings"
	"time"

	"github.com/go-chi/jwtauth/v5"
	api "github.com/tech/sendico/pkg/api/http"
	"github.com/tech/sendico/pkg/db/refreshtokens"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/messaging"
	cnotifications "github.com/tech/sendico/pkg/messaging/notifications/confirmation"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/mservice"
	"github.com/tech/sendico/pkg/mutil/mzap"
	eapi "github.com/tech/sendico/server/interface/api"
	"github.com/tech/sendico/server/interface/api/sresponse"
	"github.com/tech/sendico/server/interface/middleware"
	emodel "github.com/tech/sendico/server/interface/model"
	"go.uber.org/zap"
)

type Config struct {
	CodeLength  int
	TTL         time.Duration
	MaxAttempts int
	Cooldown    time.Duration
	ResendLimit int
}

func defaultConfig() Config {
	return Config{
		CodeLength:  6,
		TTL:         10 * time.Minute,
		MaxAttempts: 5,
		Cooldown:    time.Minute,
		ResendLimit: 5,
	}
}

func DefaultConfig() Config {
	return defaultConfig()
}

type ConfirmationAPI struct {
	logger      mlogger.Logger
	config      Config
	store       *ConfirmationStore
	rtdb        refreshtokens.DB
	producer    messaging.Producer
	tokenConfig middleware.TokenConfig
	signature   middleware.Signature
}

func (a *ConfirmationAPI) Name() mservice.Type {
	return mservice.Confirmations
}

func (a *ConfirmationAPI) Finish(_ context.Context) error {
	return nil
}

func CreateAPI(a eapi.API) (*ConfirmationAPI, error) {
	cdb, err := a.DBFactory().NewConfirmationsDB()
	if err != nil {
		return nil, err
	}
	rtdb, err := a.DBFactory().NewRefreshTokensDB()
	if err != nil {
		return nil, err
	}

	p := &ConfirmationAPI{
		logger:      a.Logger().Named(mservice.Confirmations),
		config:      defaultConfig(),
		store:       NewStore(cdb),
		rtdb:        rtdb,
		producer:    a.Register().Messaging().Producer(),
		tokenConfig: a.Config().Mw.Token,
		signature:   middleware.SignatureConf(a.Config().Mw),
	}

	a.Register().PendingAccountHandler(p.Name(), "/", api.Post, p.requestCode)
	a.Register().PendingAccountHandler(p.Name(), "/resend", api.Post, p.resendCode)
	a.Register().PendingAccountHandler(p.Name(), "/verify", api.Post, p.verifyCode)
	return p, nil
}

func (a *ConfirmationAPI) generateCode() (string, error) {
	const digits = "0123456789"
	b := make([]byte, a.config.CodeLength)
	_, err := rand.Read(b)
	if err != nil {
		return "", err
	}
	for i := range b {
		b[i] = digits[int(b[i])%len(digits)]
	}
	return string(b), nil
}

func (a *ConfirmationAPI) parseTarget(raw string) (model.ConfirmationTarget, error) {
	switch strings.ToLower(strings.TrimSpace(raw)) {
	case string(model.ConfirmationTargetLogin):
		return model.ConfirmationTargetLogin, nil
	case string(model.ConfirmationTargetPayout):
		return model.ConfirmationTargetPayout, nil
	default:
		return "", merrors.InvalidArgument(fmt.Sprintf("unsupported target '%s'", raw), "target")
	}
}

func (a *ConfirmationAPI) resolveDestination(reqDest string, account *model.Account) string {
	destination := strings.ToLower(strings.TrimSpace(reqDest))
	if destination == "" && account != nil {
		destination = strings.ToLower(strings.TrimSpace(account.Login))
	}
	return destination
}

func (a *ConfirmationAPI) sendCode(account *model.Account, target model.ConfirmationTarget, destination, code string) {
	a.logger.Info("Confirmation code generated",
		zap.String("target", string(target)),
		zap.String("destination", maskEmail(destination)),
		mzap.ObjRef("account_ref", account.ID))
	if err := a.producer.SendMessage(cnotifications.Code(a.Name(), account.ID, destination, target, code)); err != nil {
		a.logger.Warn("Failed to send confirmation code notification", zap.Error(err), mzap.ObjRef("account_ref", account.ID))
	}
	a.logger.Debug("Confirmation code debug dump", zap.String("code", code))
}

func maskEmail(email string) string {
	parts := strings.Split(email, "@")
	if len(parts) != 2 {
		return email
	}
	local := parts[0]
	if len(local) > 2 {
		local = local[:1] + "***" + local[len(local)-1:]
	} else {
		local = local[:1] + "***"
	}
	return local + "@" + parts[1]
}

func (a *ConfirmationAPI) createAccessToken(account *model.Account) (sresponse.TokenData, error) {
	ja := jwtauth.New(a.signature.Algorithm, a.signature.PrivateKey, a.signature.PublicKey)
	_, res, err := ja.Encode(emodel.Account2Claims(account, a.tokenConfig.Expiration.Account))
	token := sresponse.TokenData{
		Token:      res,
		Expiration: time.Now().Add(time.Duration(a.tokenConfig.Expiration.Account) * time.Hour),
	}
	return token, err
}
