package engine

import (
	"context"
	"errors"
	"fmt"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/gitaly"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/gitlab"
	gapi "gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/gitlab/api"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/module/autoflow/flow"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/cache"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/git"
	"go.starlark.net/starlark"
)

const (
	localGitalySourceState = "localGitalySourceState"
)

var (
	_ flow.Source = (*gitalySource)(nil)
)

type gitLabSource struct {
	projectPath string
	fullGitRef  string
}

type gitalySourceState struct {
	sources map[string]gitLabSource
}

type gitalySource struct {
	gitalyPool    gitaly.PoolInterface
	gitLabClient  gitlab.ClientInterface
	repoInfoCache *cache.CacheWithErr[RepoInfoCacheKey, *api.ProjectInfo]
	maxFileSize   int64
}

func (s *gitalySource) SetupThreadLocal(thread *starlark.Thread, projectPath string) {
	thread.SetLocal(localGitalySourceState, &gitalySourceState{
		sources: map[string]gitLabSource{
			"": {
				projectPath: projectPath,
			},
		},
	})
}

func (s *gitalySource) RecordSource(thread *starlark.Thread, name, projectPath, fullGitRef string) error {
	ss, ok := thread.Local(localGitalySourceState).(*gitalySourceState)
	if !ok {
		return errors.New("internal error: local gitalySourceState not found")
	}
	if _, ok = ss.sources[name]; ok {
		return fmt.Errorf("duplicate source(name = %s)", name)
	}
	ss.sources[name] = gitLabSource{
		projectPath: projectPath,
		fullGitRef:  fullGitRef,
	}
	return nil
}

func (s *gitalySource) LoadFile(ctx context.Context, thread *starlark.Thread, ref flow.ModuleRef) ([]byte, error) {
	ss, ok := thread.Local(localGitalySourceState).(*gitalySourceState)
	if !ok {
		return nil, errors.New("internal error: local gitalySourceState not found")
	}
	src, ok := ss.sources[ref.SourceName]
	if !ok {
		return nil, fmt.Errorf("unknown source(name = %s)", ref.SourceName)
	}
	projectPath := src.projectPath
	fetchRef := src.fullGitRef
	info, err := s.getRepoInfo(ctx, projectPath)
	if err != nil {
		return nil, fmt.Errorf("get repo info: %w", err)
	}
	fetcher, err := s.gitalyPool.PathFetcher(ctx, info.GitalyInfo)
	if err != nil {
		return nil, err
	}
	if fetchRef == "" || fetchRef == gitaly.DefaultBranch {
		fetchRef = git.ExplicitRefOrHead(info.DefaultBranch)
	}
	return fetcher.FetchFile(ctx, info.Repository, []byte(fetchRef), []byte(ref.File), s.maxFileSize)
}

func (s *gitalySource) getRepoInfo(ctx context.Context, projectID string) (*api.ProjectInfo, error) {
	key := RepoInfoCacheKey{ProjectID: projectID}
	return s.repoInfoCache.GetItem(ctx, key, func() (*api.ProjectInfo, error) {
		return gapi.GetRepositoryInfo(ctx, s.gitLabClient, projectID, gitlab.WithoutRetries())
	})
}
