package anonstore

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/grafana/grafana/pkg/infra/db"
	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/services/search/model"
	"github.com/grafana/grafana/pkg/services/sqlstore"
	"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)

const cacheKeyPrefix = "anon-device"
const anonymousDeviceExpiration = 30 * 24 * time.Hour
const tableName = "anon_device"

var ErrDeviceLimitReached = fmt.Errorf("device limit reached")

type AnonDBStore struct {
	sqlStore    db.DB
	log         log.Logger
	deviceLimit int64
}

type Device struct {
	ID        int64     `json:"-" xorm:"pk autoincr 'id'" db:"id"`
	DeviceID  string    `json:"deviceId" xorm:"device_id" db:"device_id"`
	ClientIP  string    `json:"clientIp" xorm:"client_ip" db:"client_ip"`
	UserAgent string    `json:"userAgent" xorm:"user_agent" db:"user_agent"`
	CreatedAt time.Time `json:"createdAt" xorm:"created_at" db:"created_at"`
	UpdatedAt time.Time `json:"updatedAt" xorm:"updated_at" db:"updated_at"`
}

type DeviceSearchHitDTO struct {
	DeviceID   string    `json:"deviceId" xorm:"device_id" db:"device_id"`
	ClientIP   string    `json:"clientIp" xorm:"client_ip" db:"client_ip"`
	UserAgent  string    `json:"userAgent" xorm:"user_agent" db:"user_agent"`
	CreatedAt  time.Time `json:"createdAt" xorm:"created_at" db:"created_at"`
	UpdatedAt  time.Time `json:"updatedAt" xorm:"updated_at" db:"updated_at"`
	LastSeenAt time.Time `json:"lastSeenAt"`
}

type SearchDeviceQueryResult struct {
	TotalCount int64                 `json:"totalCount"`
	Devices    []*DeviceSearchHitDTO `json:"devices"`
	Page       int                   `json:"page"`
	PerPage    int                   `json:"perPage"`
}
type SearchDeviceQuery struct {
	Query    string
	Page     int
	Limit    int
	From     time.Time
	To       time.Time
	SortOpts []model.SortOption
}

func (a *Device) CacheKey() string {
	return strings.Join([]string{cacheKeyPrefix, a.DeviceID}, ":")
}

type AnonStore interface {
	// ListDevices returns all devices that have been updated between the given times.
	ListDevices(ctx context.Context, from *time.Time, to *time.Time) ([]*Device, error)
	// CreateOrUpdateDevice creates or updates a device.
	CreateOrUpdateDevice(ctx context.Context, device *Device) error
	// CountDevices returns the number of devices that have been updated between the given times.
	CountDevices(ctx context.Context, from time.Time, to time.Time) (int64, error)
	// DeleteDevice deletes a device by its ID.
	DeleteDevice(ctx context.Context, deviceID string) error
	// DeleteDevicesOlderThan deletes all devices that have no been updated since the given time.
	DeleteDevicesOlderThan(ctx context.Context, olderThan time.Time) error
	// SearchDevices searches for devices within the 30 days active.
	SearchDevices(ctx context.Context, query *SearchDeviceQuery) (*SearchDeviceQueryResult, error)
}

func ProvideAnonDBStore(sqlStore db.DB, deviceLimit int64) *AnonDBStore {
	return &AnonDBStore{sqlStore: sqlStore, log: log.New("anonstore"), deviceLimit: deviceLimit}
}

func (s *AnonDBStore) ListDevices(ctx context.Context, from *time.Time, to *time.Time) ([]*Device, error) {
	devices := []*Device{}
	query := "SELECT * FROM anon_device"
	args := []any{}
	if from != nil && to != nil {
		query += " WHERE updated_at BETWEEN ? AND ?"
		args = append(args, from.UTC(), to.UTC())
	}

	err := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
		return dbSession.SQL(query, args...).Find(&devices)
	})

	return devices, err
}

// updateDevice updates a device if it exists and has been updated between the given times.
func (s *AnonDBStore) updateDevice(ctx context.Context, device *Device) error {
	const query = `UPDATE anon_device SET
client_ip = ?,
user_agent = ?,
updated_at = ?
WHERE device_id = ? AND updated_at BETWEEN ? AND ?`

	args := []interface{}{device.ClientIP, device.UserAgent, device.UpdatedAt.UTC(), device.DeviceID,
		device.UpdatedAt.UTC().Add(-anonymousDeviceExpiration), device.UpdatedAt.UTC().Add(time.Minute),
	}
	err := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
		args = append([]interface{}{query}, args...)
		result, err := dbSession.Exec(args...)
		if err != nil {
			return err
		}

		rowsAffected, err := result.RowsAffected()
		if err != nil {
			return err
		}

		if rowsAffected == 0 {
			return ErrDeviceLimitReached
		}

		return nil
	})

	return err
}

func (s *AnonDBStore) CreateOrUpdateDevice(ctx context.Context, device *Device) error {
	var query string

	// if device limit is reached, only update devices
	if s.deviceLimit > 0 {
		count, err := s.CountDevices(ctx, time.Now().UTC().Add(-anonymousDeviceExpiration), time.Now().UTC().Add(time.Minute))
		if err != nil {
			return err
		}

		if count >= s.deviceLimit {
			return s.updateDevice(ctx, device)
		}
	}

	// If CreatedAt time is not set (i.e. it's zero), and we end up creating the device, use current time as creation time.
	// If database converts zero time to NULL, but CreatedAt is not nullable, this helps to fix that problem too.
	created := device.CreatedAt
	if created.IsZero() {
		created = time.Now()
	}

	args := []any{device.DeviceID, device.ClientIP, device.UserAgent, created.UTC(), device.UpdatedAt.UTC()}
	switch s.sqlStore.GetDBType() {
	case migrator.Postgres:
		query = `INSERT INTO anon_device (device_id, client_ip, user_agent, created_at, updated_at)
					VALUES ($1, $2, $3, $4, $5)
					ON CONFLICT (device_id) DO UPDATE SET
					client_ip = $2,
					user_agent = $3,
					updated_at = $5
					RETURNING id`
	case migrator.MySQL:
		query = `INSERT INTO anon_device (device_id, client_ip, user_agent, created_at, updated_at)
					VALUES (?, ?, ?, ?, ?)
					ON DUPLICATE KEY UPDATE
					client_ip = VALUES(client_ip),
					user_agent = VALUES(user_agent),
					updated_at = VALUES(updated_at)`
	case migrator.SQLite:
		query = `INSERT INTO anon_device (device_id, client_ip, user_agent, created_at, updated_at)
					VALUES (?, ?, ?, ?, ?)
					ON CONFLICT (device_id) DO UPDATE SET
					client_ip = excluded.client_ip,
					user_agent = excluded.user_agent,
					updated_at = excluded.updated_at`
	default:
		return fmt.Errorf("unsupported database driver: %s", s.sqlStore.GetDBType())
	}

	err := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
		args = append([]any{query}, args...)
		_, err := dbSession.Exec(args...)
		return err
	})

	return err
}

func (s *AnonDBStore) CountDevices(ctx context.Context, from time.Time, to time.Time) (int64, error) {
	var count int64
	err := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
		_, err := dbSession.SQL("SELECT COUNT(*) FROM anon_device WHERE updated_at BETWEEN ? AND ?", from.UTC(), to.UTC()).Get(&count)
		return err
	})

	return count, err
}

func (s *AnonDBStore) DeleteDevice(ctx context.Context, deviceID string) error {
	err := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
		_, err := dbSession.Exec("DELETE FROM anon_device WHERE device_id = ?", deviceID)
		return err
	})

	return err
}

// deleteOldDevices deletes all devices that have no been updated since the given time.
func (s *AnonDBStore) DeleteDevicesOlderThan(ctx context.Context, olderThan time.Time) error {
	err := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
		_, err := dbSession.Exec("DELETE FROM anon_device WHERE updated_at <= ?", olderThan.UTC())
		return err
	})

	return err
}

func (s *AnonDBStore) SearchDevices(ctx context.Context, query *SearchDeviceQuery) (*SearchDeviceQueryResult, error) {
	result := SearchDeviceQueryResult{
		Devices: make([]*DeviceSearchHitDTO, 0),
	}
	err := s.sqlStore.WithDbSession(ctx, func(dbSess *db.Session) error {
		if query.From.IsZero() && !query.To.IsZero() {
			return fmt.Errorf("from date must be set if to date is set")
		}
		if !query.From.IsZero() && query.To.IsZero() {
			return fmt.Errorf("to date must be set if from date is set")
		}

		// restricted only to last 30 days, if noting else specified
		if query.From.IsZero() && query.To.IsZero() {
			query.From = time.Now().Add(-anonymousDeviceExpiration)
			query.To = time.Now()
		}

		sess := dbSess.Table(tableName).Alias("d")

		if query.Limit > 0 {
			offset := query.Limit * (query.Page - 1)
			sess.Limit(query.Limit, offset)
		}
		sess.Cols("d.id", "d.device_id", "d.client_ip", "d.user_agent", "d.updated_at")

		if len(query.SortOpts) > 0 {
			for i := range query.SortOpts {
				for j := range query.SortOpts[i].Filter {
					sess.OrderBy(query.SortOpts[i].Filter[j].OrderBy())
				}
			}
		} else {
			sess.Asc("d.user_agent")
		}

		// add to query about from and to session
		sess.Where("d.updated_at BETWEEN ? AND ?", query.From.UTC(), query.To.UTC())

		if query.Query != "" {
			sql, param := s.sqlStore.GetDialect().LikeOperator("d.client_ip", true, strings.ReplaceAll(query.Query, "\\", ""), true)
			sess.Where(sql, param)
		}

		// get total
		devices, err := s.ListDevices(ctx, &query.From, &query.To)
		if err != nil {
			return err
		}
		// cast to int64
		result.TotalCount = int64(len(devices))
		if err := sess.Find(&result.Devices); err != nil {
			return err
		}
		result.Page = query.Page
		result.PerPage = query.Limit
		return nil
	})
	return &result, err
}
