package session

import (
	"fmt"
	"net/http"
	"time"

	"git.kirsle.net/apps/gophertype/pkg/cache"
	"git.kirsle.net/apps/gophertype/pkg/console"
	"git.kirsle.net/apps/gophertype/pkg/constants"
	"github.com/google/uuid"
)

// Session cookie object that is kept server side in Redis.
type Session struct {
	UUID      string    `json:"-"` // not stored
	LoggedIn  bool      `json:"loggedIn"`
	UserID    int       `json:"userId,omitempty"`
	Flashes   []string  `json:"flashes,omitempty"`
	Errors    []string  `json:"errors,omitempty"`
	AgeOK     bool      `json:"ageOK,omitempty"`
	EditToken string    `json:"editToken,omitempty"`
	Email     string    `json:"email,omitempty"`
	Name      string    `json:"name,omitempty"`
	LastSeen  time.Time `json:"lastSeen"`
}

const (
	ContextKey     = "session"
	CurrentUserKey = "current_user"
	CSRFKey        = "csrf"
	RequestTimeKey = "req_time"
)

// New creates a blank session object.
func New() *Session {
	return &Session{
		UUID:    uuid.New().String(),
		Flashes: []string{},
		Errors:  []string{},
	}
}

// Load the session from the browser session_id token and Redis or creates a new session.
func LoadOrNew(r *http.Request) *Session {
	var sess = New()

	// Read the session cookie value.
	cookie, err := r.Cookie(constants.SessionCookieName)
	if err != nil {
		console.Debug("session.LoadOrNew: cookie error, new sess: %s", err)
		return sess
	}

	// Look up this UUID in Redis.
	sess.UUID = cookie.Value
	key := fmt.Sprintf(constants.SessionRedisKeyFormat, sess.UUID)

	err = cache.GetJSON(key, sess)
	// console.Error("LoadOrNew: raw from Redis: %+v", sess)
	if err != nil {
		console.Error("session.LoadOrNew: didn't find %s in Redis: %s", key, err)
	}

	return sess
}

// Save the session and send a cookie header.
func (s *Session) Save(w http.ResponseWriter) {
	// Roll a UUID session_id value.
	if s.UUID == "" {
		s.UUID = uuid.New().String()
	}

	// Ensure it is a valid UUID.
	if _, err := uuid.Parse(s.UUID); err != nil {
		console.Error("Session.Save: got an invalid UUID session_id: %s", err)
		s.UUID = uuid.New().String()
	}

	// Ping last seen.
	s.LastSeen = time.Now()

	// Save their session object in Redis.
	key := fmt.Sprintf(constants.SessionRedisKeyFormat, s.UUID)
	if err := cache.SetJSON(key, s, constants.SessionCookieMaxAge*time.Second); err != nil {
		console.Error("Session.Save: couldn't write to Redis: %s", err)
	}

	cookie := &http.Cookie{
		Name:     constants.SessionCookieName,
		Value:    s.UUID,
		MaxAge:   constants.SessionCookieMaxAge,
		Path:     "/",
		HttpOnly: true,
	}
	http.SetCookie(w, cookie)
}

// Get the session from the current HTTP request context.
func Get(r *http.Request) *Session {
	if r == nil {
		panic("session.Get: http.Request is required")
	}

	// Cached in the request context?
	ctx := r.Context()
	if sess, ok := ctx.Value(ContextKey).(*Session); ok {
		return sess
	}

	return LoadOrNew(r)
}

// ReadFlashes returns and clears the Flashes and Errors for this session.
func (s *Session) ReadFlashes(w http.ResponseWriter) (flashes, errors []string) {
	flashes = s.Flashes
	errors = s.Errors
	s.Flashes = []string{}
	s.Errors = []string{}
	if len(flashes)+len(errors) > 0 {
		s.Save(w)
	}
	return flashes, errors
}

// Flash adds a transient message to the user's session to show on next page load.
func Flash(w http.ResponseWriter, r *http.Request, msg string, args ...interface{}) {
	sess := Get(r)
	sess.Flashes = append(sess.Flashes, fmt.Sprintf(msg, args...))
	sess.Save(w)
}

// FlashError adds a transient error message to the session.
func FlashError(w http.ResponseWriter, r *http.Request, msg string, args ...interface{}) {
	sess := Get(r)
	sess.Errors = append(sess.Errors, fmt.Sprintf(msg, args...))
	sess.Save(w)
}

// LoginUser marks a session as logged in to an account.
func LoginUser(w http.ResponseWriter, r *http.Request, userID int) error {
	sess := Get(r)
	sess.LoggedIn = true
	sess.UserID = userID
	sess.Save(w)
	return nil
}

// LogoutUser signs a user out.
func LogoutUser(w http.ResponseWriter, r *http.Request) {
	sess := Get(r)
	sess.LoggedIn = false
	sess.UserID = 0
	sess.Save(w)
}
