// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: MPL-2.0

// Package clientcountutil provides a library to generate activity log data for
// testing.
package clientcountutil

import (
	"context"
	"errors"
	"fmt"

	"github.com/hashicorp/vault/api"
	"github.com/hashicorp/vault/sdk/helper/clientcountutil/generation"
	"google.golang.org/protobuf/encoding/protojson"
)

// ActivityLogDataGenerator holds an ActivityLogMockInput. Users can create the
// generator with NewActivityLogData(), add content to the generator using
// the fluent API methods, and generate and write the JSON representation of the
// input to the Vault API.
type ActivityLogDataGenerator struct {
	data            *generation.ActivityLogMockInput
	addingToMonth   *generation.Data
	addingToSegment *generation.Segment
	client          *api.Client
}

// NewActivityLogData creates a new instance of an activity log data generator
// The type returned by this function cannot be called concurrently
func NewActivityLogData(client *api.Client) *ActivityLogDataGenerator {
	return &ActivityLogDataGenerator{
		client: client,
		data:   new(generation.ActivityLogMockInput),
	}
}

// NewCurrentMonthData opens a new month of data for the current month. All
// clients will continue to be added to this month until a new month is created
// with NewPreviousMonthData.
func (d *ActivityLogDataGenerator) NewCurrentMonthData() *ActivityLogDataGenerator {
	return d.newMonth(&generation.Data{Month: &generation.Data_CurrentMonth{CurrentMonth: true}})
}

// NewPreviousMonthData opens a new month of data, where the clients will be
// recorded as having been seen monthsAgo months ago. All clients will continue
// to be added to this month until a new month is created with
// NewPreviousMonthData or NewCurrentMonthData.
func (d *ActivityLogDataGenerator) NewPreviousMonthData(monthsAgo int) *ActivityLogDataGenerator {
	return d.newMonth(&generation.Data{Month: &generation.Data_MonthsAgo{MonthsAgo: int32(monthsAgo)}})
}

func (d *ActivityLogDataGenerator) newMonth(newMonth *generation.Data) *ActivityLogDataGenerator {
	d.data.Data = append(d.data.Data, newMonth)
	d.addingToMonth = newMonth
	d.addingToSegment = nil
	return d
}

// MonthOption holds an option that can be set for the entire month
type MonthOption func(m *generation.Data)

// WithMaximumSegmentIndex sets the maximum segment index for the segments in
// the open month. Set this value in order to set how many indexes the data
// should be split across. This must include any empty or skipped indexes. For
// example, say that you would like all of your data split across indexes 0 and
// 3, with the following empty and skipped indexes:
//
//	empty indexes: [2]
//	skipped indexes: [1]
//
// To accomplish that, you will need to call WithMaximumSegmentIndex(3).
// This value will be ignored if you have called Segment() for the open month
// If not set, all data will be in 1 segment.
func WithMaximumSegmentIndex(n int) MonthOption {
	return func(m *generation.Data) {
		m.NumSegments = int32(n)
	}
}

// WithEmptySegmentIndexes sets which segment indexes should be empty for the
// segments in the open month. If you use this option, you must either:
//  1. ensure that you've called Segment() for the open month
//  2. use WithMaximumSegmentIndex() to set the total number of segments
//
// If you haven't set either of those values then this option will be ignored,
// unless you included 0 as an empty segment index in which case only an empty
// segment will be created.
func WithEmptySegmentIndexes(i ...int) MonthOption {
	return func(m *generation.Data) {
		indexes := make([]int32, 0, len(i))
		for _, index := range i {
			indexes = append(indexes, int32(index))
		}
		m.EmptySegmentIndexes = indexes
	}
}

// WithSkipSegmentIndexes sets which segment indexes should be skipped for the
// segments in the open month. If you use this option, you must either:
//  1. ensure that you've called Segment() for the open month
//  2. use WithMaximumSegmentIndex() to set the total number of segments
//
// If you haven't set either of those values then this option will be ignored,
// unless you included 0 as a skipped segment index in which case no segments
// will be created.
func WithSkipSegmentIndexes(i ...int) MonthOption {
	return func(m *generation.Data) {
		indexes := make([]int32, 0, len(i))
		for _, index := range i {
			indexes = append(indexes, int32(index))
		}
		m.SkipSegmentIndexes = indexes
	}
}

// SetMonthOptions can be called at any time to set options for the open month
func (d *ActivityLogDataGenerator) SetMonthOptions(opts ...MonthOption) *ActivityLogDataGenerator {
	for _, opt := range opts {
		opt(d.addingToMonth)
	}
	return d
}

// ClientOption defines additional options for the client
// This type and the functions that return it are here for ease of use. A user
// could also choose to create the *generation.Client themselves, without using
// a ClientOption
type ClientOption func(client *generation.Client)

// WithClientNamespace sets the namespace for the client
func WithClientNamespace(n string) ClientOption {
	return func(client *generation.Client) {
		client.Namespace = n
	}
}

// WithClientMount sets the mount path for the client
func WithClientMount(m string) ClientOption {
	return func(client *generation.Client) {
		client.Mount = m
	}
}

// WithClientIsNonEntity sets whether the client is an entity client or a non-
// entity token client
func WithClientIsNonEntity() ClientOption {
	return WithClientType("non-entity")
}

// WithClientType sets the client type to the given string. If this client type
// is not "entity", then the client will be counted in the activity log as a
// non-entity client
func WithClientType(typ string) ClientOption {
	return func(client *generation.Client) {
		client.ClientType = typ
	}
}

// WithClientID sets the ID for the client
func WithClientID(id string) ClientOption {
	return func(client *generation.Client) {
		client.Id = id
	}
}

// ClientsSeen adds clients to the month that was most recently opened with
// NewPreviousMonthData or NewCurrentMonthData.
func (d *ActivityLogDataGenerator) ClientsSeen(clients ...*generation.Client) *ActivityLogDataGenerator {
	if d.addingToSegment == nil {
		if d.addingToMonth.Clients == nil {
			d.addingToMonth.Clients = &generation.Data_All{All: &generation.Clients{}}
		}
		d.addingToMonth.GetAll().Clients = append(d.addingToMonth.GetAll().Clients, clients...)
		return d
	}
	d.addingToSegment.Clients.Clients = append(d.addingToSegment.Clients.Clients, clients...)
	return d
}

// NewClientSeen adds 1 new client with the given options to the most recently
// opened month.
func (d *ActivityLogDataGenerator) NewClientSeen(opts ...ClientOption) *ActivityLogDataGenerator {
	return d.NewClientsSeen(1, opts...)
}

// NewClientsSeen adds n new clients with the given options to the most recently
// opened month.
func (d *ActivityLogDataGenerator) NewClientsSeen(n int, opts ...ClientOption) *ActivityLogDataGenerator {
	c := new(generation.Client)
	for _, opt := range opts {
		opt(c)
	}
	c.Count = int32(n)
	return d.ClientsSeen(c)
}

// RepeatedClientSeen adds 1 client that was seen in the previous month to
// the month that was most recently opened. This client will have the attributes
// described by the provided options.
func (d *ActivityLogDataGenerator) RepeatedClientSeen(opts ...ClientOption) *ActivityLogDataGenerator {
	return d.RepeatedClientsSeen(1, opts...)
}

// RepeatedClientsSeen adds n clients that were seen in the previous month to
// the month that was most recently opened. These clients will have the
// attributes described by provided options.
func (d *ActivityLogDataGenerator) RepeatedClientsSeen(n int, opts ...ClientOption) *ActivityLogDataGenerator {
	c := new(generation.Client)
	for _, opt := range opts {
		opt(c)
	}
	c.Repeated = true
	c.Count = int32(n)
	return d.ClientsSeen(c)
}

// RepeatedClientSeenFromMonthsAgo adds 1 client that was seen in monthsAgo
// month to the month that was most recently opened. This client will have the
// attributes described by provided options.
func (d *ActivityLogDataGenerator) RepeatedClientSeenFromMonthsAgo(monthsAgo int, opts ...ClientOption) *ActivityLogDataGenerator {
	return d.RepeatedClientsSeenFromMonthsAgo(1, monthsAgo, opts...)
}

// RepeatedClientsSeenFromMonthsAgo adds n clients that were seen in monthsAgo
// month to the month that was most recently opened. These clients will have the
// attributes described by provided options.
func (d *ActivityLogDataGenerator) RepeatedClientsSeenFromMonthsAgo(n, monthsAgo int, opts ...ClientOption) *ActivityLogDataGenerator {
	c := new(generation.Client)
	for _, opt := range opts {
		opt(c)
	}
	c.RepeatedFromMonth = int32(monthsAgo)
	c.Count = int32(n)
	return d.ClientsSeen(c)
}

// SegmentOption defines additional options for the segment
type SegmentOption func(segment *generation.Segment)

// WithSegmentIndex sets the index for the segment to n. If this option is not
// provided, the segment will be given the next consecutive index
func WithSegmentIndex(n int) SegmentOption {
	return func(segment *generation.Segment) {
		index := int32(n)
		segment.SegmentIndex = &index
	}
}

// Segment starts a segment within the current month. All clients will be added
// to this segment, until either Segment is called again to create a new open
// segment, or NewPreviousMonthData or NewCurrentMonthData is called to open a
// new month.
func (d *ActivityLogDataGenerator) Segment(opts ...SegmentOption) *ActivityLogDataGenerator {
	s := &generation.Segment{
		Clients: &generation.Clients{},
	}
	for _, opt := range opts {
		opt(s)
	}
	if d.addingToMonth.GetSegments() == nil {
		d.addingToMonth.Clients = &generation.Data_Segments{Segments: &generation.Segments{}}
	}
	d.addingToMonth.GetSegments().Segments = append(d.addingToMonth.GetSegments().Segments, s)
	d.addingToSegment = s
	return d
}

// ToJSON returns the JSON representation of the data
func (d *ActivityLogDataGenerator) ToJSON(writeOptions ...generation.WriteOptions) ([]byte, error) {
	if len(writeOptions) > 0 {
		d.data.Write = writeOptions
	}
	return protojson.Marshal(d.data)
}

// ToProto returns the ActivityLogMockInput protobuf
func (d *ActivityLogDataGenerator) ToProto() *generation.ActivityLogMockInput {
	return d.data
}

// Write writes the data to the API with the given write options. The method
// returns the new paths that have been written. Note that the API endpoint will
// only be present when Vault has been compiled with the "testonly" flag.
func (d *ActivityLogDataGenerator) Write(ctx context.Context, writeOptions ...generation.WriteOptions) ([]string, error) {
	d.data.Write = writeOptions
	err := VerifyInput(d.data)
	if err != nil {
		return nil, err
	}
	data, err := d.ToJSON()
	if err != nil {
		return nil, err
	}
	resp, err := d.client.Logical().WriteWithContext(ctx, "sys/internal/counters/activity/write", map[string]interface{}{"input": string(data)})
	if err != nil {
		return nil, err
	}
	if resp.Data == nil {
		return nil, fmt.Errorf("received no data")
	}
	paths := resp.Data["paths"]
	castedPaths, ok := paths.([]interface{})
	if !ok {
		return nil, fmt.Errorf("invalid paths data: %v", paths)
	}
	returnPaths := make([]string, 0, len(castedPaths))
	for _, path := range castedPaths {
		returnPaths = append(returnPaths, path.(string))
	}
	return returnPaths, nil
}

// VerifyInput checks that the input data is valid
func VerifyInput(input *generation.ActivityLogMockInput) error {
	// mapping from monthsAgo to the month's data
	months := make(map[int32]*generation.Data)

	// this keeps track of the index of the earliest month. We need to verify
	// that this month doesn't have any repeated clients
	earliestMonthsAgo := int32(0)

	// this map holds a set of the month indexes for any RepeatedFromMonth
	// values. Each element will be checked to ensure month that should be
	// repeated from exists in the input data
	repeatedFromMonths := make(map[int32]struct{})

	for _, month := range input.Data {
		monthsAgo := month.GetMonthsAgo()
		if monthsAgo > earliestMonthsAgo {
			earliestMonthsAgo = monthsAgo
		}

		// verify that no monthsAgo value is repeated
		if _, seen := months[monthsAgo]; seen {
			return fmt.Errorf("multiple months with monthsAgo %d", monthsAgo)
		}
		months[monthsAgo] = month

		// the number of segments should be correct
		if month.NumSegments > 0 && int(month.NumSegments)-len(month.GetSkipSegmentIndexes())-len(month.GetEmptySegmentIndexes()) <= 0 {
			return fmt.Errorf("number of segments %d is too small. It must be large enough to include the empty (%v) and skipped (%v) segments", month.NumSegments, month.GetSkipSegmentIndexes(), month.GetEmptySegmentIndexes())
		}

		if segments := month.GetSegments(); segments != nil {
			if month.NumSegments > 0 {
				return errors.New("cannot specify both number of segments and create segmented data")
			}

			segmentIndexes := make(map[int32]struct{})
			for _, segment := range segments.Segments {

				// collect any RepeatedFromMonth values
				for _, client := range segment.GetClients().GetClients() {
					if repeatFrom := client.RepeatedFromMonth; repeatFrom > 0 {
						repeatedFromMonths[repeatFrom] = struct{}{}
					}
				}

				// verify that no segment indexes are repeated
				segmentIndex := segment.SegmentIndex
				if segmentIndex == nil {
					continue
				}
				if _, seen := segmentIndexes[*segmentIndex]; seen {
					return fmt.Errorf("cannot have repeated segment index %d", *segmentIndex)
				}
				segmentIndexes[*segmentIndex] = struct{}{}
			}
		} else {
			for _, client := range month.GetAll().GetClients() {
				// collect any RepeatedFromMonth values
				if repeatFrom := client.RepeatedFromMonth; repeatFrom > 0 {
					repeatedFromMonths[repeatFrom] = struct{}{}
				}
			}
		}
	}

	// check that the corresponding month exists for all the RepeatedFromMonth
	// values
	for repeated := range repeatedFromMonths {
		if _, ok := months[repeated]; !ok {
			return fmt.Errorf("cannot repeat from %d months ago", repeated)
		}
	}
	// the earliest month can't have any repeated clients, because there are no
	// earlier months to repeat from
	earliestMonth := months[earliestMonthsAgo]
	repeatedClients := false
	if all := earliestMonth.GetAll(); all != nil {
		for _, client := range all.GetClients() {
			repeatedClients = repeatedClients || client.Repeated || client.RepeatedFromMonth != 0
		}
	} else {
		for _, segment := range earliestMonth.GetSegments().GetSegments() {
			for _, client := range segment.GetClients().GetClients() {
				repeatedClients = repeatedClients || client.Repeated || client.RepeatedFromMonth != 0
			}
		}
	}

	if repeatedClients {
		return fmt.Errorf("%d months ago cannot have repeated clients, because it is the earliest month", earliestMonthsAgo)
	}

	return nil
}
