//////////////////////////////////////////////////////////////////////
//
// Given is a SessionManager that stores session information in
// memory. The SessionManager itself is working, however, since we
// keep on adding new sessions to the manager our program will
// eventually run out of memory.
//
// Your task is to implement a session cleaner routine that runs
// concurrently in the background and cleans every session that
// hasn't been updated for more than 5 seconds (of course usually
// session times are much longer).
//
// Note that we expect the session to be removed anytime between 5 and
// 7 seconds after the last update. Also, note that you have to be
// very careful in order to prevent race conditions.
//

package main

import (
	"errors"
	"log"
	"sync"
	"time"
)

// SessionManager keeps track of all sessions from creation, updating
// to destroying.
type SessionManager struct {
	sessions map[string]Session
	rwmu     sync.RWMutex
}

// Session stores the session's data
type Session struct {
	Data  map[string]interface{}
	timer *time.Timer
	mu    *sync.Mutex
}

// NewSessionManager creates a new sessionManager
func NewSessionManager() *SessionManager {
	m := &SessionManager{
		sessions: make(map[string]Session),
	}

	return m
}

func (m *SessionManager) CleanSession(sessionID string) {
	m.rwmu.Lock()
	delete(m.sessions, sessionID)
	m.rwmu.Unlock()
}

// CreateSession creates a new session and returns the sessionID
func (m *SessionManager) CreateSession() (string, error) {
	sessionID, err := MakeSessionID()
	if err != nil {
		return "", err
	}

	m.rwmu.Lock()
	m.sessions[sessionID] = Session{
		Data: make(map[string]interface{}),
		timer: time.AfterFunc(5*time.Second, func() {
			m.CleanSession(sessionID)
		}),
		mu: &sync.Mutex{},
	}
	m.rwmu.Unlock()

	return sessionID, nil
}

// ErrSessionNotFound returned when sessionID not listed in
// SessionManager
var ErrSessionNotFound = errors.New("SessionID does not exists")

// GetSessionData returns data related to session if sessionID is
// found, errors otherwise
func (m *SessionManager) GetSessionData(sessionID string) (map[string]interface{}, error) {
	m.rwmu.RLock()
	session, ok := m.sessions[sessionID]
	m.rwmu.RUnlock()
	if !ok {
		return nil, ErrSessionNotFound
	}
	return session.Data, nil
}

// UpdateSessionData overwrites the old session data with the new one
func (m *SessionManager) UpdateSessionData(sessionID string, data map[string]interface{}) error {
	m.rwmu.RLock()
	s, ok := m.sessions[sessionID]
	m.rwmu.RUnlock()

	if !ok {
		return ErrSessionNotFound
	}

	// Lock the session data to handle concurrent access to the same session
	s.mu.Lock()
	s.Data = data
	if !s.timer.Stop() {
		<-s.timer.C
	}
	s.timer.Reset(5 * time.Second)
	s.mu.Unlock()

	return nil
}

func main() {
	// Create new sessionManager and new session
	m := NewSessionManager()
	sID, err := m.CreateSession()
	if err != nil {
		log.Fatal(err)
	}

	log.Println("Created new session with ID", sID)

	// Update session data
	data := make(map[string]interface{})
	data["website"] = "longhoang.de"

	err = m.UpdateSessionData(sID, data)
	if err != nil {
		log.Fatal(err)
	}

	log.Println("Update session data, set website to longhoang.de")

	// Retrieve data from manager again
	updatedData, err := m.GetSessionData(sID)
	if err != nil {
		log.Fatal(err)
	}

	log.Println("Get session data:", updatedData)
}
