// Package avatarstore persists normalized avatar images outside the database.
package avatarstore

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

	"github.com/pingcap/agent-git-service/internal/randutil"
)

// ErrNotFound reports that an avatar object is absent. Callers should fall
// back to the generated identicon instead of exposing a broken image URL.
var ErrNotFound = errors.New("avatar not found")

// Store is the small object-storage contract required by account avatars.
// Production currently uses FileStore rooted in the tenant's durable git
// volume; the interface deliberately keeps an object-store implementation
// possible without changing service or HTTP behavior.
type Store interface {
	Put(context.Context, string, []byte) error
	Get(context.Context, string) ([]byte, error)
	Delete(context.Context, string) error
}

// FileStore keeps each tenant's avatar objects under <root>/.avatars.
// Avatar keys are server-generated random hexadecimal strings, never user
// input, so the store can reject path traversal at its boundary.
type FileStore struct {
	root string
}

func NewFileStore(root string) *FileStore {
	return &FileStore{root: root}
}

func (s *FileStore) Put(ctx context.Context, key string, content []byte) error {
	if err := ctx.Err(); err != nil {
		return err
	}
	path, err := s.path(key)
	if err != nil {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
		return fmt.Errorf("create avatar directory: %w", err)
	}
	tmp := path + ".tmp-" + randutil.Hex(8)
	if err := os.WriteFile(tmp, content, 0o640); err != nil {
		return fmt.Errorf("write avatar: %w", err)
	}
	if err := os.Rename(tmp, path); err != nil {
		_ = os.Remove(tmp)
		return fmt.Errorf("publish avatar: %w", err)
	}
	return nil
}

func (s *FileStore) Get(ctx context.Context, key string) ([]byte, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	path, err := s.path(key)
	if err != nil {
		return nil, err
	}
	content, err := os.ReadFile(path)
	if errors.Is(err, fs.ErrNotExist) {
		return nil, ErrNotFound
	}
	if err != nil {
		return nil, fmt.Errorf("read avatar: %w", err)
	}
	return content, nil
}

func (s *FileStore) Delete(ctx context.Context, key string) error {
	if err := ctx.Err(); err != nil {
		return err
	}
	path, err := s.path(key)
	if err != nil {
		return err
	}
	if err := os.Remove(path); err != nil && !errors.Is(err, fs.ErrNotExist) {
		return fmt.Errorf("delete avatar: %w", err)
	}
	return nil
}

func (s *FileStore) path(key string) (string, error) {
	if s == nil || strings.TrimSpace(s.root) == "" {
		return "", errors.New("avatar store root is empty")
	}
	if len(key) != 68 || !strings.HasSuffix(key, ".png") {
		return "", fmt.Errorf("invalid avatar key")
	}
	for _, c := range strings.TrimSuffix(key, ".png") {
		if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
			return "", fmt.Errorf("invalid avatar key")
		}
	}
	return filepath.Join(s.root, ".avatars", key[:2], key), nil
}
