package contentpayload

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"

	git "github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/filemode"
	"github.com/go-git/go-git/v5/plumbing/object"
)

const gitRepositoryReadyMarker = "content-store.ready"

var gitRepositoryReadyPayload = []byte("content-store-ready-v1\n")

func validateContentStoreOwnedDirectory(path string) error {
	info, err := os.Lstat(path)
	if err != nil {
		return fmt.Errorf("inspect content git owned directory: %w", err)
	}
	if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 || info.Mode().Perm()&0o022 != 0 || !directoryOwnedByProcess(info) {
		return fmt.Errorf("content git directory must be owned by the service account and not group/world writable: %s", path)
	}
	return nil
}

func (s *gitTreeStorage) syncImportedRepository(ctx context.Context, repoPath string) error {
	var files, directories []string
	err := filepath.WalkDir(repoPath, func(path string, entry os.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if err := ctx.Err(); err != nil {
			return err
		}
		info, err := entry.Info()
		if err != nil {
			return err
		}
		if info.Mode()&os.ModeSymlink != 0 || (!info.IsDir() && !info.Mode().IsRegular()) {
			return fmt.Errorf("imported content repository contains special entry")
		}
		if info.IsDir() {
			directories = append(directories, path)
		} else {
			files = append(files, path)
		}
		return nil
	})
	if err != nil {
		return err
	}
	for _, path := range files {
		if err := s.syncPath(path, false); err != nil {
			return fmt.Errorf("sync imported content git file: %w", err)
		}
	}
	// Sync child directories before parents so every reachable directory entry
	// is durable before the repository itself is renamed into its final name.
	sort.Slice(directories, func(i, j int) bool { return len(directories[i]) > len(directories[j]) })
	for _, path := range directories {
		if err := s.syncPath(path, true); err != nil {
			return fmt.Errorf("sync imported content git directory: %w", err)
		}
	}
	if err := s.syncPath(filepath.Dir(repoPath), true); err != nil {
		return fmt.Errorf("sync imported content git staging parent: %w", err)
	}
	return nil
}

func syncGitPath(path string, directory bool) error {
	info, err := os.Lstat(path)
	if err != nil {
		return err
	}
	if info.Mode()&os.ModeSymlink != 0 || directory != info.IsDir() {
		return fmt.Errorf("content git sync target has unexpected type")
	}
	file, err := os.Open(path)
	if err != nil {
		return err
	}
	defer file.Close()
	return file.Sync()
}

func syncGitFile(file *os.File, _ string) error { return file.Sync() }

func repositoryReady(repoRoot *os.File) (bool, error) {
	marker, err := openRegularAt(repoRoot, gitRepositoryReadyMarker, os.O_RDONLY, 0)
	if errors.Is(err, os.ErrNotExist) {
		return false, nil
	}
	if err != nil {
		return false, fmt.Errorf("open content git ready marker: %w", err)
	}
	defer marker.Close()
	payload, err := io.ReadAll(io.LimitReader(marker, int64(len(gitRepositoryReadyPayload)+1)))
	if err != nil {
		return false, fmt.Errorf("read content git ready marker: %w", err)
	}
	if !bytes.Equal(payload, gitRepositoryReadyPayload) {
		return false, fmt.Errorf("content git ready marker is invalid")
	}
	return true, nil
}

func (s *gitTreeStorage) markRepositoryReady(repoRoot *os.File) error {
	marker, err := openRegularAt(repoRoot, gitRepositoryReadyMarker, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o640)
	if errors.Is(err, os.ErrExist) {
		ready, readyErr := repositoryReady(repoRoot)
		if readyErr != nil {
			return readyErr
		}
		if ready {
			return nil
		}
	}
	if err != nil {
		return fmt.Errorf("create content git ready marker: %w", err)
	}
	removeMarker := true
	defer func() {
		_ = marker.Close()
		if removeMarker {
			_ = unlinkAt(repoRoot, gitRepositoryReadyMarker, false)
			_ = s.syncFile(repoRoot, "repo-ready-cleanup")
		}
	}()
	if _, err := marker.Write(gitRepositoryReadyPayload); err != nil {
		return fmt.Errorf("write content git ready marker: %w", err)
	}
	if err := s.syncFile(marker, "ready-marker"); err != nil {
		return fmt.Errorf("sync content git ready marker: %w", err)
	}
	if err := marker.Close(); err != nil {
		return fmt.Errorf("close content git ready marker: %w", err)
	}
	if err := s.syncFile(repoRoot, "repo-ready"); err != nil {
		return fmt.Errorf("sync content git ready directory: %w", err)
	}
	removeMarker = false
	return nil
}

func (s *gitTreeStorage) syncInitializedRepository(repoRoot *os.File, workspaceID string) error {
	for _, name := range []string{"config", "HEAD"} {
		file, err := openRegularAt(repoRoot, name, os.O_RDONLY, 0)
		if err != nil {
			return fmt.Errorf("open initialized content git file: %w", err)
		}
		if err := s.syncFile(file, name); err != nil {
			file.Close()
			return fmt.Errorf("sync initialized content git file: %w", err)
		}
		file.Close()
	}
	for _, parts := range [][]string{{"refs", "content-versions"}, {"refs"}, nil} {
		directory, err := openDirectoryAt(repoRoot, parts, false)
		if err != nil {
			return err
		}
		label := "repo"
		if len(parts) != 0 {
			label = filepath.Join(parts...)
		}
		if err := s.syncFile(directory, label); err != nil {
			directory.Close()
			return fmt.Errorf("sync initialized content git directory: %w", err)
		}
		directory.Close()
	}
	workspace, err := openDirectoryAt(s.rootFile, []string{workspaceID}, false)
	if err != nil {
		return err
	}
	if err := s.syncFile(workspace, "workspace"); err != nil {
		workspace.Close()
		return fmt.Errorf("sync initialized content git workspace: %w", err)
	}
	workspace.Close()
	if err := s.syncFile(s.rootFile, "root"); err != nil {
		return fmt.Errorf("sync initialized content git root: %w", err)
	}
	return s.markRepositoryReady(repoRoot)
}

func (s *gitTreeStorage) syncCommitObjects(ctx context.Context, repo *git.Repository, repoRoot *os.File, commitHash plumbing.Hash) error {
	commit, err := repo.CommitObject(commitHash)
	if err != nil {
		return fmt.Errorf("open content commit for durability: %w", err)
	}
	tree, err := commit.Tree()
	if err != nil {
		return fmt.Errorf("open content tree for durability: %w", err)
	}
	hashes := []plumbing.Hash{commitHash}
	var collect func(*object.Tree) error
	collect = func(current *object.Tree) error {
		if err := ctx.Err(); err != nil {
			return err
		}
		hashes = append(hashes, current.Hash)
		for _, entry := range current.Entries {
			switch entry.Mode {
			case filemode.Dir:
				child, err := current.Tree(entry.Name)
				if err != nil {
					return err
				}
				if err := collect(child); err != nil {
					return err
				}
			case filemode.Regular, filemode.Executable:
				hashes = append(hashes, entry.Hash)
			default:
				return fmt.Errorf("content tree contains unsupported entry")
			}
		}
		return nil
	}
	if err := collect(tree); err != nil {
		return err
	}
	objects, err := openDirectoryAt(repoRoot, []string{"objects"}, false)
	if err != nil {
		return fmt.Errorf("open content git objects root: %w", err)
	}
	defer objects.Close()
	directories := make(map[string]*os.File)
	defer func() {
		for _, directory := range directories {
			_ = directory.Close()
		}
	}()
	for _, hash := range hashes {
		text := hash.String()
		directory := directories[text[:2]]
		if directory == nil {
			directory, err = openDirectoryAt(objects, []string{text[:2]}, false)
			if errors.Is(err, os.ErrNotExist) {
				continue
			}
			if err != nil {
				return err
			}
			directories[text[:2]] = directory
		}
		file, err := openRegularAt(directory, text[2:], os.O_RDONLY, 0)
		if errors.Is(err, os.ErrNotExist) {
			continue // Object was already durable in a pack imported before this publish.
		} else if err != nil {
			return err
		}
		if err := s.syncFile(file, "object"); err != nil {
			file.Close()
			return fmt.Errorf("sync content git object: %w", err)
		}
		file.Close()
	}
	for _, directory := range directories {
		if err := s.syncFile(directory, "object-directory"); err != nil {
			return fmt.Errorf("sync content git object directory: %w", err)
		}
	}
	if err := s.syncFile(objects, "objects"); err != nil {
		return fmt.Errorf("sync content git objects root: %w", err)
	}
	return nil
}
