// CRUD demo against the universal MinIO storage frontend
// (minio.storage.svc.cluster.local:9000).
//
// Run from outside the cluster via a port-forward:
//
//	kubectl port-forward svc/minio -n storage 9000:9000 &
//	source logging/.env
//	cd storage/test && go mod tidy && go run .
//
// In-cluster, set MINIO_ENDPOINT=minio.storage.svc.cluster.local:9000.
//
// Every S3 call emits [SERVICE_METRIC] op latency; every failure emits
// [APP_METRIC] ERROR with context and aborts (no silent catches).
package main

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"math/rand"
	"os"
	"time"

	"github.com/minio/minio-go/v7"
	"github.com/minio/minio-go/v7/pkg/credentials"
)

const bucket = "crud-test"

func getenv(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

// timed wraps an S3 operation: emits a serviceMetric on success,
// an applicationMetric and exit(1) on failure.
func timed(op string, fn func() error) {
	start := time.Now()
	if err := fn(); err != nil {
		fmt.Printf("[APP_METRIC] ERROR s3.%s failed bucket=%s | trace=%v\n", op, bucket, err)
		os.Exit(1)
	}
	fmt.Printf("[SERVICE_METRIC] s3.%s.latency_ms=%d ms\n", op, time.Since(start).Milliseconds())
}

func randomText(n int) []byte {
	const letters = "abcdefghijklmnopqrstuvwxyz \n"
	b := make([]byte, n)
	for i := range b {
		b[i] = letters[rand.Intn(len(letters))]
	}
	return b
}

func main() {
	endpoint := getenv("MINIO_ENDPOINT", "localhost:9000")
	user := os.Getenv("MINIO_ROOT_USER")
	pass := os.Getenv("MINIO_ROOT_PASSWORD")
	if user == "" || pass == "" {
		fmt.Println("[APP_METRIC] ERROR config missing | trace=MINIO_ROOT_USER / MINIO_ROOT_PASSWORD not set (source storage/.env)")
		os.Exit(1)
	}

	ctx := context.Background()
	client, err := minio.New(endpoint, &minio.Options{
		Creds:  credentials.NewStaticV4(user, pass, ""),
		Secure: false, // in-cluster traffic, no TLS
	})
	if err != nil {
		fmt.Printf("[APP_METRIC] ERROR s3.connect failed endpoint=%s | trace=%v\n", endpoint, err)
		os.Exit(1)
	}

	key := fmt.Sprintf("demo/%d.txt", time.Now().Unix())
	original := randomText(256)
	updated := append([]byte("UPDATED ---\n"), randomText(128)...)

	// Ensure bucket (idempotent). Site replication propagates it to az-b.
	timed("ensure_bucket", func() error {
		exists, err := client.BucketExists(ctx, bucket)
		if err != nil || exists {
			return err
		}
		return client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
	})

	// CREATE
	timed("put", func() error {
		_, err := client.PutObject(ctx, bucket, key,
			bytes.NewReader(original), int64(len(original)),
			minio.PutObjectOptions{ContentType: "text/plain"})
		return err
	})
	fmt.Printf("created %s/%s (%d bytes of random text)\n", bucket, key, len(original))

	// READ — and verify content round-trips
	timed("get", func() error {
		obj, err := client.GetObject(ctx, bucket, key, minio.GetObjectOptions{})
		if err != nil {
			return err
		}
		defer obj.Close()
		got, err := io.ReadAll(obj)
		if err != nil {
			return err
		}
		if !bytes.Equal(got, original) {
			return fmt.Errorf("read-back mismatch: want %d bytes, got %d", len(original), len(got))
		}
		return nil
	})
	fmt.Println("read back and verified content")

	// UPDATE — S3 semantics: overwrite the object in place
	timed("update", func() error {
		_, err := client.PutObject(ctx, bucket, key,
			bytes.NewReader(updated), int64(len(updated)),
			minio.PutObjectOptions{ContentType: "text/plain"})
		return err
	})
	fmt.Println("updated (overwrote) object")

	// LIST the demo/ prefix
	timed("list", func() error {
		for obj := range client.ListObjects(ctx, bucket, minio.ListObjectsOptions{Prefix: "demo/", Recursive: true}) {
			if obj.Err != nil {
				return obj.Err
			}
			fmt.Printf("  %s  %d bytes  %s\n", obj.Key, obj.Size, obj.LastModified.Format(time.RFC3339))
		}
		return nil
	})

	// DELETE — and verify it is gone
	timed("delete", func() error {
		if err := client.RemoveObject(ctx, bucket, key, minio.RemoveObjectOptions{}); err != nil {
			return err
		}
		_, err := client.StatObject(ctx, bucket, key, minio.StatObjectOptions{})
		if err == nil {
			return fmt.Errorf("object %s still exists after delete", key)
		}
		if minio.ToErrorResponse(err).Code != "NoSuchKey" {
			return err
		}
		return nil
	})
	fmt.Println("deleted and verified gone — CRUD cycle complete")
}
