// 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 gitrpc

import (
	"context"
	"errors"
	"fmt"
	"io"

	"github.com/harness/gitness/gitrpc/rpc"

	"github.com/rs/zerolog/log"
)

// TreeNodeType specifies the different types of nodes in a git tree.
// IMPORTANT: has to be consistent with rpc.TreeNodeType (proto).
type TreeNodeType string

const (
	TreeNodeTypeTree   TreeNodeType = "tree"
	TreeNodeTypeBlob   TreeNodeType = "blob"
	TreeNodeTypeCommit TreeNodeType = "commit"
)

// TreeNodeMode specifies the different modes of a node in a git tree.
// IMPORTANT: has to be consistent with rpc.TreeNodeMode (proto).
type TreeNodeMode string

const (
	TreeNodeModeFile    TreeNodeMode = "file"
	TreeNodeModeSymlink TreeNodeMode = "symlink"
	TreeNodeModeExec    TreeNodeMode = "exec"
	TreeNodeModeTree    TreeNodeMode = "tree"
	TreeNodeModeCommit  TreeNodeMode = "commit"
)

type TreeNode struct {
	Type TreeNodeType
	Mode TreeNodeMode
	SHA  string
	Name string
	Path string
}

type ListTreeNodeParams struct {
	ReadParams
	// GitREF is a git reference (branch / tag / commit SHA)
	GitREF              string
	Path                string
	IncludeLatestCommit bool
	Recursive           bool
}

type ListTreeNodeOutput struct {
	Nodes []TreeNodeWithCommit
}

type TreeNodeWithCommit struct {
	TreeNode
	Commit *Commit
}

type GetTreeNodeParams struct {
	ReadParams
	// GitREF is a git reference (branch / tag / commit SHA)
	GitREF              string
	Path                string
	IncludeLatestCommit bool
}

type GetTreeNodeOutput struct {
	Node   TreeNode
	Commit *Commit
}

func (c *Client) GetTreeNode(ctx context.Context, params *GetTreeNodeParams) (*GetTreeNodeOutput, error) {
	if params == nil {
		return nil, ErrNoParamsProvided
	}
	resp, err := c.repoService.GetTreeNode(ctx, &rpc.GetTreeNodeRequest{
		Base:                mapToRPCReadRequest(params.ReadParams),
		GitRef:              params.GitREF,
		Path:                params.Path,
		IncludeLatestCommit: params.IncludeLatestCommit,
	})
	if err != nil {
		return nil, processRPCErrorf(err, "failed to get tree node from server")
	}

	node, err := mapRPCTreeNode(resp.GetNode())
	if err != nil {
		return nil, fmt.Errorf("failed to map rpc node: %w", err)
	}
	var commit *Commit
	if resp.GetCommit() != nil {
		commit, err = mapRPCCommit(resp.GetCommit())
		if err != nil {
			return nil, fmt.Errorf("failed to map rpc commit: %w", err)
		}
	}

	return &GetTreeNodeOutput{
		Node:   node,
		Commit: commit,
	}, nil
}

func (c *Client) ListTreeNodes(ctx context.Context, params *ListTreeNodeParams) (*ListTreeNodeOutput, error) {
	if params == nil {
		return nil, ErrNoParamsProvided
	}
	stream, err := c.repoService.ListTreeNodes(ctx, &rpc.ListTreeNodesRequest{
		Base:                mapToRPCReadRequest(params.ReadParams),
		GitRef:              params.GitREF,
		Path:                params.Path,
		IncludeLatestCommit: params.IncludeLatestCommit,
		Recursive:           params.Recursive,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to start stream for tree nodes: %w", err)
	}

	nodes := make([]TreeNodeWithCommit, 0, 16)
	for {
		var next *rpc.ListTreeNodesResponse
		next, err = stream.Recv()
		if errors.Is(err, io.EOF) {
			log.Ctx(ctx).Debug().Msg("received end of stream")
			break
		}
		if err != nil {
			return nil, processRPCErrorf(err, "received unexpected error from server")
		}

		var node TreeNode
		node, err = mapRPCTreeNode(next.GetNode())
		if err != nil {
			return nil, fmt.Errorf("failed to map rpc node: %w", err)
		}
		var commit *Commit
		if next.GetCommit() != nil {
			commit, err = mapRPCCommit(next.GetCommit())
			if err != nil {
				return nil, fmt.Errorf("failed to map rpc commit: %w", err)
			}
		}

		nodes = append(nodes, TreeNodeWithCommit{
			TreeNode: node,
			Commit:   commit,
		})
	}

	return &ListTreeNodeOutput{
		Nodes: nodes,
	}, nil
}
