// Package gitstore manages on-disk bare git repositories using go-git.
package gitstore

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

	"github.com/go-git/go-billy/v5/osfs"
	git "github.com/go-git/go-git/v5"
	gitcfg "github.com/go-git/go-git/v5/config"
	"github.com/go-git/go-git/v5/plumbing/cache"
	"github.com/go-git/go-git/v5/storage/filesystem"

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

const (
	// ZeroSHA is the null/zero commit SHA used when no real SHA is available.
	ZeroSHA = "0000000000000000000000000000000000000000"
	// RefsHeadsPrefix is the fully-qualified prefix for branch refs.
	RefsHeadsPrefix = "refs/heads/"
	// RefsTagsPrefix is the fully-qualified prefix for tag refs.
	RefsTagsPrefix = "refs/tags/"

	tenantRootMarker = ".ags-tenant-id"
)

// Store provides access to bare git repositories on the local filesystem.
type Store struct {
	root     string
	resolver func(context.Context) (*Store, error)

	repoLocks sync.Map // per-repo mutexes for write operations

	commitTreeCacheMu    sync.Mutex
	commitTreeCache      map[string]commitTreeCacheEntry
	commitTreeCacheOrder []string
}

// repoLock returns a mutex owned by the concrete tenant store.
func (s *Store) repoLock(ctx context.Context, fullName string) (*sync.Mutex, error) {
	target, err := s.resolvedStore(ctx)
	if err != nil {
		return nil, err
	}
	if target != s {
		return target.repoLock(ctx, fullName)
	}
	v, _ := s.repoLocks.LoadOrStore(fullName, &sync.Mutex{})
	return v.(*sync.Mutex), nil
}

// WithRepoLock serializes callers that need a repo-scoped critical section.
func (s *Store) WithRepoLock(ctx context.Context, fullName string, fn func() error) error {
	mu, err := s.repoLock(ctx, fullName)
	if err != nil {
		return err
	}
	mu.Lock()
	defer mu.Unlock()
	return fn()
}

// New creates a Store rooted at dir, creating it if needed.
func New(dir string) (*Store, error) {
	if err := os.MkdirAll(dir, 0o750); err != nil {
		return nil, fmt.Errorf("gitstore: mkdir %s: %w", dir, err)
	}
	return &Store{root: dir}, nil
}

// NewTenantRouter returns a stateless Store facade that resolves the concrete
// tenant store from every operation's context.
func NewTenantRouter(resolver func(context.Context) (*Store, error)) (*Store, error) {
	if resolver == nil {
		return nil, fmt.Errorf("gitstore: tenant resolver is required")
	}
	return &Store{resolver: resolver}, nil
}

func (s *Store) resolvedStore(ctx context.Context) (*Store, error) {
	if s == nil {
		return nil, fmt.Errorf("gitstore: nil store")
	}
	if s.resolver == nil {
		return s, nil
	}
	target, err := s.resolver(ctx)
	if err != nil {
		return nil, fmt.Errorf("gitstore: resolve tenant store: %w", err)
	}
	if target == nil || target == s || target.resolver != nil {
		return nil, fmt.Errorf("gitstore: tenant resolver returned an invalid concrete store")
	}
	return target, nil
}

// NewForTenant creates a store after atomically claiming or verifying the
// persistent root ownership marker for id. Configuration is authoritative for
// roots that predate ownership markers.
func NewForTenant(dir string, id tenant.ID) (*Store, error) {
	normalized, err := tenant.ParseID(string(id))
	if err != nil || normalized != id {
		return nil, fmt.Errorf("gitstore: invalid tenant ID %q", id)
	}
	root, err := CanonicalizeRoot(dir)
	if err != nil {
		return nil, err
	}
	if err := claimTenantRoot(root, id); err != nil {
		return nil, err
	}
	return &Store{root: root}, nil
}

// CanonicalizeRoot creates a Git root and resolves symlinks so isolation
// checks compare the actual filesystem locations used by concrete stores.
func CanonicalizeRoot(dir string) (string, error) {
	if err := os.MkdirAll(dir, 0o750); err != nil {
		return "", fmt.Errorf("gitstore: mkdir %s: %w", dir, err)
	}
	root, err := filepath.EvalSymlinks(dir)
	if err != nil {
		return "", fmt.Errorf("gitstore: resolve root %s: %w", dir, err)
	}
	root, err = filepath.Abs(root)
	if err != nil {
		return "", fmt.Errorf("gitstore: absolute root %s: %w", root, err)
	}
	return filepath.Clean(root), nil
}

func claimTenantRoot(root string, id tenant.ID) error {
	marker := filepath.Join(root, tenantRootMarker)
	if _, err := os.Stat(marker); err != nil && !errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("gitstore: inspect tenant marker %s: %w", marker, err)
	}
	file, err := os.OpenFile(marker, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o640)
	switch {
	case err == nil:
		payload := []byte(id + "\n")
		if _, err := file.Write(payload); err != nil {
			_ = file.Close()
			_ = os.Remove(marker)
			return fmt.Errorf("gitstore: write tenant marker %s: %w", marker, err)
		}
		if err := file.Sync(); err != nil {
			_ = file.Close()
			_ = os.Remove(marker)
			return fmt.Errorf("gitstore: sync tenant marker %s: %w", marker, err)
		}
		if err := file.Close(); err != nil {
			return fmt.Errorf("gitstore: close tenant marker %s: %w", marker, err)
		}
		return nil
	case !errors.Is(err, os.ErrExist):
		return fmt.Errorf("gitstore: claim tenant root %s: %w", root, err)
	}

	payload, err := os.ReadFile(marker)
	if err != nil {
		return fmt.Errorf("gitstore: read tenant marker %s: %w", marker, err)
	}
	actual := tenant.ID(strings.TrimSpace(string(payload)))
	if actual != id {
		return fmt.Errorf("gitstore: root %s belongs to tenant %q, requested %q", root, actual, id)
	}
	return nil
}

// CanonicalRoot returns the resolved root used by the store.
func (s *Store) CanonicalRoot() string {
	if s == nil || s.resolver != nil {
		return ""
	}
	return filepath.Clean(s.root)
}

// VerifyTenantRoot checks that the concrete store still points at the
// canonical directory claimed by id. It performs no filesystem mutation.
func (s *Store) VerifyTenantRoot(id tenant.ID) error {
	root := s.CanonicalRoot()
	if root == "" {
		return fmt.Errorf("gitstore: concrete tenant root is unavailable")
	}
	resolved, err := filepath.EvalSymlinks(root)
	if err != nil {
		return fmt.Errorf("gitstore: resolve tenant root %s: %w", root, err)
	}
	resolved, err = filepath.Abs(resolved)
	if err != nil {
		return fmt.Errorf("gitstore: absolute tenant root %s: %w", root, err)
	}
	if filepath.Clean(resolved) != root {
		return fmt.Errorf("gitstore: tenant root changed from %s to %s", root, resolved)
	}
	stat, err := os.Stat(root)
	if err != nil {
		return fmt.Errorf("gitstore: stat tenant root %s: %w", root, err)
	}
	if !stat.IsDir() {
		return fmt.Errorf("gitstore: tenant root %s is not a directory", root)
	}
	payload, err := os.ReadFile(filepath.Join(root, tenantRootMarker))
	if err != nil {
		return fmt.Errorf("gitstore: read tenant marker: %w", err)
	}
	actual := tenant.ID(strings.TrimSpace(string(payload)))
	if actual != id {
		return fmt.Errorf("gitstore: root belongs to tenant %q, requested %q", actual, id)
	}
	return nil
}

// validateFullName validates the owner/repo fullName format to prevent path traversal.
// fullName must be in the format "owner/repo" where both owner and repo are valid segments.
func validateFullName(fullName string) error {
	parts := strings.SplitN(fullName, "/", 2)
	if len(parts) != 2 {
		return fmt.Errorf("invalid repo name %q (must be in format owner/repo)", fullName)
	}
	owner, repo := parts[0], parts[1]
	if err := validateNameSegment(owner); err != nil {
		return fmt.Errorf("invalid owner %q: %w", owner, err)
	}
	if err := validateNameSegment(repo); err != nil {
		return fmt.Errorf("invalid repo %q: %w", repo, err)
	}
	return nil
}

// validateNameSegment validates a single name segment (owner or repo) to prevent path traversal.
func validateNameSegment(name string) error {
	if name == "" {
		return errors.New("empty name")
	}
	if name == "." || name == ".." {
		return fmt.Errorf("invalid name %q", name)
	}
	if strings.ContainsAny(name, `/\`) {
		return fmt.Errorf("invalid name %q (must not contain path separators)", name)
	}
	if strings.Contains(name, "\x00") {
		return fmt.Errorf("invalid name %q (contains null byte)", name)
	}
	return nil
}

// RepoRoot returns the filesystem root that contains bare repositories.
func (s *Store) RepoRoot(ctx context.Context) (string, error) {
	target, err := s.resolvedStore(ctx)
	if err != nil {
		return "", err
	}
	return target.root, nil
}

// repoPath returns the filesystem path for a repository's bare .git directory.
func (s *Store) repoPath(ctx context.Context, fullName string) (string, error) {
	if err := validateFullName(fullName); err != nil {
		return "", err
	}
	target, err := s.resolvedStore(ctx)
	if err != nil {
		return "", err
	}
	return filepath.Join(target.root, fullName+".git"), nil
}

// GetRepoPath returns the filesystem path for a repository's bare .git directory.
func (s *Store) GetRepoPath(ctx context.Context, fullName string) (string, error) {
	return s.repoPath(ctx, fullName)
}

// open opens an existing bare git repository.
func (s *Store) open(ctx context.Context, fullName string) (*git.Repository, error) {
	dir, err := s.repoPath(ctx, fullName)
	if err != nil {
		return nil, err
	}
	stg := filesystem.NewStorage(osfs.New(dir), cache.NewObjectLRUDefault())
	repo, err := git.Open(stg, nil)
	if err != nil {
		return nil, fmt.Errorf("gitstore: open %s: %w", fullName, err)
	}
	return repo, nil
}

// Exists reports whether a repository exists in the store.
func (s *Store) Exists(ctx context.Context, fullName string) bool {
	p, err := s.repoPath(ctx, fullName)
	if err != nil {
		return false
	}
	_, err = os.Stat(p)
	return err == nil
}

// ErrNotFound is returned when a repository doesn't exist in the store.
var ErrNotFound = errors.New("repository not found in gitstore")

// SetupConfig sets remote origin URL in the repo config.
func (s *Store) SetupConfig(ctx context.Context, fullName, baseURL string) error {
	repo, err := s.open(ctx, fullName)
	if err != nil {
		return err
	}
	cfg, err := repo.Config()
	if err != nil {
		return err
	}
	cfg.Remotes["origin"] = &gitcfg.RemoteConfig{
		Name: "origin",
		URLs: []string{baseURL + "/" + fullName + ".git"},
	}
	return repo.SetConfig(cfg)
}
