// Copyright (C) The Arvados Authors. All rights reserved.
//
// SPDX-License-Identifier: AGPL-3.0

package localdb

import (
	"context"
	"time"

	"git.arvados.org/arvados.git/lib/ctrlctx"
	"git.arvados.org/arvados.git/sdk/go/arvados"
	"git.arvados.org/arvados.git/sdk/go/ctxlog"
)

var loggedLogActivityDisabled = false

func (conn *Conn) logActivity(ctx context.Context) {
	p := conn.cluster.Users.ActivityLoggingPeriod.Duration()
	if p < 1 {
		if !loggedLogActivityDisabled {
			ctxlog.FromContext(ctx).Debug("logActivity disabled by config")
			loggedLogActivityDisabled = true
		}
		return
	}
	user, _, err := ctrlctx.CurrentAuth(ctx)
	if err == ctrlctx.ErrUnauthenticated {
		ctxlog.FromContext(ctx).Debug("logActivity skipped for unauthenticated request")
		return
	} else if err != nil {
		ctxlog.FromContext(ctx).WithError(err).Error("logActivity CurrentAuth failed")
		return
	}
	now := time.Now()
	conn.activeUsersLock.Lock()
	if conn.activeUsers == nil || conn.activeUsersReset.IsZero() || conn.activeUsersReset.Before(now) {
		conn.activeUsersReset = alignedPeriod(now, p)
		conn.activeUsers = map[string]bool{}
	}
	logged := conn.activeUsers[user.UUID]
	if !logged {
		// Prevent other concurrent calls from logging about
		// this user until we finish.
		conn.activeUsers[user.UUID] = true
	}
	conn.activeUsersLock.Unlock()
	if logged {
		return
	}
	defer func() {
		// If we return without logging, reset the flag so we
		// try again on the user's next API call.
		if !logged {
			conn.activeUsersLock.Lock()
			conn.activeUsers[user.UUID] = false
			conn.activeUsersLock.Unlock()
		}
	}()

	tx, err := ctrlctx.NewTx(ctx)
	if err != nil {
		ctxlog.FromContext(ctx).WithError(err).Error("logActivity NewTx failed")
		return
	}
	defer tx.Rollback()
	_, err = tx.ExecContext(ctx, `
insert into logs
 (uuid,
  owner_uuid, modified_by_user_uuid, object_owner_uuid,
  event_type,
  summary,
  object_uuid,
  properties,
  event_at, created_at, updated_at, modified_at)
 values
 ($1, $2, $2, $2, $3, $4, $5, $6,
  current_timestamp at time zone 'UTC',
  current_timestamp at time zone 'UTC',
  current_timestamp at time zone 'UTC',
  current_timestamp at time zone 'UTC')
 returning id`,
		arvados.RandomUUID(conn.cluster.ClusterID, "57u5n"),
		conn.cluster.ClusterID+"-tpzed-000000000000000", // both modified_by and object_owner
		"activity",
		"activity of "+user.UUID,
		user.UUID,
		"{}")
	if err != nil {
		ctxlog.FromContext(ctx).WithError(err).Error("logActivity query failed")
		return
	}
	err = tx.Commit()
	if err != nil {
		ctxlog.FromContext(ctx).WithError(err).Error("logActivity commit failed")
		return
	}
	logged = true
}

// alignedPeriod computes a time interval that includes now and aligns
// to local clock times that are multiples of p. For example, if local
// time is UTC-5 and ActivityLoggingPeriod=4h, periodStart and
// periodEnd will be 0000-0400, 0400-0800, etc., in local time. If p
// is a multiple of 24h, periods will start and end at midnight.
//
// If DST starts or ends during this period, the boundaries will be
// aligned based on either DST or non-DST time depending on whether
// now is before or after the DST transition. The consequences are
// presumed to be inconsequential, e.g., logActivity may unnecessarily
// log activity more than once in a period that includes a DST
// transition.
//
// In all cases, the period ends in the future.
//
// Only the end of the period is returned.
func alignedPeriod(now time.Time, p time.Duration) time.Time {
	_, tzsec := now.Zone()
	tzoff := time.Duration(tzsec) * time.Second
	periodStart := now.Add(tzoff).Truncate(p).Add(-tzoff)
	return periodStart.Add(p)
}
