// Package recipe holds what the recipes share: arguments, connecting, printing values and the
// test documents some recipes create.
package recipe

import (
	"context"
	"fmt"
	"os"
	"slices"
	"strconv"
	"strings"
	"time"

	"github.com/M-Files/mfiles-grpc-api-samples/mfilesclient"
	"github.com/M-Files/mfiles-grpc-api/go/mfiles"
)

// ClientName is the client name the recipes log in with.
const ClientName = "M-Files gRPC recipes"

// TestPrefix starts the title of every object a recipe creates.
const TestPrefix = "[docs-test]"

// Main runs a recipe with a time limit and exits non-zero if it fails, printing the M-Files
// error code when there is one.
func Main(run func(ctx context.Context) error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
	err := run(ctx)
	cancel()
	if err == nil {
		return
	}
	if mfilesErr, ok := mfilesclient.AsError(err); ok {
		fmt.Fprintln(os.Stderr, mfilesErr) // "M-Files error 0x8004000B (Unknown): Not found."
	} else {
		fmt.Fprintln(os.Stderr, "Error:", err)
	}
	os.Exit(1)
}

// Connect logs in with the configuration in the MFILES_* environment variables.
func Connect(ctx context.Context) (*mfilesclient.Client, error) {
	cfg, err := mfilesclient.ConfigFromEnv()
	if err != nil {
		return nil, err
	}
	cfg.ClientName = ClientName
	return mfilesclient.Connect(ctx, cfg)
}

// positional is the command-line arguments that are not options such as --keep.
func positional() []string {
	var args []string
	for _, arg := range os.Args[1:] {
		if !strings.HasPrefix(arg, "-") {
			args = append(args, arg)
		}
	}
	return args
}

// IntArg is the positional argument at index as an integer, or defaultValue.
func IntArg(index int, defaultValue int32) int32 {
	if args := positional(); index < len(args) {
		value, err := strconv.ParseInt(args[index], 10, 32)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Argument %d: not an integer: %q\n", index+1, args[index])
			os.Exit(2)
		}
		return int32(value)
	}
	return defaultValue
}

// StringArg is the positional argument at index, or defaultValue.
func StringArg(index int, defaultValue string) string {
	if args := positional(); index < len(args) {
		return args[index]
	}
	return defaultValue
}

// Keep reports whether the caller asked to keep the objects the recipe creates (--keep).
func Keep() bool {
	return slices.Contains(os.Args[1:], "--keep") || slices.Contains(os.Args[1:], "-keep")
}

// Describe is one line about an object version, the same in every recipe.
func Describe(object *mfiles.ObjectVersionEx) string {
	version := object.GetVersionInfo()
	return fmt.Sprintf("%s (ID %d, version %d, modified %s UTC)", version.GetTitle(),
		object.GetObjectInfo().GetObjId().GetItemId().GetInternalId(),
		version.GetVersion().GetInternalVersion(),
		version.GetLastModifiedAtUtc().AsTime().Format(time.DateTime))
}

// [START format-value]

// FormatValue prints a typed value; lookups show their names and IDs. The value's Data holds
// one member of the TypedValueUnion oneof, as one of the TypedValueUnion_* wrapper types.
func FormatValue(value *mfiles.TypedValue) string {
	if value.GetIsNullValue() {
		return "(empty)"
	}
	switch data := value.GetData().GetData().(type) {
	case *mfiles.TypedValueUnion_Text:
		return data.Text
	case *mfiles.TypedValueUnion_MultiLineText:
		return data.MultiLineText
	case *mfiles.TypedValueUnion_Integer:
		return strconv.Itoa(int(data.Integer))
	case *mfiles.TypedValueUnion_Integer64:
		return strconv.FormatInt(data.Integer64, 10)
	case *mfiles.TypedValueUnion_RealNumber:
		return strconv.FormatFloat(data.RealNumber, 'g', -1, 64)
	case *mfiles.TypedValueUnion_DecimalNumber:
		return data.DecimalNumber
	case *mfiles.TypedValueUnion_Boolean:
		if data.Boolean {
			return "yes"
		}
		return "no"
	case *mfiles.TypedValueUnion_Date: // midnight UTC of the day
		return data.Date.AsTime().Format(time.DateOnly)
	case *mfiles.TypedValueUnion_Timestamp:
		return data.Timestamp.AsTime().Format(time.DateTime) + " UTC"
	case *mfiles.TypedValueUnion_Lookup:
		return formatLookup(data.Lookup)
	case *mfiles.TypedValueUnion_MultiSelectLookup:
		items := make([]string, 0, len(data.MultiSelectLookup.GetValues()))
		for _, lookup := range data.MultiSelectLookup.GetValues() {
			items = append(items, formatLookup(lookup))
		}
		return strings.Join(items, "; ")
	default:
		return fmt.Sprintf("(%s)", value.GetType())
	}
}

func formatLookup(lookup *mfiles.Lookup) string {
	item := lookup.GetValueListItemInfo()
	return fmt.Sprintf("%s (ID %d)", item.GetName(), item.GetObjId().GetItemId().GetInternalId())
}

// [END format-value]

// [START create-test-document]

// CreateTestDocument creates a "[docs-test]" document of class "Other Document" (1) without
// files, for recipes that need an object to change.
func CreateTestDocument(ctx context.Context, client *mfilesclient.Client, title string,
	extra ...*mfiles.PropertyValue) (*mfiles.ObjectVersionEx, error) {
	properties := []*mfiles.PropertyValue{
		mfilesclient.PropertyValue(0, mfilesclient.Text(TestPrefix+" Go "+title)), // Name or title
		mfilesclient.PropertyValue(100, mfilesclient.Lookup(1, 1)),                // Class: Other Document
		mfilesclient.PropertyValue(22, mfilesclient.Boolean(false)),               // Single file: no
	}
	response, err := mfiles.NewIRPCObjectOperationsClient(client.Conn).AddObjectWithFiles(ctx,
		&mfiles.AddObjectWithFilesRequest{
			ObjectTypeId: 0,
			Properties:   append(properties, extra...),
			// Named ACL -1, "Full control for all internal users". Without an ACL, not even the
			// creator could see the object.
			Acl:     mfilesclient.NamedACL(mfilesclient.NamedACLFullControlForAllInternalUsers),
			CheckIn: true,
		})
	if err != nil {
		return nil, err
	}
	return response.GetCreatedObject().GetObjectVersion(), nil
}

// [END create-test-document]

// [START delete-object]

// Delete deletes an object a recipe created. A deleted object can be undeleted; destroying it
// (DestroyObject) is final and needs more rights.
func Delete(ctx context.Context, client *mfilesclient.Client, objID *mfiles.ObjID) error {
	_, err := mfiles.NewIRPCObjectOperationsClient(client.Conn).RemoveObject(
		context.WithoutCancel(ctx), &mfiles.RemoveObjectRequest{ObjId: objID})
	if err == nil {
		fmt.Printf("Deleted object %d-%d\n", objID.GetType(), objID.GetItemId().GetInternalId())
	}
	return err
}

// [END delete-object]
