// Package blobstore is a content-addressed object store for media ciphertext.
//
// The bus transports messages, not blobs. Media (images, files, large payloads)
// is encrypted by the client BEFORE being stored here, so the store only ever
// sees ciphertext. Objects are addressed by the sha256 hex of their (encrypted)
// bytes, which makes Put idempotent and deduplicating.
//
// Store is an interface (branch-by-abstraction, issue 0003d) with two backends:
// diskStore (the default, local filesystem) and objectStore (NATS Object Store
// on JetStream, replicated across the cluster so blobs survive a node loss and
// are reachable from any node). The wire contract (sha256-hex addressing) is
// identical, so a client cannot tell which backend a membershipd uses.
package blobstore

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"os"
	"path/filepath"
)

// Store is a content-addressed blob store: Put returns the sha256-hex address of
// the stored bytes, Get fetches by that address, Has reports presence.
type Store interface {
	Put(data []byte) (string, error)
	Get(hash string) ([]byte, error)
	Has(hash string) bool
}

// diskStore is a directory-backed content-addressed blob store (the default,
// single-node backend).
type diskStore struct {
	dir string
}

// New creates a disk-backed Store rooted at dir, creating the directory if
// needed. It remains the default backend; the replicated NATS Object Store is
// constructed separately (NewObjectStore) when decentralization is enabled.
func New(dir string) (Store, error) {
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return nil, fmt.Errorf("blobstore: mkdir %q: %w", dir, err)
	}
	return &diskStore{dir: dir}, nil
}

// path returns the on-disk path for a given content hash.
func (s *diskStore) path(hash string) string {
	return filepath.Join(s.dir, hash)
}

// Put writes data to the store and returns its sha256 hex hash. If an object
// with the same content already exists, Put is a no-op and returns the hash.
func (s *diskStore) Put(data []byte) (string, error) {
	sum := sha256.Sum256(data)
	hash := hex.EncodeToString(sum[:])
	p := s.path(hash)

	if _, err := os.Stat(p); err == nil {
		return hash, nil // already present (content-addressed: identical bytes)
	}

	// Write atomically: temp file + rename, so readers never see a partial blob.
	tmp, err := os.CreateTemp(s.dir, ".tmp-*")
	if err != nil {
		return "", fmt.Errorf("blobstore: create temp: %w", err)
	}
	tmpName := tmp.Name()
	if _, err := tmp.Write(data); err != nil {
		tmp.Close()
		os.Remove(tmpName)
		return "", fmt.Errorf("blobstore: write temp: %w", err)
	}
	if err := tmp.Close(); err != nil {
		os.Remove(tmpName)
		return "", fmt.Errorf("blobstore: close temp: %w", err)
	}
	if err := os.Rename(tmpName, p); err != nil {
		os.Remove(tmpName)
		return "", fmt.Errorf("blobstore: rename: %w", err)
	}
	return hash, nil
}

// Get reads the object with the given hash.
func (s *diskStore) Get(hash string) ([]byte, error) {
	data, err := os.ReadFile(s.path(hash))
	if err != nil {
		return nil, fmt.Errorf("blobstore: get %q: %w", hash, err)
	}
	return data, nil
}

// Has reports whether an object with the given hash exists.
func (s *diskStore) Has(hash string) bool {
	_, err := os.Stat(s.path(hash))
	return err == nil
}
