// Package lfs brings together the core LFS functionality
// NOTE: Subject to change, do not rely on this package from outside git-lfs source
package lfs

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

	"github.com/git-lfs/git-lfs/config"
	"github.com/git-lfs/git-lfs/lfsapi"
	"github.com/git-lfs/git-lfs/localstorage"
	"github.com/git-lfs/git-lfs/tools"
	"github.com/git-lfs/git-lfs/tq"
	"github.com/rubyist/tracerx"
)

// LocalMediaDir returns the root of lfs objects
func LocalMediaDir() string {
	if localstorage.Objects() != nil {
		return localstorage.Objects().RootDir
	}
	return ""
}

func LocalObjectTempDir() string {
	if localstorage.Objects() != nil {
		return localstorage.Objects().TempDir
	}
	return ""
}

func TempDir() string {
	return localstorage.TempDir
}

func TempFile(prefix string) (*os.File, error) {
	return localstorage.TempFile(prefix)
}

func LocalMediaPath(oid string) (string, error) {
	return localstorage.Objects().BuildObjectPath(oid)
}

func LocalMediaPathReadOnly(oid string) string {
	return localstorage.Objects().ObjectPath(oid)
}

func LocalReferencePath(sha string) string {
	if config.LocalReferenceDir == "" {
		return ""
	}
	return filepath.Join(config.LocalReferenceDir, sha[0:2], sha[2:4], sha)
}

func ObjectExistsOfSize(oid string, size int64) bool {
	path := localstorage.Objects().ObjectPath(oid)
	return tools.FileExistsOfSize(path, size)
}

func Environ(cfg *config.Configuration, manifest *tq.Manifest) []string {
	osEnviron := os.Environ()
	env := make([]string, 0, len(osEnviron)+7)

	api, err := lfsapi.NewClient(cfg.Os, cfg.Git)
	if err != nil {
		// TODO(@ttaylorr): don't panic
		panic(err.Error())
	}

	download := api.Endpoints.AccessFor(api.Endpoints.Endpoint("download", cfg.CurrentRemote).Url)
	upload := api.Endpoints.AccessFor(api.Endpoints.Endpoint("upload", cfg.CurrentRemote).Url)

	dltransfers := manifest.GetDownloadAdapterNames()
	sort.Strings(dltransfers)
	ultransfers := manifest.GetUploadAdapterNames()
	sort.Strings(ultransfers)

	fetchPruneConfig := cfg.FetchPruneConfig()
	storageConfig := cfg.StorageConfig()

	env = append(env,
		fmt.Sprintf("LocalWorkingDir=%s", config.LocalWorkingDir),
		fmt.Sprintf("LocalGitDir=%s", config.LocalGitDir),
		fmt.Sprintf("LocalGitStorageDir=%s", config.LocalGitStorageDir),
		fmt.Sprintf("LocalMediaDir=%s", LocalMediaDir()),
		fmt.Sprintf("LocalReferenceDir=%s", config.LocalReferenceDir),
		fmt.Sprintf("TempDir=%s", TempDir()),
		fmt.Sprintf("ConcurrentTransfers=%d", api.ConcurrentTransfers),
		fmt.Sprintf("TusTransfers=%v", cfg.TusTransfersAllowed()),
		fmt.Sprintf("BasicTransfersOnly=%v", cfg.BasicTransfersOnly()),
		fmt.Sprintf("SkipDownloadErrors=%v", cfg.SkipDownloadErrors()),
		fmt.Sprintf("FetchRecentAlways=%v", fetchPruneConfig.FetchRecentAlways),
		fmt.Sprintf("FetchRecentRefsDays=%d", fetchPruneConfig.FetchRecentRefsDays),
		fmt.Sprintf("FetchRecentCommitsDays=%d", fetchPruneConfig.FetchRecentCommitsDays),
		fmt.Sprintf("FetchRecentRefsIncludeRemotes=%v", fetchPruneConfig.FetchRecentRefsIncludeRemotes),
		fmt.Sprintf("PruneOffsetDays=%d", fetchPruneConfig.PruneOffsetDays),
		fmt.Sprintf("PruneVerifyRemoteAlways=%v", fetchPruneConfig.PruneVerifyRemoteAlways),
		fmt.Sprintf("PruneRemoteName=%s", fetchPruneConfig.PruneRemoteName),
		fmt.Sprintf("LfsStorageDir=%s", storageConfig.LfsStorageDir),
		fmt.Sprintf("AccessDownload=%s", download),
		fmt.Sprintf("AccessUpload=%s", upload),
		fmt.Sprintf("DownloadTransfers=%s", strings.Join(dltransfers, ",")),
		fmt.Sprintf("UploadTransfers=%s", strings.Join(ultransfers, ",")),
	)
	if len(cfg.FetchExcludePaths()) > 0 {
		env = append(env, fmt.Sprintf("FetchExclude=%s", strings.Join(cfg.FetchExcludePaths(), ", ")))
	}
	if len(cfg.FetchIncludePaths()) > 0 {
		env = append(env, fmt.Sprintf("FetchInclude=%s", strings.Join(cfg.FetchIncludePaths(), ", ")))
	}
	for _, ext := range cfg.Extensions() {
		env = append(env, fmt.Sprintf("Extension[%d]=%s", ext.Priority, ext.Name))
	}

	for _, e := range osEnviron {
		if !strings.Contains(strings.SplitN(e, "=", 2)[0], "GIT_") {
			continue
		}
		env = append(env, e)
	}

	return env
}

func InRepo() bool {
	return config.LocalGitDir != ""
}

func ClearTempObjects() error {
	if localstorage.Objects() == nil {
		return nil
	}
	return localstorage.Objects().ClearTempObjects()
}

func ScanObjectsChan() <-chan localstorage.Object {
	return localstorage.Objects().ScanObjectsChan()
}

func init() {
	tracerx.DefaultKey = "GIT"
	tracerx.Prefix = "trace git-lfs: "
	if len(os.Getenv("GIT_TRACE")) < 1 {
		if tt := os.Getenv("GIT_TRANSFER_TRACE"); len(tt) > 0 {
			os.Setenv("GIT_TRACE", tt)
		}
	}
}

const (
	gitExt       = ".git"
	gitPtrPrefix = "gitdir: "
)

// only used in tests
func AllObjects() []localstorage.Object {
	return localstorage.Objects().AllObjects()
}

func LinkOrCopyFromReference(oid string, size int64) error {
	if ObjectExistsOfSize(oid, size) {
		return nil
	}
	altMediafile := LocalReferencePath(oid)
	mediafile, err := LocalMediaPath(oid)
	if err != nil {
		return err
	}
	if altMediafile != "" && tools.FileExistsOfSize(altMediafile, size) {
		return LinkOrCopy(altMediafile, mediafile)
	}
	return nil
}
