// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package statestore

import (
	"context"
	"log/slog"
	"strconv"
	"time"

	"github.com/Azure/iot-operations-sdks/go/internal/options"
	"github.com/Azure/iot-operations-sdks/go/protocol"
	"github.com/Azure/iot-operations-sdks/go/protocol/hlc"
	"github.com/Azure/iot-operations-sdks/go/services/statestore/internal/resp"
)

type (
	// SetOption represents a single option for the Set method.
	SetOption interface{ set(*SetOptions) }

	// SetOptions are the resolved options for the Set method.
	SetOptions struct {
		Expiry       time.Duration
		Condition    Condition
		FencingToken hlc.HybridLogicalClock
		Timeout      time.Duration
		Persist      bool
	}
)

// Set the value of the given key. If the key is successfully set, it returns
// true and the new or updated version; if the key is not set due to the
// specified condition, it returns false and the stored version.
func (c *Client[K, V]) Set(
	ctx context.Context,
	key K,
	val V,
	opt ...SetOption,
) (res *Response[bool], err error) {
	defer func() { c.logReturn(ctx, err) }()
	if len(key) == 0 {
		return nil, ArgumentError{Name: "key"}
	}

	var opts SetOptions
	opts.Apply(opt)

	var rest []string
	var attr []slog.Attr
	if opts.Condition != Always {
		rest = append(rest, string(opts.Condition))
		attr = append(attr, slog.String("condition", string(opts.Condition)))
	}
	switch {
	case opts.Expiry < 0:
		return nil, ArgumentError{Name: "Expiry", Value: opts.Expiry}
	case opts.Expiry > 0:
		rest = append(rest, "PX", strconv.Itoa(int(opts.Expiry.Milliseconds())))
		attr = append(attr, slog.Duration("expiry", opts.Expiry))
	}

	c.logOp(ctx, "SET", key, attr...)
	req := resp.OpKV("SET", key, val, rest...)
	return invoke(ctx, c.invoker, parseOK, &opts, req)
}

// Apply resolves the provided list of options.
func (o *SetOptions) Apply(opts []SetOption, rest ...SetOption) {
	for opt := range options.Apply[SetOption](opts, rest...) {
		opt.set(o)
	}
}

func (o *SetOptions) set(opt *SetOptions) {
	if o != nil {
		*opt = *o
	}
}

func (o WithCondition) set(opt *SetOptions) {
	opt.Condition = Condition(o)
}

// Allow Condition to be used directly as an option for convenience.
func (o Condition) set(opt *SetOptions) {
	opt.Condition = o
}

func (o WithExpiry) set(opt *SetOptions) {
	opt.Expiry = time.Duration(o)
}

func (o WithFencingToken) set(opt *SetOptions) {
	opt.FencingToken = hlc.HybridLogicalClock(o)
}

func (o WithTimeout) set(opt *SetOptions) {
	opt.Timeout = time.Duration(o)
}

func (o WithPersist) set(opt *SetOptions) {
	opt.Persist = bool(o)
}

func (o *SetOptions) invoke() *protocol.InvokeOptions {
	inv := &protocol.InvokeOptions{
		Timeout: o.Timeout,
	}
	if !o.FencingToken.IsZero() {
		if inv.Metadata == nil {
			inv.Metadata = make(map[string]string, 1)
		}
		inv.Metadata[fencingToken] = o.FencingToken.String()
	}
	if o.Persist {
		if inv.Metadata == nil {
			inv.Metadata = make(map[string]string, 1)
		}
		inv.Metadata["aio-persistence"] = "true"
	}
	return inv
}
