package mvcc

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"

	"gitlab.com/gitlab-org/gitaly/v18/internal/gitaly/storage/mode"
	"gitlab.com/gitlab-org/gitaly/v18/internal/safe"
)

const (
	manifestPointer   = "manifest"
	manifestDirectory = "manifests"
	mvccCacheDir      = "mvcc"
)

// LocalStorage is an implementation of ObjectStorage. It is a very simple
// implementation in the sense that it executes solely in the context of an
// on-disk repository. It treats the current on-disk repository as the source
// of truth. This is useful as a first step towards MVCC since it allows to
// migrate repositories to MVCC without relying on an object store to store
// references and objects.
type LocalStorage struct {
	// storageRoot is the location on disk that contains all repositories.
	// When fetching artifacts, the repository on "remote storage"
	// is located by joining `storageRoot` with `repositoryId`.
	storageRoot string
}

// Interface conformance check
var _ ObjectStorage = &LocalStorage{}

// NewLocalStorage creates a new instance of LocalStorage
func NewLocalStorage(storageRoot string) *LocalStorage {
	return &LocalStorage{
		storageRoot: storageRoot,
	}
}

// ReadManifestPointer reads the manifest pointer for the repository ID.
// In this LocalStorage implementation, it reads at ./.git/mvcc/manifest
// It also trims the trailing newline character, if any.
func (l *LocalStorage) ReadManifestPointer(_ context.Context, repositoryID string) (string, error) {
	pointerPath := filepath.Join(l.storageRoot, repositoryID, mvccCacheDir, manifestPointer)
	raw, err := os.ReadFile(pointerPath)
	if err != nil {
		return "", err
	}
	return strings.TrimSuffix(string(raw), "\n"), nil
}

// ReadManifest reads the manifest defined by hash for the given repository ID.
func (l *LocalStorage) ReadManifest(_ context.Context, repositoryID string, hash string) (io.ReadCloser, error) {
	manifestPath := filepath.Join(l.storageRoot, repositoryID, mvccCacheDir, manifestDirectory, hash)
	return os.Open(manifestPath)
}

// ReadArtifact ensures the artifact identified by key is fetched for the given repository.
func (l *LocalStorage) ReadArtifact(ctx context.Context, repositoryID, key string) (io.ReadCloser, error) {
	path := filepath.Join(l.storageRoot, repositoryID, mvccCacheDir, key)
	return os.Open(path)
}

// PutArtifact writes `content` into `key`. No-op for local storage system since Git already write the new
// artifacts to local MVCC cache dir.
func (l *LocalStorage) PutArtifact(_ context.Context, repositoryID, key string, content io.ReadCloser) error {
	return nil
}

// Commit atomically advances the manifest pointer from baseHash to newHash with a
// compare-and-swap. As the local filesystem has no native conditional write, a lock
// file serializes concurrent committers so the read-compare-write is atomic. If the
// pointer no longer matches baseHash (a concurrent writer won), it returns
// ErrManifestPointerConflict without modifying the pointer.
func (l *LocalStorage) Commit(ctx context.Context, repositoryID, baseHash string, newHash string) error {
	pointerPath := filepath.Join(l.storageRoot, repositoryID, mvccCacheDir, manifestPointer)

	lockedFile, err := safe.NewLockingFileWriter(pointerPath, safe.LockingFileWriterConfig{
		FileWriterConfig: safe.FileWriterConfig{
			FileMode: mode.File,
		},
	})
	if err != nil {
		return fmt.Errorf("creating file writer: %w", err)
	}
	defer func() {
		_ = lockedFile.Close()
	}()

	if _, err := io.Copy(lockedFile, strings.NewReader(newHash)); err != nil {
		return fmt.Errorf("copy to writer: %w", err)
	}
	if err := lockedFile.Lock(); err != nil {
		if errors.Is(err, safe.ErrFileAlreadyLocked) {
			return fmt.Errorf("manifest pointer locked: %w", ErrManifestPointerConflict)
		}
		return fmt.Errorf("lock manifest pointer: %w", err)
	}

	current, err := l.ReadManifestPointer(ctx, repositoryID)
	if err != nil {
		return fmt.Errorf("read current pointer: %w", err)
	}
	if current != baseHash {
		return fmt.Errorf("base manifest pointer changed: %w, base %q current %q",
			ErrManifestPointerConflict, baseHash, current)
	}
	if err := lockedFile.Commit(ctx); err != nil {
		return fmt.Errorf("commit writer: %w", err)
	}

	return nil
}
