// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package scm

import (
	"context"
	"fmt"
	"io"
	"net/url"
	"path"
	"strings"
	"time"

	"github.com/harness/gitness/app/api/usererror"
	"github.com/harness/gitness/app/bootstrap"
	"github.com/harness/gitness/app/jwt"
	"github.com/harness/gitness/app/services/refcache"
	"github.com/harness/gitness/app/store"
	"github.com/harness/gitness/app/token"
	urlprovider "github.com/harness/gitness/app/url"
	"github.com/harness/gitness/git"
	"github.com/harness/gitness/types"
	"github.com/harness/gitness/types/enum"
)

var _ Provider = (*GitnessSCM)(nil)

var gitspaceJWTLifetime = 720 * 24 * time.Hour

const defaultGitspacePATIdentifier = "Gitspace_Default"

type GitnessSCM struct {
	git            git.Interface
	repoStore      store.RepoStore
	repoFinder     refcache.RepoFinder
	tokenStore     store.TokenStore
	principalStore store.PrincipalStore
	urlProvider    urlprovider.Provider
}

// ListBranches implements Provider.
func (s *GitnessSCM) ListBranches(
	ctx context.Context,
	filter *BranchFilter,
	_ *ResolvedCredentials,
) ([]Branch, error) {
	repo, err := s.repoFinder.FindByRef(ctx, filter.Repository)
	if err != nil {
		return nil, fmt.Errorf("failed to find repo: %w", err)
	}
	rpcOut, err := s.git.ListBranches(ctx, &git.ListBranchesParams{
		ReadParams:    git.CreateReadParams(repo),
		IncludeCommit: false,
		Query:         filter.Query,
		Sort:          git.BranchSortOptionDate,
		Order:         git.SortOrderDesc,
		Page:          int32(filter.Page),
		PageSize:      int32(filter.Size),
	})
	if err != nil {
		return nil, err
	}
	branches := make([]Branch, len(rpcOut.Branches))
	for i := range rpcOut.Branches {
		branches[i] = mapBranch(rpcOut.Branches[i])
	}

	return branches, nil
}

func mapBranch(b git.Branch) Branch {
	return Branch{
		Name: b.Name,
		SHA:  b.SHA.String(),
	}
}

// ListRepositories implements Provider.
func (s *GitnessSCM) ListRepositories(
	ctx context.Context,
	filter *RepositoryFilter,
	_ *ResolvedCredentials,
) ([]Repository, error) {
	repos, err := s.repoStore.List(ctx, filter.SpaceID, &types.RepoFilter{
		Page:  filter.Page,
		Size:  filter.Size,
		Query: filter.Query,
		Sort:  enum.RepoAttrUpdated,
		Order: enum.OrderDesc,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to list child repos: %w", err)
	}
	var reposOut []Repository
	for _, repo := range repos {
		// backfill URLs
		repo.GitURL = s.urlProvider.GenerateGITCloneURL(ctx, repo.Path)
		repo.GitSSHURL = s.urlProvider.GenerateGITCloneSSHURL(ctx, repo.Path)

		repoOut, err := mapRepository(repo)
		if err != nil {
			return nil, fmt.Errorf("failed to get repo %q output: %w", repo.Path, err)
		}

		reposOut = append(reposOut, repoOut)
	}
	return reposOut, nil
}

func mapRepository(repo *types.Repository) (Repository, error) {
	if repo == nil {
		return Repository{}, fmt.Errorf("repository is null")
	}
	return Repository{
		Name:          repo.Identifier,
		DefaultBranch: repo.DefaultBranch,
		GitURL:        repo.GitURL,
		GitSSHURL:     repo.GitSSHURL,
	}, nil
}

func NewGitnessSCM(
	repoStore store.RepoStore,
	repoFinder refcache.RepoFinder,
	git git.Interface,
	tokenStore store.TokenStore,
	principalStore store.PrincipalStore,
	urlProvider urlprovider.Provider,
) *GitnessSCM {
	return &GitnessSCM{
		repoStore:      repoStore,
		repoFinder:     repoFinder,
		git:            git,
		tokenStore:     tokenStore,
		principalStore: principalStore,
		urlProvider:    urlProvider,
	}
}

func (s *GitnessSCM) ResolveCredentials(
	ctx context.Context,
	gitspaceConfig types.GitspaceConfig,
) (*ResolvedCredentials, error) {
	repoURL, err := url.Parse(gitspaceConfig.CodeRepo.URL)
	if err != nil {
		return nil, fmt.Errorf("failed to parse repository URL %s: %w", gitspaceConfig.CodeRepo.URL, err)
	}
	repoName := strings.TrimSuffix(path.Base(repoURL.Path), ".git")
	repo, err := s.repoFinder.FindByRef(ctx, *gitspaceConfig.CodeRepo.Ref)
	if err != nil {
		return nil, fmt.Errorf("failed to find repository: %w", err)
	}
	cloneURL, err := url.Parse(repo.Path)
	if err != nil {
		return nil, fmt.Errorf("failed to parse clone url '%s': %w", cloneURL, err)
	}
	// Backfill clone URL
	gitURL := s.urlProvider.GenerateContainerGITCloneURL(ctx, repo.Path)
	resolvedCredentails := &ResolvedCredentials{Branch: gitspaceConfig.CodeRepo.Branch}
	resolvedCredentails.RepoName = repoName
	gitspacePrincipal := bootstrap.NewGitspaceServiceSession().Principal
	user, err := findUserFromUID(ctx, s.principalStore, gitspaceConfig.GitspaceUser.Identifier)
	if err != nil {
		return nil, err
	}
	var jwtToken string
	existingToken, _ := s.tokenStore.FindByIdentifier(ctx, user.ID, defaultGitspacePATIdentifier)
	if existingToken != nil {
		// create jwt token.
		jwtToken, err = jwt.GenerateForToken(existingToken, user.ToPrincipal().Salt)
		if err != nil {
			return nil, fmt.Errorf("failed to create JWT token: %w", err)
		}
	} else {
		_, jwtToken, err = token.CreatePAT(
			ctx,
			s.tokenStore,
			&gitspacePrincipal,
			user,
			defaultGitspacePATIdentifier,
			&gitspaceJWTLifetime)
	}
	if err != nil {
		return nil, fmt.Errorf("failed to create JWT: %w", err)
	}
	modifiedURL, err := url.Parse(gitURL)
	if err != nil {
		return nil, fmt.Errorf("error while parsing the clone url: %s", gitURL)
	}
	userInfo := url.UserPassword("harness", jwtToken)
	modifiedURL.User = userInfo
	resolvedCredentails.CloneURL = types.NewMaskSecret(modifiedURL.String())
	credentials := &Credentials{
		Email:    user.Email,
		Name:     types.NewMaskSecret(user.DisplayName),
		Password: types.NewMaskSecret(jwtToken),
	}
	resolvedCredentails.Credentials = credentials
	return resolvedCredentails, nil
}

func (s *GitnessSCM) GetFileContent(
	ctx context.Context,
	gitspaceConfig types.GitspaceConfig,
	filePath string,
	_ *ResolvedCredentials,
) ([]byte, error) {
	repo, err := s.repoFinder.FindByRef(ctx, *gitspaceConfig.CodeRepo.Ref)
	if err != nil {
		return nil, fmt.Errorf("failed to find repository: %w", err)
	}
	// create read params once
	readParams := git.CreateReadParams(repo)
	treeNodeOutput, err := s.git.GetTreeNode(ctx, &git.GetTreeNodeParams{
		ReadParams:          readParams,
		GitREF:              gitspaceConfig.CodeRepo.Branch,
		Path:                filePath,
		IncludeLatestCommit: false,
	})
	if err != nil {
		return make([]byte, 0), nil //nolint:nilerr
	}

	// viewing Raw content is only supported for blob content
	if treeNodeOutput.Node.Type != git.TreeNodeTypeBlob {
		return nil, usererror.BadRequestf(
			"Object in '%s' at '/%s' is of type '%s'. Only objects of type %s support raw viewing.",
			gitspaceConfig.CodeRepo.Branch, filePath, treeNodeOutput.Node.Type, git.TreeNodeTypeBlob)
	}

	blobReader, err := s.git.GetBlob(ctx, &git.GetBlobParams{
		ReadParams: readParams,
		SHA:        treeNodeOutput.Node.SHA,
		SizeLimit:  0, // no size limit, we stream whatever data there is
	})
	if err != nil {
		return nil, fmt.Errorf("failed to read blob: %w", err)
	}
	catFileOutput, err := io.ReadAll(blobReader.Content)
	if err != nil {
		return nil, fmt.Errorf("failed to read blob content: %w", err)
	}
	return catFileOutput, nil
}

func findUserFromUID(
	ctx context.Context,
	principalStore store.PrincipalStore, userUID string,
) (*types.User, error) {
	return principalStore.FindUserByUID(ctx, userUID)
}

func (s *GitnessSCM) GetBranchURL(_ string, repoURL string, branch string) (string, error) {
	return fmt.Sprintf("%s/files/%s", strings.TrimSuffix(repoURL, ".git"), branch), nil
}
