package session

import (
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"io"
	"net/http"
	"sync"
	"time"
)

var (
	providers = make(map[string]IProvider)
	session   *Manager
	lock      sync.Mutex
)

type Manager struct {
	provider    IProvider
	cookieName  string
	maxLifetime int64
	lock        sync.Mutex
}

type IManager interface {
	SessionStart(w http.ResponseWriter, r *http.Request) ISession
	GC()
}

type IProvider interface {
	SessionInit(sid string) (ISession, error)
	SessionRead(sid string) (ISession, error)
	SessionDestroy(sid string) error
	SessionGC(maxLifeTime int64)
}

type ISession interface {
	Set(key, value interface{}) error
	Get(key interface{}) interface{}
	Delete(key interface{}) error
	SessionID() string
}

func NewManager(providerName, cookieName string, maxLifetime int64) (*Manager, error) {
	provider, ok := providers[providerName]

	if !ok {
		return nil, fmt.Errorf("session: unknown provider %q", providerName)
	}

	if session == nil {
		lock.Lock()
		defer lock.Unlock()
		session = &Manager{provider: provider, cookieName: cookieName, maxLifetime: maxLifetime}
	}

	return session, nil
}

func (m *Manager) SessionStart(w http.ResponseWriter, r *http.Request) ISession {
	m.lock.Lock()
	defer m.lock.Unlock()
	cookie, err := r.Cookie(m.cookieName)
	var session ISession
	if err != nil || cookie.Value == "" {
		sid := m.sessionId()
		session, _ = m.provider.SessionInit(sid)
		cookie := http.Cookie{Name: m.cookieName, Value: sid, Path: "/", HttpOnly: true, MaxAge: int(m.maxLifetime)}
		http.SetCookie(w, &cookie)
	} else {
		sid := cookie.Value
		session, _ = m.provider.SessionRead(sid)
	}

	return session
}

func (m *Manager) GC() {
	m.lock.Lock()
	defer m.lock.Unlock()
	m.provider.SessionGC(m.maxLifetime)
	time.AfterFunc(time.Duration(m.maxLifetime), func() { m.GC() })
}

func (m *Manager) sessionId() string {
	b := make([]byte, 32)
	if _, err := io.ReadFull(rand.Reader, b); err != nil {
		return ""
	}

	return base64.URLEncoding.EncodeToString(b)
}

func Register(name string, provider IProvider) {
	if providers[name] != nil {
		return
	}

	if provider == nil {
		panic("session: Register provider is nil")
	}

	providers[name] = provider
}
