package sessions

import (
	"fmt"

	"github.com/jackc/pgtype"
	"github.com/lib/pq"

	"openreplay/backend/pkg/db/postgres/pool"
)

type Storage interface {
	Add(sess *Session) error
	Get(sessionID uint64) (*Session, error)
	GetMany(sessionIDs []uint64) ([]*Session, error)
	GetDuration(sessionID uint64) (uint64, error)
	UpdateDuration(sessionID uint64, timestamp uint64) (uint64, error)
	InsertEncryptionKey(sessionID uint64, key []byte) error
	InsertUserID(sessionID uint64, userID string) error
	InsertUserAnonymousID(sessionID uint64, userAnonymousID string) error
	InsertReferrer(sessionID uint64, referrer, baseReferrer string) error
	InsertMetadata(sessionID uint64, keyNo uint, value string) error
}

type storageImpl struct {
	db pool.Pool
}

func NewStorage(db pool.Pool) Storage {
	return &storageImpl{
		db: db,
	}
}

func (s *storageImpl) Add(sess *Session) error {
	return s.db.Exec(`
		INSERT INTO sessions (
			session_id, project_id, start_ts,
			user_uuid, user_device, user_device_type, user_country,
			user_os, user_os_version,
			rev_id, 
			tracker_version, issue_score,
			platform,
			user_browser, user_browser_version, user_device_memory_size, user_device_heap_size,
			user_id, user_state, user_city, timezone, screen_width, screen_height
		) VALUES (
			$1, $2, $3,
			$4, $5, $6, $7, 
			$8, NULLIF($9, ''),
			NULLIF($10, ''), 
			$11, $12,
			$13,
			NULLIF($14, ''), NULLIF($15, ''), NULLIF($16, 0), NULLIF($17, 0::bigint),
			NULLIF(LEFT($18, 8000), ''), NULLIF($19, ''), NULLIF($20, ''), $21, $22, $23
		)`,
		sess.SessionID, sess.ProjectID, sess.Timestamp,
		sess.UserUUID, sess.UserDevice, sess.UserDeviceType, sess.UserCountry,
		sess.UserOS, sess.UserOSVersion,
		sess.RevID,
		sess.TrackerVersion, sess.Timestamp/1000,
		sess.Platform,
		sess.UserBrowser, sess.UserBrowserVersion, sess.UserDeviceMemorySize, sess.UserDeviceHeapSize,
		sess.UserID, sess.UserState, sess.UserCity, sess.Timezone, sess.ScreenWidth, sess.ScreenHeight,
	)
}

func (s *storageImpl) Get(sessionID uint64) (*Session, error) {
	sess := &Session{SessionID: sessionID}
	var revID, userOSVersion, userBrowser, userBrowserVersion, userState, userCity *string
	var issueTypes pgtype.EnumArray
	if err := s.db.QueryRow(`
		SELECT platform,
			duration, project_id, start_ts, timezone,
			user_uuid, user_os, user_os_version, 
			user_device, user_device_type, user_country, user_state, user_city,
			rev_id, tracker_version,
			user_id, user_anonymous_id, referrer,
			pages_count, events_count, errors_count, issue_types,
			user_browser, user_browser_version, issue_score,
			metadata_1, metadata_2, metadata_3, metadata_4, metadata_5,
			metadata_6, metadata_7, metadata_8, metadata_9, metadata_10,
			utm_source, utm_medium, utm_campaign
		FROM sessions
		WHERE session_id=$1 
	`,
		sessionID,
	).Scan(&sess.Platform,
		&sess.Duration, &sess.ProjectID, &sess.Timestamp, &sess.Timezone,
		&sess.UserUUID, &sess.UserOS, &userOSVersion,
		&sess.UserDevice, &sess.UserDeviceType, &sess.UserCountry, &userState, &userCity,
		&revID, &sess.TrackerVersion,
		&sess.UserID, &sess.UserAnonymousID, &sess.Referrer,
		&sess.PagesCount, &sess.EventsCount, &sess.ErrorsCount, &issueTypes,
		&userBrowser, &userBrowserVersion, &sess.IssueScore,
		&sess.Metadata1, &sess.Metadata2, &sess.Metadata3, &sess.Metadata4, &sess.Metadata5,
		&sess.Metadata6, &sess.Metadata7, &sess.Metadata8, &sess.Metadata9, &sess.Metadata10,
		&sess.UtmSource, &sess.UtmMedium, &sess.UtmCampaign); err != nil {
		return nil, err
	}
	if userOSVersion != nil {
		sess.UserOSVersion = *userOSVersion
	}
	if userBrowser != nil {
		sess.UserBrowser = *userBrowser
	}
	if userBrowserVersion != nil {
		sess.UserBrowserVersion = *userBrowserVersion
	}
	if revID != nil {
		sess.RevID = *revID
	}
	issueTypes.AssignTo(&sess.IssueTypes)
	if userState != nil {
		sess.UserState = *userState
	}
	if userCity != nil {
		sess.UserCity = *userCity
	}
	return sess, nil
}

// For the ender service only
func (s *storageImpl) GetMany(sessionIDs []uint64) ([]*Session, error) {
	rows, err := s.db.Query("SELECT session_id, COALESCE( duration, 0 ), start_ts FROM sessions WHERE session_id = ANY($1)", pq.Array(sessionIDs))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	sessions := make([]*Session, 0, len(sessionIDs))
	for rows.Next() {
		sess := &Session{}
		if err := rows.Scan(&sess.SessionID, &sess.Duration, &sess.Timestamp); err != nil {
			return nil, err
		}
		sessions = append(sessions, sess)
	}
	return sessions, nil
}

func (s *storageImpl) GetDuration(sessionID uint64) (uint64, error) {
	var dur uint64
	if err := s.db.QueryRow("SELECT COALESCE( duration, 0 ) FROM sessions WHERE session_id=$1", sessionID).Scan(&dur); err != nil {
		return 0, err
	}
	return dur, nil
}

func (s *storageImpl) UpdateDuration(sessionID uint64, timestamp uint64) (uint64, error) {
	var dur uint64
	if err := s.db.QueryRow(`
		UPDATE sessions SET duration=$2 - start_ts
		WHERE session_id=$1
		RETURNING duration
	`,
		sessionID, timestamp,
	).Scan(&dur); err != nil {
		return 0, err
	}
	return dur, nil
}

func (s *storageImpl) InsertEncryptionKey(sessionID uint64, key []byte) error {
	sqlRequest := `
		UPDATE sessions 
		SET file_key = $2 
		WHERE session_id = $1`
	return s.db.Exec(sqlRequest, sessionID, string(key))
}

func (s *storageImpl) InsertUserID(sessionID uint64, userID string) error {
	sqlRequest := `
		UPDATE sessions 
		SET user_id = LEFT($1, 8000) 
		WHERE session_id = $2`
	return s.db.Exec(sqlRequest, userID, sessionID)
}

func (s *storageImpl) InsertUserAnonymousID(sessionID uint64, userAnonymousID string) error {
	sqlRequest := `
		UPDATE sessions 
		SET user_anonymous_id = LEFT($1, 8000) 
		WHERE session_id = $2`
	return s.db.Exec(sqlRequest, userAnonymousID, sessionID)
}

func (s *storageImpl) InsertReferrer(sessionID uint64, referrer, baseReferrer string) error {
	sqlRequest := `
		UPDATE sessions 
		SET referrer = LEFT($1, 8000), base_referrer = LEFT($2, 8000) 
		WHERE session_id = $3 AND referrer IS NULL`
	return s.db.Exec(sqlRequest, referrer, baseReferrer, sessionID)
}

func (s *storageImpl) InsertMetadata(sessionID uint64, keyNo uint, value string) error {
	sqlRequest := `
		UPDATE sessions 
		SET metadata_%v = LEFT($1, 8000) 
		WHERE session_id = $2`
	return s.db.Exec(fmt.Sprintf(sqlRequest, keyNo), value, sessionID)
}
