package contentpayload

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

	git "github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/cache"
	"github.com/go-git/go-git/v5/storage/filesystem"
)

type openGitRepository struct {
	*git.Repository
	root *os.File
}

func (opened *openGitRepository) close() { _ = opened.root.Close() }

func (s *gitTreeStorage) openRef(ref ManifestRef) (*openGitRepository, plumbing.Hash, []string, error) {
	parts, expected, err := gitPayloadRefParts(ref)
	if err != nil {
		return nil, plumbing.ZeroHash, nil, err
	}
	repo, err := s.openExisting(parts[0], parts[1])
	if err != nil {
		return nil, plumbing.ZeroHash, nil, err
	}
	versionRef := plumbing.ReferenceName("refs/content-versions/" + parts[2])
	reference, err := repo.Reference(versionRef, true)
	if errors.Is(err, plumbing.ErrReferenceNotFound) {
		repo.close()
		return nil, plumbing.ZeroHash, nil, ErrNotFound
	}
	if err != nil {
		repo.close()
		return nil, plumbing.ZeroHash, nil, fmt.Errorf("read immutable content version ref: %w", err)
	}
	if !expected.IsZero() && reference.Hash() != expected {
		repo.close()
		return nil, plumbing.ZeroHash, nil, fmt.Errorf("immutable content version ref does not match exact commit")
	}
	return repo, reference.Hash(), parts, nil
}

func (s *gitTreeStorage) openExisting(workspaceID, contentID string) (*openGitRepository, error) {
	s.mu.RLock()
	closed := s.closed
	s.mu.RUnlock()
	if closed {
		return nil, errFilesystemClosed
	}
	repoPath := filepath.Join(s.root, workspaceID, contentID+".git")
	if err := validateRepositoryPath(s.root, workspaceID, repoPath); err != nil {
		return nil, err
	}
	s.afterValidateRepositoryPath()
	repoRoot, err := s.openRepositoryRoot(workspaceID, contentID, false)
	if err != nil {
		return nil, err
	}
	s.afterOpenRepositoryRoot()
	if err := sameDirectoryIdentity(repoRoot, repoPath); err != nil {
		repoRoot.Close()
		return nil, err
	}
	repo, err := git.Open(filesystem.NewStorage(newDescriptorFS(repoRoot), cache.NewObjectLRUDefault()), nil)
	if errors.Is(err, git.ErrRepositoryNotExists) {
		repoRoot.Close()
		return nil, ErrNotFound
	}
	if err != nil {
		repoRoot.Close()
		return nil, fmt.Errorf("open content repository: %w", err)
	}
	if err := validatePinnedRepository(repoRoot, repo); err != nil {
		repoRoot.Close()
		return nil, err
	}
	ready, err := repositoryReady(repoRoot)
	if err != nil {
		repoRoot.Close()
		return nil, err
	}
	if !ready {
		repoRoot.Close()
		return nil, fmt.Errorf("content git repository is not durably initialized")
	}
	return &openGitRepository{Repository: repo, root: repoRoot}, nil
}

func (s *gitTreeStorage) openOrInit(workspaceID, contentID string) (*openGitRepository, bool, error) {
	s.mu.RLock()
	root := s.rootFile
	s.mu.RUnlock()
	if root == nil {
		return nil, false, errFilesystemClosed
	}
	repoPath := filepath.Join(s.root, workspaceID, contentID+".git")
	workspace, err := openDirectoryAt(root, []string{workspaceID}, true)
	if err != nil {
		return nil, false, fmt.Errorf("content git workspace directory is unsafe: %w", err)
	}
	defer workspace.Close()
	repoRoot, err := openDirectoryAt(workspace, []string{contentID + ".git"}, false)
	created := errors.Is(err, os.ErrNotExist)
	if created {
		repoRoot, err = openDirectoryAt(workspace, []string{contentID + ".git"}, true)
	}
	if err != nil {
		return nil, false, fmt.Errorf("content git repository directory is unsafe: %w", err)
	}
	s.afterOpenRepositoryRoot()
	if err := sameDirectoryIdentity(repoRoot, repoPath); err != nil {
		repoRoot.Close()
		return nil, false, err
	}
	if !created {
		if err := validateRepositoryPath(s.root, workspaceID, repoPath); err != nil {
			repoRoot.Close()
			return nil, false, err
		}
	}
	storage := filesystem.NewStorage(newDescriptorFS(repoRoot), cache.NewObjectLRUDefault())
	repo, err := git.Open(storage, nil)
	if err == nil {
		if err := validatePinnedRepository(repoRoot, repo); err != nil {
			repoRoot.Close()
			return nil, false, err
		}
		ready, err := repositoryReady(repoRoot)
		if err != nil {
			repoRoot.Close()
			return nil, false, err
		}
		return &openGitRepository{Repository: repo, root: repoRoot}, !ready, nil
	}
	if !errors.Is(err, git.ErrRepositoryNotExists) {
		repoRoot.Close()
		return nil, false, fmt.Errorf("open content repository: %w", err)
	}
	repo, err = git.Init(storage, nil)
	if err != nil {
		repoRoot.Close()
		return nil, false, fmt.Errorf("initialize content repository: %w", err)
	}
	if err := newDescriptorFS(repoRoot).MkdirAll("hooks", 0o750); err != nil {
		repoRoot.Close()
		return nil, false, err
	}
	if err := newDescriptorFS(repoRoot).MkdirAll("refs/content-versions", 0o750); err != nil {
		repoRoot.Close()
		return nil, false, err
	}
	if err := validateRepositoryPath(s.root, workspaceID, repoPath); err != nil {
		repoRoot.Close()
		return nil, false, err
	}
	return &openGitRepository{Repository: repo, root: repoRoot}, created, nil
}

func (s *gitTreeStorage) openRepositoryRoot(workspaceID, contentID string, allowMissing bool) (*os.File, error) {
	s.mu.RLock()
	root := s.rootFile
	s.mu.RUnlock()
	if root == nil {
		return nil, errFilesystemClosed
	}
	if err := sameDirectoryIdentity(root, s.root); err != nil {
		return nil, err
	}
	workspace, err := openDirectoryAt(root, []string{workspaceID}, false)
	if err != nil {
		return nil, fmt.Errorf("open content git workspace without symlinks: %w", err)
	}
	defer workspace.Close()
	if err := sameDirectoryIdentity(workspace, filepath.Join(s.root, workspaceID)); err != nil {
		return nil, err
	}
	repo, err := openDirectoryAt(workspace, []string{contentID + ".git"}, false)
	if allowMissing && errors.Is(err, os.ErrNotExist) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("open content git repository without symlinks: %w", err)
	}
	if err := sameDirectoryIdentity(repo, filepath.Join(s.root, workspaceID, contentID+".git")); err != nil {
		repo.Close()
		return nil, err
	}
	return repo, nil
}

func sameDirectoryIdentity(opened *os.File, path string) error {
	pinned, err := opened.Stat()
	if err != nil {
		return fmt.Errorf("inspect pinned content git directory: %w", err)
	}
	current, err := os.Lstat(path)
	if err != nil || !current.IsDir() || current.Mode()&os.ModeSymlink != 0 || !os.SameFile(pinned, current) {
		return fmt.Errorf("content git directory identity changed: %s", path)
	}
	return nil
}

func (s *gitTreeStorage) resolveParent(repo *git.Repository, ref ManifestRef, workspaceID, contentID string) (plumbing.Hash, error) {
	if strings.TrimSpace(string(ref)) == "" {
		return plumbing.ZeroHash, nil
	}
	parts, expected, err := gitPayloadRefParts(ref)
	if err != nil {
		return plumbing.ZeroHash, err
	}
	if parts[0] != workspaceID || parts[1] != contentID {
		return plumbing.ZeroHash, fmt.Errorf("content tree parent belongs to another lineage")
	}
	parent, err := repo.Reference(plumbing.ReferenceName("refs/content-versions/"+parts[2]), true)
	if errors.Is(err, plumbing.ErrReferenceNotFound) {
		return plumbing.ZeroHash, ErrNotFound
	}
	if err != nil {
		return plumbing.ZeroHash, err
	}
	if !expected.IsZero() && parent.Hash() != expected {
		return plumbing.ZeroHash, fmt.Errorf("content tree parent ref does not match exact commit")
	}
	return parent.Hash(), nil
}

func gitPayloadRef(workspaceID, contentID, versionID string, commit plumbing.Hash) ManifestRef {
	ref := strings.Join([]string{workspaceID, contentID, versionID + "@" + commit.String()}, "/")
	return ManifestRef(ref)
}

func gitPayloadRefParts(ref ManifestRef) ([]string, plumbing.Hash, error) {
	parts := strings.Split(string(ref), "/")
	if len(parts) != 3 {
		return nil, plumbing.ZeroHash, fmt.Errorf("content manifest reference is invalid")
	}
	var err error
	for index := 0; index < 2; index++ {
		parts[index], err = safeID("manifest reference", parts[index])
		if err != nil {
			return nil, plumbing.ZeroHash, err
		}
	}
	versionSpec := parts[2]
	versionID, commitText, hasCommit := strings.Cut(versionSpec, "@")
	if !hasCommit {
		parts[2], err = safeID("manifest reference", versionSpec)
		if err != nil {
			return nil, plumbing.ZeroHash, err
		}
		return parts, plumbing.ZeroHash, nil
	}
	versionID, err = safeID("version", versionID)
	if err != nil {
		return nil, plumbing.ZeroHash, err
	}
	if len(commitText) != 40 {
		return nil, plumbing.ZeroHash, fmt.Errorf("content git payload commit is invalid")
	}
	commit := plumbing.NewHash(commitText)
	if commit.IsZero() || commit.String() != strings.ToLower(commitText) {
		return nil, plumbing.ZeroHash, fmt.Errorf("content git payload commit is invalid")
	}
	parts[2] = versionID
	return parts, commit, nil
}
