package server

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"log"
	"sync"
	"time"

	"github.com/jackc/pgx/v5/pgxpool"
)

// Session represents an authenticated user session.
type Session struct {
	ID           string
	UID          int64
	CompanyID    int64
	Login        string
	CreatedAt    time.Time
	LastActivity time.Time
}

// SessionStore is a session store with an in-memory cache backed by PostgreSQL.
// Mirrors: odoo/http.py OpenERPSession
type SessionStore struct {
	mu       sync.RWMutex
	sessions map[string]*Session
	ttl      time.Duration
	pool     *pgxpool.Pool
}

// NewSessionStore creates a new session store with the given TTL and DB pool.
func NewSessionStore(ttl time.Duration, pool *pgxpool.Pool) *SessionStore {
	return &SessionStore{
		sessions: make(map[string]*Session),
		ttl:      ttl,
		pool:     pool,
	}
}

// InitSessionTable creates the sessions table if it does not exist.
func InitSessionTable(ctx context.Context, pool *pgxpool.Pool) error {
	_, err := pool.Exec(ctx, `
		CREATE TABLE IF NOT EXISTS sessions (
			id VARCHAR(64) PRIMARY KEY,
			uid INT8 NOT NULL,
			company_id INT8 NOT NULL,
			login VARCHAR(255),
			created_at TIMESTAMP DEFAULT NOW(),
			last_seen TIMESTAMP DEFAULT NOW()
		)
	`)
	if err != nil {
		return err
	}
	log.Println("odoo: sessions table ready")
	return nil
}

// New creates a new session, stores it in memory and PostgreSQL, and returns it.
func (s *SessionStore) New(uid, companyID int64, login string) *Session {
	token := generateToken()
	now := time.Now()
	sess := &Session{
		ID:           token,
		UID:          uid,
		CompanyID:    companyID,
		Login:        login,
		CreatedAt:    now,
		LastActivity: now,
	}

	// Store in memory cache
	s.mu.Lock()
	s.sessions[token] = sess
	s.mu.Unlock()

	// Persist to PostgreSQL
	if s.pool != nil {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		_, err := s.pool.Exec(ctx,
			`INSERT INTO sessions (id, uid, company_id, login, created_at, last_seen)
			 VALUES ($1, $2, $3, $4, $5, $6)
			 ON CONFLICT (id) DO NOTHING`,
			token, uid, companyID, login, now, now)
		if err != nil {
			log.Printf("session: failed to persist session to DB: %v", err)
		}
	}

	return sess
}

// Get retrieves a session by ID. Checks in-memory cache first, falls back to DB.
// Returns nil if not found or expired.
func (s *SessionStore) Get(id string) *Session {
	// Check memory cache first
	s.mu.RLock()
	sess, ok := s.sessions[id]
	s.mu.RUnlock()

	if ok {
		if time.Since(sess.LastActivity) > s.ttl {
			s.Delete(id)
			return nil
		}
		// Update last activity
		now := time.Now()
		s.mu.Lock()
		sess.LastActivity = now
		s.mu.Unlock()

		// Update last_seen in DB asynchronously
		if s.pool != nil {
			go func() {
				ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
				defer cancel()
				s.pool.Exec(ctx,
					`UPDATE sessions SET last_seen = $1 WHERE id = $2`, now, id)
			}()
		}

		return sess
	}

	// Fallback to DB
	if s.pool == nil {
		return nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	sess = &Session{}
	err := s.pool.QueryRow(ctx,
		`SELECT id, uid, company_id, login, created_at, last_seen
		 FROM sessions WHERE id = $1`, id).Scan(
		&sess.ID, &sess.UID, &sess.CompanyID, &sess.Login,
		&sess.CreatedAt, &sess.LastActivity)
	if err != nil {
		return nil
	}

	// Check TTL
	if time.Since(sess.LastActivity) > s.ttl {
		s.Delete(id)
		return nil
	}

	// Update last activity
	now := time.Now()
	sess.LastActivity = now

	// Add to memory cache
	s.mu.Lock()
	s.sessions[id] = sess
	s.mu.Unlock()

	// Update last_seen in DB
	s.pool.Exec(ctx,
		`UPDATE sessions SET last_seen = $1 WHERE id = $2`, now, id)

	return sess
}

// Delete removes a session from memory and DB.
func (s *SessionStore) Delete(id string) {
	s.mu.Lock()
	delete(s.sessions, id)
	s.mu.Unlock()

	if s.pool != nil {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		_, err := s.pool.Exec(ctx, `DELETE FROM sessions WHERE id = $1`, id)
		if err != nil {
			log.Printf("session: failed to delete session from DB: %v", err)
		}
	}
}

func generateToken() string {
	b := make([]byte, 32)
	rand.Read(b)
	return hex.EncodeToString(b)
}
