package github

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

	ghcontext "github.com/github/github-mcp-server/pkg/context"
	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/jsonschema-go/jsonschema"
	"github.com/modelcontextprotocol/go-sdk/mcp"
	"github.com/shurcooL/githubv4"
)

// IssueField represents a repository issue field definition.
type IssueField struct {
	ID          string                         `json:"id"`
	DatabaseID  int64                          `json:"full_database_id,omitempty"`
	Name        string                         `json:"name"`
	Description string                         `json:"description,omitempty"`
	DataType    string                         `json:"data_type"`
	Visibility  string                         `json:"visibility"`
	Options     []IssueSingleSelectFieldOption `json:"options,omitempty"`
}

// IssueSingleSelectFieldOption represents an option for a single_select issue field.
type IssueSingleSelectFieldOption struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	Color       string `json:"color"`
	Priority    *int   `json:"priority,omitempty"`
}

// issueFieldNode is the GraphQL fragment for a single issue field in the IssueFields union.
// Only the fragment matching __typename is populated; read from the matching fragment.
// fullDatabaseId (BigInt scalar, returned as string) is fetched on each concrete type because
// shurcooL/githubv4 does not support interface fragments at the top level of a union.
type issueFieldNode struct {
	TypeName       githubv4.String `graphql:"__typename"`
	IssueFieldText struct {
		ID             githubv4.ID
		FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
		Name           githubv4.String
		Description    githubv4.String
		DataType       githubv4.String
		Visibility     githubv4.String
	} `graphql:"... on IssueFieldText"`
	IssueFieldNumber struct {
		ID             githubv4.ID
		FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
		Name           githubv4.String
		Description    githubv4.String
		DataType       githubv4.String
		Visibility     githubv4.String
	} `graphql:"... on IssueFieldNumber"`
	IssueFieldDate struct {
		ID             githubv4.ID
		FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
		Name           githubv4.String
		Description    githubv4.String
		DataType       githubv4.String
		Visibility     githubv4.String
	} `graphql:"... on IssueFieldDate"`
	IssueFieldSingleSelect struct {
		ID             githubv4.ID
		FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
		Name           githubv4.String
		Description    githubv4.String
		DataType       githubv4.String
		Visibility     githubv4.String
		Options        []struct {
			ID          githubv4.ID
			Name        githubv4.String
			Description githubv4.String
			Color       githubv4.String
			Priority    *int
		}
	} `graphql:"... on IssueFieldSingleSelect"`
}

// issueFieldsRepoQuery is the GraphQL query for listing issue fields on a repository.
type issueFieldsRepoQuery struct {
	Repository struct {
		IssueFields struct {
			Nodes []issueFieldNode
		} `graphql:"issueFields(first: 100)"`
	} `graphql:"repository(owner: $owner, name: $name)"`
}

// issueFieldsOrgQuery is the GraphQL query for listing issue fields on an organization.
type issueFieldsOrgQuery struct {
	Organization struct {
		IssueFields struct {
			Nodes []issueFieldNode
		} `graphql:"issueFields(first: 100)"`
	} `graphql:"organization(login: $login)"`
}

// ListIssueFields creates a tool to list issue field definitions for a repository or organization.
func ListIssueFields(t translations.TranslationHelperFunc) inventory.ServerTool {
	st := NewTool(
		ToolsetMetadataIssues,
		mcp.Tool{
			Name:        "list_issue_fields",
			Description: t("TOOL_LIST_ISSUE_FIELDS_DESCRIPTION", "List issue fields for a repository or organization. Returns field definitions including name, type (text, number, date, single_select), and for single_select fields the list of valid option names. When repo is omitted, returns org-level fields directly."),
			Annotations: &mcp.ToolAnnotations{
				Title:        t("TOOL_LIST_ISSUE_FIELDS_USER_TITLE", "List issue fields"),
				ReadOnlyHint: true,
			},
			InputSchema: &jsonschema.Schema{
				Type: "object",
				Properties: map[string]*jsonschema.Schema{
					"owner": {
						Type:        "string",
						Description: "The account owner of the repository or organization. The name is not case sensitive.",
					},
					"repo": {
						Type:        "string",
						Description: "The name of the repository. When provided, returns fields for this specific repository (inherited from its organization). When omitted, returns org-level fields directly.",
					},
				},
				Required: []string{"owner"},
			},
		},
		[]scopes.Scope{scopes.Repo, scopes.ReadOrg},
		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 := OptionalParam[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
			}

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

			result := utils.NewToolResultText(string(r))
			// Issue field definitions are repo/org structural metadata
			// (trusted). When scoped to a specific repo, confidentiality
			// follows that repo's visibility; for an org-level lookup (no
			// repo) it is conservatively treated as private.
			if repo == "" {
				result = attachStaticIFCLabel(ctx, deps, result, ifc.LabelRepoMetadata(true))
			} else {
				result = attachRepoVisibilityIFCLabelLazy(ctx, deps, owner, repo, result, ifc.LabelRepoMetadata)
			}
			return result, nil, nil
		})
	return st
}

// fetchIssueFields returns the issue field definitions for the given owner.
// If repo is provided, fields are scoped to that repository (inherited from its
// organization); otherwise fields are returned directly from the organization.
func fetchIssueFields(ctx context.Context, gqlClient *githubv4.Client, owner, repo string) ([]IssueField, error) {
	ctxWithFeatures := ghcontext.WithGraphQLFeatures(ctx, "issue_fields", "repo_issue_fields")
	if repo != "" {
		var query issueFieldsRepoQuery
		vars := map[string]any{
			"owner": githubv4.String(owner),
			"name":  githubv4.String(repo),
		}
		if err := gqlClient.Query(ctxWithFeatures, &query, vars); err != nil {
			return nil, err
		}
		return issueFieldsFromNodes(query.Repository.IssueFields.Nodes), nil
	}

	var query issueFieldsOrgQuery
	vars := map[string]any{
		"login": githubv4.String(owner),
	}
	if err := gqlClient.Query(ctxWithFeatures, &query, vars); err != nil {
		return nil, err
	}
	return issueFieldsFromNodes(query.Organization.IssueFields.Nodes), nil
}

// issueFieldsFromNodes converts GraphQL issue field union nodes into IssueField values.
// Read from the fragment matching __typename; the other fragments are zero-valued.
func issueFieldsFromNodes(nodes []issueFieldNode) []IssueField {
	fields := make([]IssueField, 0, len(nodes))
	for _, node := range nodes {
		var f IssueField
		switch string(node.TypeName) {
		case "IssueFieldSingleSelect":
			opts := make([]IssueSingleSelectFieldOption, 0, len(node.IssueFieldSingleSelect.Options))
			for _, o := range node.IssueFieldSingleSelect.Options {
				opts = append(opts, IssueSingleSelectFieldOption{
					ID:          fmt.Sprintf("%v", o.ID),
					Name:        string(o.Name),
					Description: string(o.Description),
					Color:       string(o.Color),
					Priority:    o.Priority,
				})
			}
			f = IssueField{
				ID:          fmt.Sprintf("%v", node.IssueFieldSingleSelect.ID),
				DatabaseID:  parseFullDatabaseID(string(node.IssueFieldSingleSelect.FullDatabaseID)),
				Name:        string(node.IssueFieldSingleSelect.Name),
				Description: string(node.IssueFieldSingleSelect.Description),
				DataType:    string(node.IssueFieldSingleSelect.DataType),
				Visibility:  string(node.IssueFieldSingleSelect.Visibility),
				Options:     opts,
			}
		case "IssueFieldText":
			f = IssueField{
				ID:          fmt.Sprintf("%v", node.IssueFieldText.ID),
				DatabaseID:  parseFullDatabaseID(string(node.IssueFieldText.FullDatabaseID)),
				Name:        string(node.IssueFieldText.Name),
				Description: string(node.IssueFieldText.Description),
				DataType:    string(node.IssueFieldText.DataType),
				Visibility:  string(node.IssueFieldText.Visibility),
			}
		case "IssueFieldNumber":
			f = IssueField{
				ID:          fmt.Sprintf("%v", node.IssueFieldNumber.ID),
				DatabaseID:  parseFullDatabaseID(string(node.IssueFieldNumber.FullDatabaseID)),
				Name:        string(node.IssueFieldNumber.Name),
				Description: string(node.IssueFieldNumber.Description),
				DataType:    string(node.IssueFieldNumber.DataType),
				Visibility:  string(node.IssueFieldNumber.Visibility),
			}
		case "IssueFieldDate":
			f = IssueField{
				ID:          fmt.Sprintf("%v", node.IssueFieldDate.ID),
				DatabaseID:  parseFullDatabaseID(string(node.IssueFieldDate.FullDatabaseID)),
				Name:        string(node.IssueFieldDate.Name),
				Description: string(node.IssueFieldDate.Description),
				DataType:    string(node.IssueFieldDate.DataType),
				Visibility:  string(node.IssueFieldDate.Visibility),
			}
		default:
			continue
		}
		fields = append(fields, f)
	}
	return fields
}

// parseFullDatabaseID converts a BigInt scalar string (e.g. "12345") to int64.
// Returns 0 if the string is empty or cannot be parsed.
func parseFullDatabaseID(s string) int64 {
	if s == "" {
		return 0
	}
	n, err := strconv.ParseInt(s, 10, 64)
	if err != nil {
		return 0
	}
	return n
}
