package store

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"sort"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/s3"

	"github.com/nexuscrm/sentry/internal/types"
)

// S3Store persists sentry snapshots to S3 and hydrates recent snapshots
// on cold start. Default layout: snapshots/YYYY/MM/DD/HHMM.json.
//
// A non-empty Prefix namespaces the store so multiple targets (e.g., prod
// and staging) can share the same bucket without colliding:
//   - Prefix "" → snapshots/YYYY/MM/DD/HHMM.json
//   - Prefix "prod" → snapshots/prod/YYYY/MM/DD/HHMM.json
//   - Prefix "staging" → snapshots/staging/YYYY/MM/DD/HHMM.json
type S3Store struct {
	client *s3.Client
	bucket string
	prefix string // target name; empty = legacy layout
}

// NewS3Store constructs a store around an already-configured S3 client.
// The bucket is assumed to exist with an appropriate lifecycle policy
// (retention handled by S3, not sentry — sentry has no s3:DeleteObject
// permission).
func NewS3Store(client *s3.Client, bucket string) *S3Store {
	return &S3Store{client: client, bucket: bucket}
}

// NewS3StoreWithPrefix constructs a target-scoped store. Every PutSnapshot
// and HydrateRecent call is scoped to `snapshots/{prefix}/...`.
func NewS3StoreWithPrefix(client *s3.Client, bucket, prefix string) *S3Store {
	return &S3Store{client: client, bucket: bucket, prefix: prefix}
}

// keyPrefix returns the full S3 key prefix used by this store for listing.
func (s *S3Store) keyPrefix() string {
	if s.prefix == "" {
		return "snapshots/"
	}
	return "snapshots/" + s.prefix + "/"
}

// snapshotKey formats the S3 key for a snapshot timestamp.
// Exposed for testing.
func snapshotKey(prefix string, ts time.Time) string {
	base := "snapshots/"
	if prefix != "" {
		base += prefix + "/"
	}
	return fmt.Sprintf("%s%04d/%02d/%02d/%02d%02d.json",
		base, ts.Year(), ts.Month(), ts.Day(), ts.Hour(), ts.Minute())
}

// PutSnapshot serializes a snapshot to JSON and uploads it to S3.
func (s *S3Store) PutSnapshot(ctx context.Context, snap types.Snapshot) error {
	b, err := json.Marshal(snap)
	if err != nil {
		return fmt.Errorf("s3 store: marshal: %w", err)
	}
	_, err = s.client.PutObject(ctx, &s3.PutObjectInput{
		Bucket:      aws.String(s.bucket),
		Key:         aws.String(snapshotKey(s.prefix, snap.SnapshotTs.UTC())),
		Body:        bytes.NewReader(b),
		ContentType: aws.String("application/json"),
	})
	if err != nil {
		return fmt.Errorf("s3 store: put: %w", err)
	}
	return nil
}

// HydrateRecent lists snapshot objects whose LastModified falls within the
// last `hours` hours and returns the decoded snapshots, ordered by SnapshotTs.
// Used at sentry cold start to reseed the in-memory LRU.
func (s *S3Store) HydrateRecent(ctx context.Context, hours int) ([]types.Snapshot, error) {
	cutoff := time.Now().UTC().Add(-time.Duration(hours) * time.Hour)
	var out []types.Snapshot
	paginator := s3.NewListObjectsV2Paginator(s.client, &s3.ListObjectsV2Input{
		Bucket: aws.String(s.bucket),
		Prefix: aws.String(s.keyPrefix()),
	})
	for paginator.HasMorePages() {
		page, err := paginator.NextPage(ctx)
		if err != nil {
			return nil, fmt.Errorf("s3 store: list: %w", err)
		}
		for _, obj := range page.Contents {
			if obj.LastModified == nil || obj.LastModified.Before(cutoff) {
				continue
			}
			snap, err := s.getSnapshot(ctx, aws.ToString(obj.Key))
			if err != nil {
				// Skip unreadable individual objects; cold-start must still
				// make progress even if one snapshot is corrupt.
				continue
			}
			out = append(out, snap)
		}
	}
	sort.SliceStable(out, func(i, j int) bool {
		return out[i].SnapshotTs.Before(out[j].SnapshotTs)
	})
	return out, nil
}

func (s *S3Store) getSnapshot(ctx context.Context, key string) (types.Snapshot, error) {
	out, err := s.client.GetObject(ctx, &s3.GetObjectInput{
		Bucket: aws.String(s.bucket),
		Key:    aws.String(key),
	})
	if err != nil {
		return types.Snapshot{}, err
	}
	defer out.Body.Close()
	body, err := io.ReadAll(out.Body)
	if err != nil {
		return types.Snapshot{}, err
	}
	var snap types.Snapshot
	if err := json.Unmarshal(body, &snap); err != nil {
		return types.Snapshot{}, err
	}
	return snap, nil
}
