package github

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"sort"
	"strings"

	ghErrors "github.com/github/github-mcp-server/pkg/errors"
	"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"
	"github.com/shurcooL/githubv4"
)

// uiGetMaxPages bounds how many pages each ui_get pagination loop will fetch.
// ui_get backs a synchronous UI picker (dropdowns for labels/assignees/etc. in
// the MCP App issue/PR write surfaces), so responsiveness matters more than
// completeness. At PerPage 100 this caps a call at ~1000 items and a bounded
// number of API round-trips, keeping latency predictable on very large
// repos/orgs. Results past the cap are truncated and surfaced via a "has_more"
// flag, which is acceptable because the picker pairs truncation with typeahead.
const uiGetMaxPages = 10

// UIGet creates a tool to fetch UI data for MCP Apps.
func UIGet(t translations.TranslationHelperFunc) inventory.ServerTool {
	st := NewTool(
		ToolsetMetadataContext, // Use context toolset so it's always available
		mcp.Tool{
			Name:        "ui_get",
			Description: t("TOOL_UI_GET_DESCRIPTION", "Fetch UI data for MCP Apps (labels, assignees, milestones, issue types, branches, issue fields, reviewers)."),
			Annotations: &mcp.ToolAnnotations{
				Title:        t("TOOL_UI_GET_USER_TITLE", "Get UI data"),
				ReadOnlyHint: true,
			},
			// ui_get only backs MCP App views; declaring app-only visibility keeps
			// it out of the agent's tool list while remaining callable by the views
			// via tools/call (per the MCP Apps 2026-01-26 spec).
			Meta: mcp.Meta{
				"ui": map[string]any{
					"visibility": []string{"app"},
				},
			},
			InputSchema: &jsonschema.Schema{
				Type: "object",
				Properties: map[string]*jsonschema.Schema{
					"method": {
						Type:        "string",
						Enum:        []any{"labels", "assignees", "milestones", "issue_types", "branches", "issue_fields", "reviewers"},
						Description: "The type of data to fetch",
					},
					"owner": {
						Type:        "string",
						Description: "Repository owner (required for all methods)",
					},
					"repo": {
						Type:        "string",
						Description: "Repository name (required for labels, assignees, milestones, branches, issue fields, reviewers)",
					},
				},
				Required: []string{"method", "owner"},
			},
		},
		[]scopes.Scope{scopes.Repo, scopes.ReadOrg},
		func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
			method, err := RequiredParam[string](args, "method")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			owner, err := RequiredParam[string](args, "owner")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			switch method {
			case "labels":
				return uiGetLabels(ctx, deps, args, owner)
			case "assignees":
				return uiGetAssignees(ctx, deps, args, owner)
			case "milestones":
				return uiGetMilestones(ctx, deps, args, owner)
			case "issue_types":
				return uiGetIssueTypes(ctx, deps, owner)
			case "branches":
				return uiGetBranches(ctx, deps, args, owner)
			case "issue_fields":
				return uiGetIssueFields(ctx, deps, args, owner)
			case "reviewers":
				return uiGetReviewers(ctx, deps, args, owner)
			default:
				return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
			}
		})
	st.FeatureFlagEnable = MCPAppsFeatureFlag
	return st
}

func uiGetLabels(ctx context.Context, deps ToolDependencies, args map[string]any, owner string) (*mcp.CallToolResult, any, error) {
	repo, err := RequiredParam[string](args, "repo")
	if err != nil {
		return utils.NewToolResultError(err.Error()), nil, nil
	}

	client, err := deps.GetGQLClient(ctx)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to get GitHub client: %w", err)
	}

	var query struct {
		Repository struct {
			Labels struct {
				Nodes []struct {
					ID          githubv4.ID
					Name        githubv4.String
					Color       githubv4.String
					Description githubv4.String
				}
				TotalCount githubv4.Int
				PageInfo   struct {
					HasNextPage githubv4.Boolean
					EndCursor   githubv4.String
				}
			} `graphql:"labels(first: 100, after: $cursor)"`
		} `graphql:"repository(owner: $owner, name: $repo)"`
	}

	vars := map[string]any{
		"owner":  githubv4.String(owner),
		"repo":   githubv4.String(repo),
		"cursor": (*githubv4.String)(nil),
	}

	labels := make([]map[string]any, 0)
	var totalCount int
	hasMore := false
	for page := 1; ; page++ {
		if err := client.Query(ctx, &query, vars); err != nil {
			return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to list labels", err), nil, nil
		}
		for _, labelNode := range query.Repository.Labels.Nodes {
			labels = append(labels, map[string]any{
				"id":          fmt.Sprintf("%v", labelNode.ID),
				"name":        string(labelNode.Name),
				"color":       string(labelNode.Color),
				"description": string(labelNode.Description),
			})
		}
		totalCount = int(query.Repository.Labels.TotalCount)
		if !query.Repository.Labels.PageInfo.HasNextPage {
			break
		}
		if page >= uiGetMaxPages {
			hasMore = true
			break
		}
		vars["cursor"] = githubv4.NewString(query.Repository.Labels.PageInfo.EndCursor)
	}

	response := map[string]any{
		"labels":     labels,
		"totalCount": totalCount,
		"has_more":   hasMore,
	}

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

	return utils.NewToolResultText(string(out)), nil, nil
}

func uiGetAssignees(ctx context.Context, deps ToolDependencies, args map[string]any, owner string) (*mcp.CallToolResult, any, error) {
	repo, err := RequiredParam[string](args, "repo")
	if err != nil {
		return utils.NewToolResultError(err.Error()), nil, nil
	}

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

	opts := &github.ListOptions{PerPage: 100}
	var allAssignees []*github.User
	hasMore := false

	for page := 1; ; page++ {
		assignees, resp, err := client.Issues.ListAssignees(ctx, owner, repo, opts)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list assignees", resp, err), nil, nil
		}
		allAssignees = append(allAssignees, assignees...)
		if resp != nil && resp.Body != nil {
			_ = resp.Body.Close()
		}
		if resp.NextPage == 0 {
			break
		}
		if page >= uiGetMaxPages {
			hasMore = true
			break
		}
		opts.Page = resp.NextPage
	}

	result := make([]map[string]string, len(allAssignees))
	for i, u := range allAssignees {
		result[i] = map[string]string{
			"login":      u.GetLogin(),
			"avatar_url": u.GetAvatarURL(),
		}
	}

	out, err := json.Marshal(map[string]any{
		"assignees":  result,
		"totalCount": len(result),
		"has_more":   hasMore,
	})
	if err != nil {
		return utils.NewToolResultErrorFromErr("failed to marshal assignees", err), nil, nil
	}

	return utils.NewToolResultText(string(out)), nil, nil
}

func uiGetMilestones(ctx context.Context, deps ToolDependencies, args map[string]any, owner string) (*mcp.CallToolResult, any, error) {
	repo, err := RequiredParam[string](args, "repo")
	if err != nil {
		return utils.NewToolResultError(err.Error()), nil, nil
	}

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

	opts := &github.MilestoneListOptions{
		State:       "open",
		ListOptions: github.ListOptions{PerPage: 100},
	}

	var allMilestones []*github.Milestone
	hasMore := false
	for page := 1; ; page++ {
		milestones, resp, err := client.Issues.ListMilestones(ctx, owner, repo, opts)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list milestones", resp, err), nil, nil
		}
		allMilestones = append(allMilestones, milestones...)
		if resp != nil && resp.Body != nil {
			_ = resp.Body.Close()
		}
		if resp.NextPage == 0 {
			break
		}
		if page >= uiGetMaxPages {
			hasMore = true
			break
		}
		opts.Page = resp.NextPage
	}

	result := make([]map[string]any, len(allMilestones))
	for i, m := range allMilestones {
		dueOn := ""
		if m.DueOn != nil {
			dueOn = m.GetDueOn().Format("2006-01-02")
		}
		result[i] = map[string]any{
			"number":      m.GetNumber(),
			"title":       m.GetTitle(),
			"description": m.GetDescription(),
			"state":       m.GetState(),
			"open_issues": m.GetOpenIssues(),
			"due_on":      dueOn,
		}
	}

	out, err := json.Marshal(map[string]any{
		"milestones": result,
		"totalCount": len(result),
		"has_more":   hasMore,
	})
	if err != nil {
		return utils.NewToolResultErrorFromErr("failed to marshal milestones", err), nil, nil
	}

	return utils.NewToolResultText(string(out)), nil, nil
}

func uiGetIssueTypes(ctx context.Context, deps ToolDependencies, owner string) (*mcp.CallToolResult, any, error) {
	client, err := deps.GetClient(ctx)
	if err != nil {
		return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
	}

	issueTypes, resp, err := client.Organizations.ListIssueTypes(ctx, owner)
	if err != nil {
		return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list issue types", resp, err), nil, nil
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return utils.NewToolResultErrorFromErr("failed to read response body", err), nil, nil
		}
		return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list issue types", resp, body), nil, nil
	}

	r, err := json.Marshal(issueTypes)
	if err != nil {
		return utils.NewToolResultErrorFromErr("failed to marshal issue types", err), nil, nil
	}

	return utils.NewToolResultText(string(r)), nil, nil
}

func uiGetBranches(ctx context.Context, deps ToolDependencies, args map[string]any, owner string) (*mcp.CallToolResult, any, error) {
	repo, err := RequiredParam[string](args, "repo")
	if err != nil {
		return utils.NewToolResultError(err.Error()), nil, nil
	}

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

	opts := &github.BranchListOptions{
		ListOptions: github.ListOptions{PerPage: 100},
	}

	var allBranches []*github.Branch
	hasMore := false
	for page := 1; ; page++ {
		branches, resp, err := client.Repositories.ListBranches(ctx, owner, repo, opts)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list branches", resp, err), nil, nil
		}
		allBranches = append(allBranches, branches...)
		if resp != nil && resp.Body != nil {
			_ = resp.Body.Close()
		}
		if resp.NextPage == 0 {
			break
		}
		if page >= uiGetMaxPages {
			hasMore = true
			break
		}
		opts.Page = resp.NextPage
	}

	minimalBranches := make([]MinimalBranch, 0, len(allBranches))
	for _, branch := range allBranches {
		minimalBranches = append(minimalBranches, convertToMinimalBranch(branch))
	}

	r, err := json.Marshal(map[string]any{
		"branches":   minimalBranches,
		"totalCount": len(minimalBranches),
		"has_more":   hasMore,
	})
	if err != nil {
		return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
	}

	return utils.NewToolResultText(string(r)), nil, nil
}

func uiGetIssueFields(ctx context.Context, deps ToolDependencies, args map[string]any, owner string) (*mcp.CallToolResult, any, error) {
	repo, err := RequiredParam[string](args, "repo")
	if err != nil {
		return utils.NewToolResultError(err.Error()), nil, nil
	}

	gqlClient, err := deps.GetGQLClient(ctx)
	if err != nil {
		return utils.NewToolResultErrorFromErr("failed to get GitHub GraphQL client", err), nil, nil
	}

	fields, err := fetchIssueFields(ctx, gqlClient, owner, repo)
	if err != nil {
		return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to list issue fields", err), nil, nil
	}

	return marshalUIGetIssueFields(fields)
}

func marshalUIGetIssueFields(fields []IssueField) (*mcp.CallToolResult, any, error) {
	resultFields := make([]map[string]any, 0, len(fields))
	for _, field := range fields {
		if !uiSupportedIssueFieldDataType(field.DataType) {
			continue
		}

		fieldResult := map[string]any{
			"id":          field.ID,
			"name":        field.Name,
			"data_type":   field.DataType,
			"description": field.Description,
		}

		if field.DataType == "single_select" {
			fieldOptions := append([]IssueSingleSelectFieldOption(nil), field.Options...)
			sort.SliceStable(fieldOptions, func(i, j int) bool {
				left, leftOK := issueFieldOptionPriority(fieldOptions[i])
				right, rightOK := issueFieldOptionPriority(fieldOptions[j])
				if leftOK != rightOK {
					return leftOK
				}
				return left < right
			})

			options := make([]map[string]string, 0, len(fieldOptions))
			for _, option := range fieldOptions {
				options = append(options, map[string]string{
					"name":        option.Name,
					"description": option.Description,
					"color":       option.Color,
				})
			}
			fieldResult["options"] = options
		}

		resultFields = append(resultFields, fieldResult)
	}

	r, err := json.Marshal(map[string]any{
		"fields":     resultFields,
		"totalCount": len(resultFields),
	})
	if err != nil {
		return utils.NewToolResultErrorFromErr("failed to marshal issue fields", err), nil, nil
	}

	return utils.NewToolResultText(string(r)), nil, nil
}

func uiSupportedIssueFieldDataType(dataType string) bool {
	switch dataType {
	case "text", "number", "date", "single_select":
		return true
	default:
		return false
	}
}

func issueFieldOptionPriority(option IssueSingleSelectFieldOption) (int, bool) {
	if option.Priority == nil {
		return 0, false
	}
	return *option.Priority, true
}

func uiGetReviewers(ctx context.Context, deps ToolDependencies, args map[string]any, owner string) (*mcp.CallToolResult, any, error) {
	repo, err := RequiredParam[string](args, "repo")
	if err != nil {
		return utils.NewToolResultError(err.Error()), nil, nil
	}

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

	collaboratorOpts := &github.ListCollaboratorsOptions{
		Affiliation: "all",
		ListOptions: github.ListOptions{PerPage: 100},
	}
	var allCollaborators []*github.User
	hasMore := false
	for page := 1; ; page++ {
		collaborators, resp, err := client.Repositories.ListCollaborators(ctx, owner, repo, collaboratorOpts)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list reviewers", resp, err), nil, nil
		}
		allCollaborators = append(allCollaborators, collaborators...)
		if resp != nil && resp.Body != nil {
			_ = resp.Body.Close()
		}
		if resp.NextPage == 0 {
			break
		}
		if page >= uiGetMaxPages {
			hasMore = true
			break
		}
		collaboratorOpts.Page = resp.NextPage
	}

	teamOpts := &github.ListOptions{PerPage: 100}
	var allTeams []*github.Team
	for page := 1; ; page++ {
		teams, resp, err := client.Repositories.ListTeams(ctx, owner, repo, teamOpts)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list reviewer teams", resp, err), nil, nil
		}
		allTeams = append(allTeams, teams...)
		if resp != nil && resp.Body != nil {
			_ = resp.Body.Close()
		}
		if resp.NextPage == 0 {
			break
		}
		if page >= uiGetMaxPages {
			hasMore = true
			break
		}
		teamOpts.Page = resp.NextPage
	}

	users := make([]map[string]string, 0, len(allCollaborators))
	for _, user := range allCollaborators {
		login := user.GetLogin()
		if user.GetType() == "Bot" || strings.HasSuffix(login, "[bot]") {
			continue
		}
		users = append(users, map[string]string{
			"login":      login,
			"avatar_url": user.GetAvatarURL(),
		})
	}

	teams := make([]map[string]string, len(allTeams))
	for i, team := range allTeams {
		teams[i] = map[string]string{
			"slug": team.GetSlug(),
			"name": team.GetName(),
			"org":  owner,
		}
	}

	r, err := json.Marshal(map[string]any{
		"users":      users,
		"teams":      teams,
		"totalCount": len(users) + len(teams),
		"has_more":   hasMore,
	})
	if err != nil {
		return utils.NewToolResultErrorFromErr("failed to marshal reviewers", err), nil, nil
	}

	return utils.NewToolResultText(string(r)), nil, nil
}
