package routers

import (
	"context"
	"crypto/rand"
	"encoding/base64"
	"io"
	"net/http"
	"time"

	"github.com/tech/sendico/pkg/api/http/response"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/mutil/mzap"
	"github.com/tech/sendico/server/interface/api/sresponse"
	"go.uber.org/zap"
)

func generateRefreshTokenData(length int) (string, error) {
	randomBytes := make([]byte, length)
	if _, err := io.ReadFull(rand.Reader, randomBytes); err != nil {
		return "", merrors.Internal("failed to generate secure random bytes: " + err.Error())
	}

	return base64.URLEncoding.EncodeToString(randomBytes), nil
}

func (er *PublicRouter) prepareRefreshToken(ctx context.Context, r *http.Request, session *model.SessionIdentifier, account *model.Account) (*model.RefreshToken, error) {
	refreshToken, err := generateRefreshTokenData(er.config.Length)
	if err != nil {
		er.logger.Warn("Failed to generate refresh token", zap.Error(err), mzap.StorableRef(account))
		return nil, err
	}

	token := &model.RefreshToken{
		AccountBoundBase: model.AccountBoundBase{
			AccountRef: account.GetID(),
		},
		ClientRefreshToken: model.ClientRefreshToken{
			SessionIdentifier: *session,
			RefreshToken:      refreshToken,
		},
		ExpiresAt: time.Now().Add(time.Duration(er.config.Expiration.Refresh) * time.Hour),
		IsRevoked: false,
		UserAgent: r.UserAgent(),
		IPAddress: r.RemoteAddr,
	}

	if err = er.rtdb.Create(ctx, token); err != nil {
		er.logger.Warn("Failed to store a refresh token", zap.Error(err), mzap.StorableRef(account),
			zap.String("client_id", token.ClientID), zap.String("device_id", token.DeviceID))
		return nil, err
	}

	return token, nil
}

func (pr *PublicRouter) refreshAndRespondLogin(
	ctx context.Context,
	r *http.Request,
	session *model.SessionIdentifier,
	account *model.Account,
	accessToken *sresponse.TokenData,
) http.HandlerFunc {
	refreshToken, err := pr.prepareRefreshToken(ctx, r, session, account)
	if err != nil {
		pr.logger.Warn("Failed to create refresh token", zap.Error(err), mzap.StorableRef(account),
			zap.String("client_id", session.ClientID), zap.String("device_id", session.DeviceID))
		return response.Internal(pr.logger, pr.service, err)
	}

	token := sresponse.TokenData{
		Token:      refreshToken.RefreshToken,
		Expiration: refreshToken.ExpiresAt,
	}
	return sresponse.Login(pr.logger, account, accessToken, &token)
}
