package session

import (
	"crypto/x509"
	"encoding/json"
	"time"

	fasthttpsession "github.com/fasthttp/session/v2"
	"github.com/fasthttp/session/v2/providers/memory"
	"github.com/fasthttp/session/v2/providers/redis"
	"github.com/valyala/fasthttp"

	"github.com/authelia/authelia/internal/configuration/schema"
	"github.com/authelia/authelia/internal/logging"
	"github.com/authelia/authelia/internal/utils"
)

// Provider a session provider.
type Provider struct {
	sessionHolder *fasthttpsession.Session
	RememberMe    time.Duration
	Inactivity    time.Duration
}

// NewProvider instantiate a session provider given a configuration.
func NewProvider(configuration schema.SessionConfiguration, certPool *x509.CertPool) *Provider {
	providerConfig := NewProviderConfig(configuration, certPool)

	provider := new(Provider)
	provider.sessionHolder = fasthttpsession.New(providerConfig.config)

	logger := logging.Logger()

	duration, err := utils.ParseDurationString(configuration.RememberMeDuration)
	if err != nil {
		logger.Fatal(err)
	}

	provider.RememberMe = duration

	duration, err = utils.ParseDurationString(configuration.Inactivity)
	if err != nil {
		logger.Fatal(err)
	}

	provider.Inactivity = duration

	var providerImpl fasthttpsession.Provider

	switch {
	case providerConfig.redisConfig != nil:
		providerImpl, err = redis.New(*providerConfig.redisConfig)
		if err != nil {
			logger.Fatal(err)
		}
	case providerConfig.redisSentinelConfig != nil:
		providerImpl, err = redis.NewFailoverCluster(*providerConfig.redisSentinelConfig)
		if err != nil {
			logger.Fatal(err)
		}
	default:
		providerImpl, err = memory.New(memory.Config{})
		if err != nil {
			logger.Fatal(err)
		}
	}

	err = provider.sessionHolder.SetProvider(providerImpl)
	if err != nil {
		logger.Fatal(err)
	}

	return provider
}

// GetSession return the user session from a request.
func (p *Provider) GetSession(ctx *fasthttp.RequestCtx) (UserSession, error) {
	store, err := p.sessionHolder.Get(ctx)

	if err != nil {
		return NewDefaultUserSession(), err
	}

	userSessionJSON, ok := store.Get(userSessionStorerKey).([]byte)

	// If userSession is not yet defined we create the new session with default values
	// and save it in the store.
	if !ok {
		userSession := NewDefaultUserSession()
		store.Set(userSessionStorerKey, userSession)

		return userSession, nil
	}

	var userSession UserSession
	err = json.Unmarshal(userSessionJSON, &userSession)

	if err != nil {
		return NewDefaultUserSession(), err
	}

	return userSession, nil
}

// SaveSession save the user session.
func (p *Provider) SaveSession(ctx *fasthttp.RequestCtx, userSession UserSession) error {
	store, err := p.sessionHolder.Get(ctx)

	if err != nil {
		return err
	}

	userSessionJSON, err := json.Marshal(userSession)

	if err != nil {
		return err
	}

	store.Set(userSessionStorerKey, userSessionJSON)

	err = p.sessionHolder.Save(ctx, store)

	if err != nil {
		return err
	}

	return nil
}

// RegenerateSession regenerate a session ID.
func (p *Provider) RegenerateSession(ctx *fasthttp.RequestCtx) error {
	err := p.sessionHolder.Regenerate(ctx)
	return err
}

// DestroySession destroy a session ID and delete the cookie.
func (p *Provider) DestroySession(ctx *fasthttp.RequestCtx) error {
	return p.sessionHolder.Destroy(ctx)
}

// UpdateExpiration update the expiration of the cookie and session.
func (p *Provider) UpdateExpiration(ctx *fasthttp.RequestCtx, expiration time.Duration) error {
	store, err := p.sessionHolder.Get(ctx)

	if err != nil {
		return err
	}

	err = store.SetExpiration(expiration)

	if err != nil {
		return err
	}

	return p.sessionHolder.Save(ctx, store)
}

// GetExpiration get the expiration of the current session.
func (p *Provider) GetExpiration(ctx *fasthttp.RequestCtx) (time.Duration, error) {
	store, err := p.sessionHolder.Get(ctx)

	if err != nil {
		return time.Duration(0), err
	}

	return store.GetExpiration(), nil
}
