package autocert

import (
	"context"
	"errors"
	"io"
	"io/fs"

	"cloud.google.com/go/storage"
	"github.com/caddyserver/certmagic"
	"google.golang.org/api/iterator"
)

type gcsStorage struct {
	client *storage.Client
	bucket string
	prefix string

	*locker
}

func newGCSStorage(client *storage.Client, bucket, prefix string) *gcsStorage {
	s := &gcsStorage{
		client: client,
		bucket: bucket,
		prefix: prefix,
	}
	s.locker = &locker{
		store:  s.Store,
		load:   s.Load,
		delete: s.Delete,
	}
	return s
}

func (s *gcsStorage) Store(ctx context.Context, key string, value []byte) error {
	obj := s.client.
		Bucket(s.bucket).
		Object(key)

	w := obj.NewWriter(ctx)
	_, err := w.Write(value)
	if err != nil {
		_ = w.CloseWithError(err)
		return err
	}

	err = w.Close()
	if err != nil {
		return err
	}

	return nil
}

func (s *gcsStorage) Load(ctx context.Context, key string) ([]byte, error) {
	r, err := s.client.
		Bucket(s.bucket).
		Object(key).
		NewReader(ctx)
	if errors.Is(err, storage.ErrObjectNotExist) {
		return nil, fs.ErrNotExist
	} else if err != nil {
		return nil, err
	}
	defer r.Close()

	return io.ReadAll(r)
}

func (s *gcsStorage) Delete(ctx context.Context, key string) error {
	err := s.client.
		Bucket(s.bucket).
		Object(key).
		Delete(ctx)
	if errors.Is(err, storage.ErrObjectNotExist) {
		return nil
	}
	return err
}

func (s *gcsStorage) Exists(ctx context.Context, key string) bool {
	_, err := s.client.
		Bucket(s.bucket).
		Object(key).
		Attrs(ctx)
	return err == nil
}

func (s *gcsStorage) List(ctx context.Context, prefix string, recursive bool) ([]string, error) {
	var delimiter string
	if !recursive {
		delimiter = "/"
	}

	it := s.client.
		Bucket(s.bucket).
		Objects(ctx, &storage.Query{
			Delimiter: delimiter,
			Prefix:    prefix,
		})
	var keys []string
	for {
		attrs, err := it.Next()
		if errors.Is(err, iterator.Done) {
			break
		} else if err != nil {
			return nil, err
		}

		if attrs.Prefix != "" {
			keys = append(keys, attrs.Prefix)
		} else {
			keys = append(keys, attrs.Name)
		}
	}
	return keys, nil
}

func (s *gcsStorage) Stat(ctx context.Context, key string) (certmagic.KeyInfo, error) {
	attrs, err := s.client.
		Bucket(s.bucket).
		Object(key).
		Attrs(ctx)
	if errors.Is(err, storage.ErrObjectNotExist) {
		return certmagic.KeyInfo{}, fs.ErrNotExist
	} else if err != nil {
		return certmagic.KeyInfo{}, err
	}

	return certmagic.KeyInfo{
		Key:        key,
		Modified:   attrs.Updated,
		Size:       attrs.Size,
		IsTerminal: true,
	}, nil
}