package sessions

import (
	"errors"
	"sync"
	"time"

	"github.com/kataras/iris/v12/context"
)

type (
	// provider contains the sessions and external databases (load and update).
	// It's the session memory manager
	provider struct {
		mu               sync.RWMutex
		sessions         map[string]*Session
		db               Database
		dbRequestHandler DatabaseRequestHandler
		destroyListeners []DestroyListener
	}
)

// newProvider returns a new sessions provider
func newProvider() *provider {
	p := &provider{
		sessions: make(map[string]*Session),
		db:       newMemDB(),
	}

	return p
}

// RegisterDatabase sets a session database.
func (p *provider) RegisterDatabase(db Database) {
	if db == nil {
		return
	}

	p.mu.Lock() // for any case
	p.db = db
	if dbreq, ok := db.(DatabaseRequestHandler); ok {
		p.dbRequestHandler = dbreq
	}
	p.mu.Unlock()
}

// newSession returns a new session from sessionid
func (p *provider) newSession(man *Sessions, sid string, expires time.Duration) *Session {
	sess := &Session{
		sid:      sid,
		Man:      man,
		provider: p,
	}

	onExpire := func() {
		p.mu.Lock()
		p.deleteSession(sess)
		p.mu.Unlock()
	}

	lifetime := p.db.Acquire(sid, expires)

	// simple and straight:
	if !lifetime.IsZero() {
		// if stored time is not zero
		// start a timer based on the stored time, if not expired.
		lifetime.Revive(onExpire)
	} else {
		// Remember:  if db not exist or it has been expired
		// then the stored time will be zero(see loadSessionFromDB) and the values will be empty.
		//
		// Even if the database has an unlimited session (possible by a previous app run)
		// priority to the "expires" is given,
		// again if <=0 then it does nothing.
		lifetime.Begin(expires, onExpire)
	}

	sess.Lifetime = &lifetime
	return sess
}

// Init creates the session  and returns it
func (p *provider) Init(man *Sessions, sid string, expires time.Duration) *Session {
	newSession := p.newSession(man, sid, expires)
	newSession.isNew = true
	p.mu.Lock()
	p.sessions[sid] = newSession
	p.mu.Unlock()
	return newSession
}

func (p *provider) EndRequest(ctx *context.Context, session *Session) {
	if p.dbRequestHandler != nil {
		p.dbRequestHandler.EndRequest(ctx, session)
	}
}

// ErrNotFound may be returned from `UpdateExpiration` of a non-existing or
// invalid session entry from memory storage or databases.
// Usage:
//
//	if err != nil && err.Is(err, sessions.ErrNotFound) {
//	    [handle error...]
//	}
var ErrNotFound = errors.New("session not found")

// UpdateExpiration resets the expiration of a session.
// if expires > 0 then it will try to update the expiration and destroy task is delayed.
// if expires <= 0 then it does nothing it returns nil, to destroy a session call the `Destroy` func instead.
//
// If the session is not found, it returns a `NotFound` error,  this can only happen when you restart the server and you used the memory-based storage(default),
// because the call of the provider's `UpdateExpiration` is always called when the client has a valid session cookie.
//
// If a backend database is used then it may return an `ErrNotImplemented` error if the underline database does not support this operation.
func (p *provider) UpdateExpiration(sid string, expires time.Duration) error {
	if expires <= 0 {
		return nil
	}

	p.mu.RLock()
	sess, found := p.sessions[sid]
	p.mu.RUnlock()
	if !found {
		return ErrNotFound
	}

	sess.Lifetime.Shift(expires)
	return p.db.OnUpdateExpiration(sid, expires)
}

// Read returns the store which sid parameter belongs
func (p *provider) Read(man *Sessions, sid string, expires time.Duration) *Session {
	p.mu.RLock()
	sess, found := p.sessions[sid]
	p.mu.RUnlock()
	if found {
		sess.mu.Lock()
		sess.isNew = false
		sess.mu.Unlock()
		sess.runFlashGC() // run the flash messages GC, new request here of existing session

		return sess
	}

	return p.Init(man, sid, expires) // if not found create new
}

func (p *provider) registerDestroyListener(ln DestroyListener) {
	if ln == nil {
		return
	}
	p.destroyListeners = append(p.destroyListeners, ln)
}

func (p *provider) fireDestroy(sid string) {
	for _, ln := range p.destroyListeners {
		ln(sid)
	}
}

// Destroy destroys the session, removes all sessions and flash values,
// the session itself and updates the registered session databases,
// this called from sessionManager which removes the client's cookie also.
func (p *provider) Destroy(sid string) {
	p.mu.Lock()
	if sess, found := p.sessions[sid]; found {
		p.deleteSession(sess)
	}
	p.mu.Unlock()
}

// DestroyAll removes all sessions
// from the server-side memory (and database if registered).
// Client's session cookie will still exist but it will be reseted on the next request.
func (p *provider) DestroyAll() {
	p.mu.Lock()
	for _, sess := range p.sessions {
		p.deleteSession(sess)
	}
	p.mu.Unlock()
}

func (p *provider) deleteSession(sess *Session) {
	sid := sess.sid

	delete(p.sessions, sid)
	p.db.Release(sid)
	p.fireDestroy(sid)
}

/*
func (p *provider) regenerateID(ctx *context.Context, oldsid string) {
	p.mu.RLock()
	sess, ok := p.sessions[oldsid]
	p.mu.RUnlock()

	if ok {
		newsid := sess.Man.config.SessionIDGenerator(ctx)
		sess.mu.Lock()
		sess.sid = newsid
		sess.mu.Unlock()

		p.mu.Lock()
		p.sessions[newsid] = sess
		delete(p.sessions, oldsid)
		p.mu.Unlock()
	}
}
*/