package services

import (
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"fmt"
	"time"

	"veza-backend-api/internal/database"

	"github.com/google/uuid"

	"go.uber.org/zap"
)

// SessionService gère les sessions utilisateur
type SessionService struct {
	db     *database.Database
	logger *zap.Logger
}

// Session représente une session utilisateur
// MIGRATION UUID: ID migré vers uuid.UUID
type Session struct {
	ID        uuid.UUID  `json:"id" db:"id"`
	UserID    uuid.UUID  `json:"user_id" db:"user_id"`
	TokenHash string     `json:"-" db:"token_hash"`
	CreatedAt time.Time  `json:"created_at" db:"created_at"`
	ExpiresAt time.Time  `json:"expires_at" db:"expires_at"`
	RevokedAt *time.Time `json:"revoked_at" db:"revoked_at"`
	IPAddress string     `json:"ip_address" db:"ip_address"`
	UserAgent string     `json:"user_agent" db:"user_agent"`
}

// SessionCreateRequest données pour créer une session
// MIGRATION UUID: UserID migré vers uuid.UUID
type SessionCreateRequest struct {
	UserID    uuid.UUID     `json:"user_id"`
	Token     string        `json:"token"`
	IPAddress string        `json:"ip_address"`
	UserAgent string        `json:"user_agent"`
	Metadata  string        `json:"metadata"` // Ignored by DB, kept for compatibility if needed
	ExpiresIn time.Duration `json:"expires_in"`
}

// NewSessionService crée un nouveau service de session
func NewSessionService(db *database.Database, logger *zap.Logger) *SessionService {
	return &SessionService{
		db:     db,
		logger: logger,
	}
}

// CreateSession crée une nouvelle session
func (ss *SessionService) CreateSession(ctx context.Context, req *SessionCreateRequest) (*Session, error) {
	// Hasher le token pour le stockage
	tokenHash := ss.hashToken(req.Token)

	// Calculer la date d'expiration
	// If ExpiresIn is 0, default to 24 hours
	expiresIn := req.ExpiresIn
	if expiresIn == 0 {
		expiresIn = 24 * time.Hour
	}
	expiresAt := time.Now().Add(expiresIn)

	// Créer la session struct
	session := &Session{
		ID:        uuid.New(),
		UserID:    req.UserID,
		TokenHash: tokenHash,
		CreatedAt: time.Now(),
		ExpiresAt: expiresAt,
		IPAddress: req.IPAddress,
		UserAgent: req.UserAgent,
	}

	// Insérer en base
	query := `
		INSERT INTO sessions (id, user_id, token_hash, created_at, expires_at, ip_address, user_agent)
		VALUES ($1, $2, $3, $4, $5, $6, $7)
	`

	_, err := ss.db.ExecContext(ctx, query,
		session.ID,
		session.UserID,
		session.TokenHash,
		session.CreatedAt,
		session.ExpiresAt,
		session.IPAddress,
		session.UserAgent,
	)

	if err != nil {
		ss.logger.Error("Failed to create session",
			zap.Error(err),
			zap.String("user_id", req.UserID.String()),
		)
		return nil, fmt.Errorf("failed to create session: %w", err)
	}

	ss.logger.Info("Session created",
		zap.String("session_id", session.ID.String()),
		zap.String("user_id", req.UserID.String()),
		zap.Time("expires_at", session.ExpiresAt),
	)

	return session, nil
}

// ValidateSession valide une session par token hash
func (ss *SessionService) ValidateSession(ctx context.Context, token string) (*Session, error) {
	tokenHash := ss.hashToken(token)

	query := `
		SELECT id, user_id, token_hash, created_at, expires_at, revoked_at, ip_address, user_agent
		FROM sessions
		WHERE token_hash = $1 AND expires_at > $2 AND revoked_at IS NULL
	`

	var session Session
	err := ss.db.QueryRowContext(ctx, query, tokenHash, time.Now()).Scan(
		&session.ID,
		&session.UserID,
		&session.TokenHash,
		&session.CreatedAt,
		&session.ExpiresAt,
		&session.RevokedAt,
		&session.IPAddress,
		&session.UserAgent,
	)

	if err != nil {
		if err == sql.ErrNoRows {
			return nil, fmt.Errorf("session not found or expired")
		}
		ss.logger.Error("Failed to validate session",
			zap.Error(err),
			zap.String("token_hash", tokenHash),
		)
		return nil, fmt.Errorf("failed to validate session: %w", err)
	}

	return &session, nil
}

// RevokeSession révoque une session par token
func (ss *SessionService) RevokeSession(ctx context.Context, token string) error {
	tokenHash := ss.hashToken(token)

	query := `
		UPDATE sessions 
		SET revoked_at = $1
		WHERE token_hash = $2 AND revoked_at IS NULL
	`

	result, err := ss.db.ExecContext(ctx, query, time.Now(), tokenHash)
	if err != nil {
		ss.logger.Error("Failed to revoke session",
			zap.Error(err),
			zap.String("token_hash", tokenHash),
		)
		return fmt.Errorf("failed to revoke session: %w", err)
	}

	rowsAffected, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("failed to get rows affected: %w", err)
	}

	if rowsAffected == 0 {
		return fmt.Errorf("session not found or already revoked")
	}

	ss.logger.Info("Session revoked",
		zap.String("token_hash", tokenHash),
	)

	return nil
}

// RevokeAllUserSessions révoque toutes les sessions d'un utilisateur
func (ss *SessionService) RevokeAllUserSessions(ctx context.Context, userID uuid.UUID) (int64, error) {
	query := `
		UPDATE sessions 
		SET revoked_at = $2
		WHERE user_id = $1 AND revoked_at IS NULL
	`

	result, err := ss.db.ExecContext(ctx, query, userID, time.Now())
	if err != nil {
		ss.logger.Error("Failed to revoke user sessions",
			zap.Error(err),
			zap.String("user_id", userID.String()),
		)
		return 0, fmt.Errorf("failed to revoke user sessions: %w", err)
	}

	rowsAffected, err := result.RowsAffected()
	if err != nil {
		return 0, fmt.Errorf("failed to get rows affected: %w", err)
	}

	return rowsAffected, nil
}

// RevokeAllUserSessionsByUserID est un alias pour satisfaire l'interface attendue par AuthService
func (ss *SessionService) RevokeAllUserSessionsByUserID(ctx context.Context, userID uuid.UUID) (int64, error) {
	return ss.RevokeAllUserSessions(ctx, userID)
}

// RefreshSession étend la durée d'une session
func (ss *SessionService) RefreshSession(ctx context.Context, token string, newExpiresIn time.Duration) error {
	tokenHash := ss.hashToken(token)
	newExpiresAt := time.Now().Add(newExpiresIn)

	query := `
		UPDATE sessions 
		SET expires_at = $1
		WHERE token_hash = $2 AND revoked_at IS NULL AND expires_at > $3
	`

	result, err := ss.db.ExecContext(ctx, query, newExpiresAt, tokenHash, time.Now())
	if err != nil {
		ss.logger.Error("Failed to refresh session",
			zap.Error(err),
			zap.String("token_hash", tokenHash),
		)
		return fmt.Errorf("failed to refresh session: %w", err)
	}

	rowsAffected, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("failed to get rows affected: %w", err)
	}

	if rowsAffected == 0 {
		return fmt.Errorf("session not found or expired")
	}

	ss.logger.Info("Session refreshed",
		zap.String("token_hash", tokenHash),
		zap.Time("new_expires_at", newExpiresAt),
	)

	return nil
}

// CleanupExpiredSessions supprime les sessions expirées
func (ss *SessionService) CleanupExpiredSessions(ctx context.Context) error {
	query := `
		DELETE FROM sessions 
		WHERE expires_at < $1 OR revoked_at IS NOT NULL
	`

	result, err := ss.db.ExecContext(ctx, query, time.Now())
	if err != nil {
		ss.logger.Error("Failed to cleanup expired sessions", zap.Error(err))
		return fmt.Errorf("failed to cleanup expired sessions: %w", err)
	}

	rowsAffected, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("failed to get rows affected: %w", err)
	}

	if rowsAffected > 0 {
		ss.logger.Info("Expired sessions cleaned up", zap.Int64("count", rowsAffected))
	}

	return nil
}

// hashToken hashe un token pour le stockage
func (ss *SessionService) hashToken(token string) string {
	hash := sha256.Sum256([]byte(token))
	return hex.EncodeToString(hash[:])
}

// GetSessionStats retourne les statistiques des sessions
func (ss *SessionService) GetSessionStats(ctx context.Context) (map[string]interface{}, error) {
	query := `
		SELECT 
			COUNT(*) as total_active,
			COUNT(DISTINCT user_id) as unique_users
		FROM sessions
		WHERE expires_at > $1 AND revoked_at IS NULL
	`

	var totalActive, uniqueUsers int64
	err := ss.db.QueryRowContext(ctx, query, time.Now()).Scan(&totalActive, &uniqueUsers)
	if err != nil {
		return nil, fmt.Errorf("failed to get session stats: %w", err)
	}

	return map[string]interface{}{
		"total_active": totalActive,
		"unique_users": uniqueUsers,
	}, nil
}

// GetSessionByID récupère une session par ID
func (ss *SessionService) GetSessionByID(sessionID uuid.UUID) (*Session, error) {
	ctx := context.Background()
	query := `
		SELECT id, user_id, token_hash, created_at, expires_at, revoked_at, ip_address, user_agent
		FROM sessions
		WHERE id = $1
	`

	var session Session
	err := ss.db.QueryRowContext(ctx, query, sessionID).Scan(
		&session.ID,
		&session.UserID,
		&session.TokenHash,
		&session.CreatedAt,
		&session.ExpiresAt,
		&session.RevokedAt,
		&session.IPAddress,
		&session.UserAgent,
	)

	if err != nil {
		if err == sql.ErrNoRows {
			return nil, fmt.Errorf("session not found")
		}
		ss.logger.Error("Failed to get session by ID",
			zap.Error(err),
			zap.String("session_id", sessionID.String()),
		)
		return nil, fmt.Errorf("failed to get session by ID: %w", err)
	}

	return &session, nil
}

// GetUserSessions récupère toutes les sessions d'un utilisateur
func (ss *SessionService) GetUserSessions(userID uuid.UUID) ([]*Session, error) {
	ctx := context.Background()
	query := `
		SELECT id, user_id, token_hash, created_at, expires_at, revoked_at, ip_address, user_agent
		FROM sessions
		WHERE user_id = $1 AND expires_at > $2 AND revoked_at IS NULL
		ORDER BY created_at DESC
	`

	rows, err := ss.db.QueryContext(ctx, query, userID, time.Now())
	if err != nil {
		ss.logger.Error("Failed to get user sessions",
			zap.Error(err),
			zap.String("user_id", userID.String()),
		)
		return nil, fmt.Errorf("failed to get user sessions: %w", err)
	}
	defer rows.Close()

	var sessions []*Session
	for rows.Next() {
		var session Session
		if err := rows.Scan(
			&session.ID,
			&session.UserID,
			&session.TokenHash,
			&session.CreatedAt,
			&session.ExpiresAt,
			&session.RevokedAt,
			&session.IPAddress,
			&session.UserAgent,
		); err != nil {
			return nil, fmt.Errorf("failed to scan session: %w", err)
		}
		sessions = append(sessions, &session)
	}

	return sessions, nil
}

// HashTokenForMiddleware hashe un token (pour usage middleware/handler)
func (ss *SessionService) HashTokenForMiddleware(token string) string {
	return ss.hashToken(token)
}

// DeleteSession révoque une session (alias pour RevokeSession, utilisé par les handlers)
func (ss *SessionService) DeleteSession(tokenHash string) error {
	// Note: tokenHash is already hashed. RevokeSession expects raw token.
	// But DeleteSession takes tokenHash.
	// We need a method to revoke by hash.

	ctx := context.Background()
	query := `
		UPDATE sessions 
		SET revoked_at = $2
		WHERE token_hash = $1 AND revoked_at IS NULL
	`

	result, err := ss.db.ExecContext(ctx, query, tokenHash, time.Now())
	if err != nil {
		ss.logger.Error("Failed to revoke session by hash",
			zap.Error(err),
			zap.String("token_hash", tokenHash),
		)
		return fmt.Errorf("failed to revoke session: %w", err)
	}

	rowsAffected, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("failed to get rows affected: %w", err)
	}

	if rowsAffected == 0 {
		return fmt.Errorf("session not found or already revoked")
	}

	ss.logger.Info("Session revoked by hash",
		zap.String("token_hash", tokenHash),
	)

	return nil
}
