package github

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

	ghErrors "github.com/github/github-mcp-server/pkg/errors"
	"github.com/github/github-mcp-server/pkg/ifc"
	"github.com/github/github-mcp-server/pkg/inventory"
	"github.com/github/github-mcp-server/pkg/scopes"
	"github.com/github/github-mcp-server/pkg/translations"
	"github.com/github/github-mcp-server/pkg/utils"
	"github.com/google/go-github/v89/github"
	"github.com/google/jsonschema-go/jsonschema"
	"github.com/modelcontextprotocol/go-sdk/mcp"
)

// TreeEntryResponse represents a single entry in a Git tree.
type TreeEntryResponse struct {
	Path string `json:"path"`
	Type string `json:"type"`
	Size *int   `json:"size,omitempty"`
	Mode string `json:"mode"`
	SHA  string `json:"sha"`
	URL  string `json:"url"`
}

// TreeResponse represents the response structure for a Git tree.
type TreeResponse struct {
	SHA       string              `json:"sha"`
	Truncated bool                `json:"truncated"`
	Tree      []TreeEntryResponse `json:"tree"`
	TreeSHA   string              `json:"tree_sha"`
	Owner     string              `json:"owner"`
	Repo      string              `json:"repo"`
	Recursive bool                `json:"recursive"`
	Count     int                 `json:"count"`
}

// GetRepositoryTree creates a tool to get the tree structure of a GitHub repository.
func GetRepositoryTree(t translations.TranslationHelperFunc) inventory.ServerTool {
	return NewTool(
		ToolsetMetadataGit,
		mcp.Tool{
			Name:        "get_repository_tree",
			Description: t("TOOL_GET_REPOSITORY_TREE_DESCRIPTION", "Get the tree structure (files and directories) of a GitHub repository at a specific ref or SHA"),
			Annotations: &mcp.ToolAnnotations{
				Title:        t("TOOL_GET_REPOSITORY_TREE_USER_TITLE", "Get repository tree"),
				ReadOnlyHint: true,
			},
			InputSchema: &jsonschema.Schema{
				Type: "object",
				Properties: map[string]*jsonschema.Schema{
					"owner": {
						Type:        "string",
						Description: "Repository owner (username or organization)",
					},
					"repo": {
						Type:        "string",
						Description: "Repository name",
					},
					"tree_sha": {
						Type:        "string",
						Description: "The SHA1 value or ref (branch or tag) name of the tree. Defaults to the repository's default branch",
					},
					"recursive": {
						Type:        "boolean",
						Description: "Setting this parameter to true returns the objects or subtrees referenced by the tree. Default is false",
						Default:     json.RawMessage(`false`),
					},
					"path_filter": {
						Type:        "string",
						Description: "Optional path prefix to filter the tree results (e.g., 'src/' to only show files in the src directory)",
					},
				},
				Required: []string{"owner", "repo"},
			},
		},
		[]scopes.Scope{scopes.Repo},
		func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
			owner, err := RequiredParam[string](args, "owner")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			repo, err := RequiredParam[string](args, "repo")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			treeSHA, err := OptionalParam[string](args, "tree_sha")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			recursive, err := OptionalBoolParamWithDefault(args, "recursive", false)
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			pathFilter, err := OptionalParam[string](args, "path_filter")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			client, err := deps.GetClient(ctx)
			if err != nil {
				return utils.NewToolResultError("failed to get GitHub client"), nil, nil
			}

			// If no tree_sha is provided, use the repository's default branch
			if treeSHA == "" {
				repoInfo, repoResp, err := client.Repositories.Get(ctx, owner, repo)
				if err != nil {
					return ghErrors.NewGitHubAPIErrorResponse(ctx,
						"failed to get repository info",
						repoResp,
						err,
					), nil, nil
				}
				treeSHA = *repoInfo.DefaultBranch
			}

			// Get the tree using the GitHub Git Tree API
			tree, resp, err := client.Git.GetTree(ctx, owner, repo, treeSHA, recursive)
			if err != nil {
				return ghErrors.NewGitHubAPIErrorResponse(ctx,
					"failed to get repository tree",
					resp,
					err,
				), nil, nil
			}
			defer func() { _ = resp.Body.Close() }()

			// Filter tree entries if path_filter is provided
			var filteredEntries []*github.TreeEntry
			if pathFilter != "" {
				for _, entry := range tree.Entries {
					if strings.HasPrefix(entry.GetPath(), pathFilter) {
						filteredEntries = append(filteredEntries, entry)
					}
				}
			} else {
				filteredEntries = tree.Entries
			}

			treeEntries := make([]TreeEntryResponse, len(filteredEntries))
			for i, entry := range filteredEntries {
				treeEntries[i] = TreeEntryResponse{
					Path: entry.GetPath(),
					Type: entry.GetType(),
					Mode: entry.GetMode(),
					SHA:  entry.GetSHA(),
					URL:  entry.GetURL(),
				}
				if entry.Size != nil {
					treeEntries[i].Size = entry.Size
				}
			}

			response := TreeResponse{
				SHA:       *tree.SHA,
				Truncated: *tree.Truncated,
				Tree:      treeEntries,
				TreeSHA:   treeSHA,
				Owner:     owner,
				Repo:      repo,
				Recursive: recursive,
				Count:     len(filteredEntries),
			}

			r, err := json.Marshal(response)
			if err != nil {
				return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
			}

			result := utils.NewToolResultText(string(r))
			// The repository tree exposes committed file structure; in public
			// repos anyone can land content via a PR (untrusted), in private
			// repos only collaborators can (trusted). Confidentiality follows
			// repo visibility.
			result = attachRepoVisibilityIFCLabel(ctx, deps, client, owner, repo, result, ifc.LabelCommitContents)
			return result, nil, nil
		},
	)
}
