// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.

package repo

import (
	"context"
	"encoding/base64"
	"fmt"
	"io"

	"github.com/harness/gitness/gitrpc"
	apiauth "github.com/harness/gitness/internal/api/auth"
	"github.com/harness/gitness/internal/api/controller"
	"github.com/harness/gitness/internal/auth"
	"github.com/harness/gitness/types"
	"github.com/harness/gitness/types/enum"
)

const (
	// maxGetContentFileSize specifies the maximum number of bytes a file content response contains.
	// If a file is any larger, the content is truncated.
	maxGetContentFileSize = 1 << 22 // 4 MB
)

type ContentType string

const (
	ContentTypeFile      ContentType = "file"
	ContentTypeDir       ContentType = "dir"
	ContentTypeSymlink   ContentType = "symlink"
	ContentTypeSubmodule ContentType = "submodule"
)

type ContentInfo struct {
	Type         ContentType   `json:"type"`
	SHA          string        `json:"sha"`
	Name         string        `json:"name"`
	Path         string        `json:"path"`
	LatestCommit *types.Commit `json:"latest_commit,omitempty"`
}

type GetContentOutput struct {
	ContentInfo
	Content Content `json:"content"`
}

// Content restricts the possible types of content returned by the api.
type Content interface {
	isContent()
}

type FileContent struct {
	Encoding enum.ContentEncodingType `json:"encoding"`
	Data     string                   `json:"data"`
	Size     int64                    `json:"size"`
	DataSize int64                    `json:"data_size"`
}

func (c *FileContent) isContent() {}

type SymlinkContent struct {
	Target string `json:"target"`
	Size   int64  `json:"size"`
}

func (c *SymlinkContent) isContent() {}

type DirContent struct {
	Entries []ContentInfo `json:"entries"`
}

func (c *DirContent) isContent() {}

type SubmoduleContent struct {
	URL       string `json:"url"`
	CommitSHA string `json:"commit_sha"`
}

func (c *SubmoduleContent) isContent() {}

/*
 * GetContent finds the content of the repo at the given path.
 * If no gitRef is provided, the content is retrieved from the default branch.
 * If includeLatestCommit is enabled, the response contains information of the latest commit that changed the object.
 */
func (c *Controller) GetContent(ctx context.Context, session *auth.Session, repoRef string,
	gitRef string, repoPath string, includeLatestCommit bool) (*GetContentOutput, error) {
	repo, err := c.repoStore.FindByRef(ctx, repoRef)
	if err != nil {
		return nil, err
	}

	if err = apiauth.CheckRepo(ctx, c.authorizer, session, repo, enum.PermissionRepoView, true); err != nil {
		return nil, err
	}

	// set gitRef to default branch in case an empty reference was provided
	if gitRef == "" {
		gitRef = repo.DefaultBranch
	}

	// create read params once
	readParams := CreateRPCReadParams(repo)

	treeNodeOutput, err := c.gitRPCClient.GetTreeNode(ctx, &gitrpc.GetTreeNodeParams{
		ReadParams:          readParams,
		GitREF:              gitRef,
		Path:                repoPath,
		IncludeLatestCommit: includeLatestCommit,
	})
	if err != nil {
		return nil, err
	}

	info, err := mapToContentInfo(&treeNodeOutput.Node, treeNodeOutput.Commit)
	if err != nil {
		return nil, err
	}

	var content Content
	switch info.Type {
	case ContentTypeDir:
		// for getContent we don't want any recursiveness for dir content.
		content, err = c.getDirContent(ctx, readParams, gitRef, repoPath, includeLatestCommit, false)
	case ContentTypeFile:
		content, err = c.getFileContent(ctx, readParams, info.SHA)
	case ContentTypeSymlink:
		content, err = c.getSymlinkContent(ctx, readParams, info.SHA)
	case ContentTypeSubmodule:
		content, err = c.getSubmoduleContent(ctx, readParams, gitRef, repoPath, info.SHA)
	default:
		err = fmt.Errorf("unknown tree node type '%s'", treeNodeOutput.Node.Type)
	}

	if err != nil {
		return nil, err
	}

	return &GetContentOutput{
		ContentInfo: *info,
		Content:     content,
	}, nil
}

func (c *Controller) getSubmoduleContent(ctx context.Context, readParams gitrpc.ReadParams, gitRef string,
	repoPath string, commitSHA string) (*SubmoduleContent, error) {
	output, err := c.gitRPCClient.GetSubmodule(ctx, &gitrpc.GetSubmoduleParams{
		ReadParams: readParams,
		GitREF:     gitRef,
		Path:       repoPath,
	})
	if err != nil {
		// TODO: handle not found error
		// This requires gitrpc to also return notfound though!
		return nil, fmt.Errorf("failed to get submodule: %w", err)
	}

	return &SubmoduleContent{
		URL:       output.Submodule.URL,
		CommitSHA: commitSHA,
	}, nil
}

func (c *Controller) getFileContent(ctx context.Context, readParams gitrpc.ReadParams,
	blobSHA string) (*FileContent, error) {
	output, err := c.gitRPCClient.GetBlob(ctx, &gitrpc.GetBlobParams{
		ReadParams: readParams,
		SHA:        blobSHA,
		SizeLimit:  maxGetContentFileSize,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to get file content: %w", err)
	}

	content, err := io.ReadAll(output.Content)
	if err != nil {
		return nil, fmt.Errorf("failed to read blob content: %w", err)
	}

	return &FileContent{
		Size:     output.Size,
		DataSize: output.ContentSize,
		Encoding: enum.ContentEncodingTypeBase64,
		Data:     base64.StdEncoding.EncodeToString(content),
	}, nil
}

func (c *Controller) getSymlinkContent(ctx context.Context, readParams gitrpc.ReadParams,
	blobSHA string) (*SymlinkContent, error) {
	output, err := c.gitRPCClient.GetBlob(ctx, &gitrpc.GetBlobParams{
		ReadParams: readParams,
		SHA:        blobSHA,
		SizeLimit:  maxGetContentFileSize, // TODO: do we need to guard against too big symlinks?
	})
	if err != nil {
		// TODO: handle not found error
		// This requires gitrpc to also return notfound though!
		return nil, fmt.Errorf("failed to get symlink: %w", err)
	}

	content, err := io.ReadAll(output.Content)
	if err != nil {
		return nil, fmt.Errorf("failed to read blob content: %w", err)
	}

	return &SymlinkContent{
		Size:   output.Size,
		Target: string(content),
	}, nil
}

func (c *Controller) getDirContent(ctx context.Context, readParams gitrpc.ReadParams, gitRef string,
	repoPath string, includeLatestCommit bool, recursive bool) (*DirContent, error) {
	output, err := c.gitRPCClient.ListTreeNodes(ctx, &gitrpc.ListTreeNodeParams{
		ReadParams:          readParams,
		GitREF:              gitRef,
		Path:                repoPath,
		IncludeLatestCommit: includeLatestCommit,
		Recursive:           recursive,
	})
	if err != nil {
		// TODO: handle not found error
		// This requires gitrpc to also return notfound though!
		return nil, fmt.Errorf("failed to get content of dir: %w", err)
	}

	entries := make([]ContentInfo, len(output.Nodes))
	for i := range output.Nodes {
		node := output.Nodes[i]

		var entry *ContentInfo
		entry, err = mapToContentInfo(&node.TreeNode, node.Commit)
		if err != nil {
			return nil, err
		}
		entries[i] = *entry
	}

	return &DirContent{
		Entries: entries,
	}, nil
}

func mapToContentInfo(node *gitrpc.TreeNode, commit *gitrpc.Commit) (*ContentInfo, error) {
	// node data is expected
	if node == nil {
		return nil, fmt.Errorf("node can't be nil")
	}
	typ, err := mapNodeModeToContentType(node.Mode)
	if err != nil {
		return nil, err
	}

	res := &ContentInfo{
		Type: typ,
		SHA:  node.SHA,
		Name: node.Name,
		Path: node.Path,
	}

	// parse commit only if available
	if commit != nil {
		res.LatestCommit, err = controller.MapCommit(commit)
		if err != nil {
			return nil, err
		}
	}

	return res, nil
}

func mapNodeModeToContentType(m gitrpc.TreeNodeMode) (ContentType, error) {
	switch m {
	case gitrpc.TreeNodeModeFile, gitrpc.TreeNodeModeExec:
		return ContentTypeFile, nil
	case gitrpc.TreeNodeModeSymlink:
		return ContentTypeSymlink, nil
	case gitrpc.TreeNodeModeCommit:
		return ContentTypeSubmodule, nil
	case gitrpc.TreeNodeModeTree:
		return ContentTypeDir, nil
	default:
		return ContentTypeFile, fmt.Errorf("unsupported tree node mode '%s'", m)
	}
}
