package github

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"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"
)

// IssueDependencyRead creates a tool to read an issue's blocked-by and blocking
// relationships. It is a separate, feature-flagged tool (rather than a method on
// the default issue_read) so the whole dependency capability can be gated as a
// unit without enlarging the default issue tool surface.
func IssueDependencyRead(t translations.TranslationHelperFunc) inventory.ServerTool {
	schema := &jsonschema.Schema{
		Type: "object",
		Properties: map[string]*jsonschema.Schema{
			"method": {
				Type: "string",
				Description: `The read operation to perform on a single issue's dependencies.
Options are:
1. get_blocked_by - List the issues that block this issue (this issue is blocked by them).
2. get_blocking - List the issues that this issue blocks.
`,
				Enum: []any{"get_blocked_by", "get_blocking"},
			},
			"owner": {
				Type:        "string",
				Description: "The owner of the repository",
			},
			"repo": {
				Type:        "string",
				Description: "The name of the repository",
			},
			"issue_number": {
				Type:        "number",
				Description: "The number of the issue",
			},
		},
		Required: []string{"method", "owner", "repo", "issue_number"},
	}
	WithPagination(schema)

	st := NewTool(
		ToolsetMetadataIssues,
		mcp.Tool{
			Name:        "issue_dependency_read",
			Description: t("TOOL_ISSUE_DEPENDENCY_READ_DESCRIPTION", "Read an issue's dependency relationships in a GitHub repository: the issues that block it (blocked_by) or the issues it blocks (blocking)."),
			Annotations: &mcp.ToolAnnotations{
				Title:        t("TOOL_ISSUE_DEPENDENCY_READ_USER_TITLE", "Read issue dependencies"),
				ReadOnlyHint: true,
			},
			InputSchema: schema,
		},
		[]scopes.Scope{scopes.Repo},
		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
			}
			repo, err := RequiredParam[string](args, "repo")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			issueNumber, err := RequiredInt(args, "issue_number")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}

			pagination, err := OptionalPaginationParams(args)
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			opts := &github.ListOptions{Page: pagination.Page, PerPage: pagination.PerPage}

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

			switch method {
			case "get_blocked_by":
				result, err := GetIssueBlockedBy(ctx, client, owner, repo, issueNumber, opts)
				return result, nil, err
			case "get_blocking":
				result, err := GetIssueBlocking(ctx, client, owner, repo, issueNumber, opts)
				return result, nil, err
			default:
				return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
			}
		})
	st.FeatureFlagEnable = FeatureFlagIssueDependencies
	return st
}

// GetIssueBlockedBy lists the issues that block the given issue.
func GetIssueBlockedBy(ctx context.Context, client *github.Client, owner, repo string, issueNumber int, opts *github.ListOptions) (*mcp.CallToolResult, error) {
	issues, resp, err := client.Issues.ListBlockedBy(ctx, owner, repo, int64(issueNumber), opts)
	if err != nil {
		return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list blocked-by issues", resp, err), nil
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return nil, fmt.Errorf("failed to read response body: %w", err)
		}
		return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list blocked-by issues", resp, body), nil
	}
	return dependencyReadResult(issues, resp), nil
}

// GetIssueBlocking lists the issues that the given issue blocks.
func GetIssueBlocking(ctx context.Context, client *github.Client, owner, repo string, issueNumber int, opts *github.ListOptions) (*mcp.CallToolResult, error) {
	issues, resp, err := client.Issues.ListBlocking(ctx, owner, repo, int64(issueNumber), opts)
	if err != nil {
		return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list blocking issues", resp, err), nil
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode != http.StatusOK {
		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return nil, fmt.Errorf("failed to read response body: %w", err)
		}
		return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list blocking issues", resp, body), nil
	}
	return dependencyReadResult(issues, resp), nil
}

// dependencyReadResult projects a list of related issues into the minimal
// dependency shape and attaches page-based pagination info.
func dependencyReadResult(issues []*github.Issue, resp *github.Response) *mcp.CallToolResult {
	refs := make([]MinimalIssueRef, 0, len(issues))
	for _, issue := range issues {
		if issue == nil {
			continue
		}
		refs = append(refs, issueToDependencyRef(issue))
	}
	return MarshalledTextResult(map[string]any{
		"issues": refs,
		"pageInfo": map[string]any{
			"hasNextPage": resp.NextPage != 0,
			"nextPage":    resp.NextPage,
		},
	})
}

// issueToDependencyRef converts a REST issue into the compact reference used by
// the dependency tools, deriving the "owner/repo" name from the issue's
// repository URL. The state is upper-cased so it matches the GraphQL-sourced
// state (e.g. "OPEN"/"CLOSED") that MinimalIssueRef carries for the other issue
// tools such as get_parent, keeping the field consistent across tools.
func issueToDependencyRef(issue *github.Issue) MinimalIssueRef {
	if issue == nil {
		return MinimalIssueRef{}
	}
	ref := MinimalIssueRef{
		Number: issue.GetNumber(),
		Title:  issue.GetTitle(),
		State:  strings.ToUpper(issue.GetState()),
		URL:    issue.GetHTMLURL(),
	}
	if owner, repo, ok := parseRepositoryURL(issue.GetRepositoryURL()); ok {
		ref.Repository = owner + "/" + repo
	}
	return ref
}

// IssueDependencyWrite creates a tool to add or remove an issue dependency
// (blocked-by / blocking) relationship. The REST dependency endpoints are always
// expressed as "the blocked issue is blocked_by the blocking issue", so both
// directions are served by the same endpoint pair with the two issues swapped.
func IssueDependencyWrite(t translations.TranslationHelperFunc) inventory.ServerTool {
	st := NewTool(
		ToolsetMetadataIssues,
		mcp.Tool{
			Name: "issue_dependency_write",
			Description: t("TOOL_ISSUE_DEPENDENCY_WRITE_DESCRIPTION",
				"Add or remove an issue dependency relationship in a GitHub repository. "+
					"Use type 'blocked_by' to record that the subject issue is blocked by a related issue, "+
					"or type 'blocking' to record that the subject issue blocks a related issue. "+
					"The related issue defaults to the same repository as the subject unless related_owner/related_repo are provided."),
			Annotations: &mcp.ToolAnnotations{
				Title:        t("TOOL_ISSUE_DEPENDENCY_WRITE_USER_TITLE", "Change issue dependency"),
				ReadOnlyHint: false,
			},
			InputSchema: &jsonschema.Schema{
				Type: "object",
				Properties: map[string]*jsonschema.Schema{
					"method": {
						Type: "string",
						Description: `The action to perform.
Options are:
- 'add' - create the dependency relationship.
- 'remove' - delete the dependency relationship.`,
						Enum: []any{"add", "remove"},
					},
					"type": {
						Type: "string",
						Description: `The relationship direction relative to the subject issue.
Options are:
- 'blocked_by' - the subject issue is blocked by the related issue.
- 'blocking' - the subject issue blocks the related issue.`,
						Enum: []any{"blocked_by", "blocking"},
					},
					"owner": {
						Type:        "string",
						Description: "The owner of the subject issue's repository",
					},
					"repo": {
						Type:        "string",
						Description: "The name of the subject issue's repository",
					},
					"issue_number": {
						Type:        "number",
						Description: "The number of the subject issue",
					},
					"related_issue_number": {
						Type:        "number",
						Description: "The number of the related issue to link or unlink",
					},
					"related_owner": {
						Type:        "string",
						Description: "The owner of the related issue's repository. Defaults to 'owner' when omitted.",
					},
					"related_repo": {
						Type:        "string",
						Description: "The name of the related issue's repository. Defaults to 'repo' when omitted.",
					},
				},
				Required: []string{"method", "type", "owner", "repo", "issue_number", "related_issue_number"},
			},
		},
		[]scopes.Scope{scopes.Repo},
		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
			}
			relationshipType, err := RequiredParam[string](args, "type")
			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
			}
			repo, err := RequiredParam[string](args, "repo")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			issueNumber, err := RequiredInt(args, "issue_number")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			relatedIssueNumber, err := RequiredInt(args, "related_issue_number")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			relatedOwner, err := OptionalParam[string](args, "related_owner")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			relatedRepo, err := OptionalParam[string](args, "related_repo")
			if err != nil {
				return utils.NewToolResultError(err.Error()), nil, nil
			}
			if relatedOwner == "" {
				relatedOwner = owner
			}
			if relatedRepo == "" {
				relatedRepo = repo
			}

			method = strings.ToLower(method)
			relationshipType = strings.ToLower(relationshipType)
			if method != "add" && method != "remove" {
				return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
			}
			if relationshipType != "blocked_by" && relationshipType != "blocking" {
				return utils.NewToolResultError(fmt.Sprintf("unknown type: %s", relationshipType)), nil, nil
			}

			if owner == relatedOwner && repo == relatedRepo && issueNumber == relatedIssueNumber {
				return utils.NewToolResultError("an issue cannot block or depend on itself"), nil, nil
			}

			// Map the subject/related pair onto the blocked/blocking roles the REST
			// endpoints expect. For type 'blocked_by' the subject is the blocked
			// issue; for 'blocking' the subject blocks the related issue, so the
			// roles swap.
			blocked := issueCoordinate{owner: owner, repo: repo, number: issueNumber}
			blocking := issueCoordinate{owner: relatedOwner, repo: relatedRepo, number: relatedIssueNumber}
			if relationshipType == "blocking" {
				blocked, blocking = blocking, blocked
			}

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

			result, err := writeIssueDependency(ctx, client, method, blocked, blocking)
			return result, nil, err
		})
	st.FeatureFlagEnable = FeatureFlagIssueDependencies
	return st
}

// issueCoordinate identifies an issue by repository and number.
type issueCoordinate struct {
	owner  string
	repo   string
	number int
}

// writeIssueDependency resolves the blocking issue to its global database ID and
// then adds or removes the blocked-by relationship on the blocked issue.
func writeIssueDependency(ctx context.Context, client *github.Client, method string, blocked, blocking issueCoordinate) (*mcp.CallToolResult, error) {
	// The REST API identifies the blocking issue by its global database ID
	// (not its number), so resolve the number to an ID first.
	blockingIssue, resp, err := client.Issues.Get(ctx, blocking.owner, blocking.repo, blocking.number)
	if err != nil {
		return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to resolve blocking issue", resp, err), nil
	}
	_ = resp.Body.Close()
	blockingID := blockingIssue.GetID()

	switch method {
	case "add":
		blockedIssue, opResp, err := client.Issues.AddBlockedBy(ctx, blocked.owner, blocked.repo, int64(blocked.number), github.IssueDependencyRequest{IssueID: blockingID})
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to add issue dependency", opResp, err), nil
		}
		defer func() { _ = opResp.Body.Close() }()
		if opResp.StatusCode != http.StatusCreated {
			body, readErr := io.ReadAll(opResp.Body)
			if readErr != nil {
				return nil, fmt.Errorf("failed to read response body: %w", readErr)
			}
			return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to add issue dependency", opResp, body), nil
		}
		return dependencyWriteResult("dependency added", blockedIssue, blockingIssue, blocked, blocking), nil
	case "remove":
		blockedIssue, opResp, err := client.Issues.RemoveBlockedBy(ctx, blocked.owner, blocked.repo, int64(blocked.number), blockingID)
		if err != nil {
			return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to remove issue dependency", opResp, err), nil
		}
		defer func() { _ = opResp.Body.Close() }()
		if opResp.StatusCode != http.StatusOK {
			body, readErr := io.ReadAll(opResp.Body)
			if readErr != nil {
				return nil, fmt.Errorf("failed to read response body: %w", readErr)
			}
			return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to remove issue dependency", opResp, body), nil
		}
		return dependencyWriteResult("dependency removed", blockedIssue, blockingIssue, blocked, blocking), nil
	default:
		return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil
	}
}

// dependencyWriteResult builds the minimal description of the affected issues.
// The blocked issue comes from the mutation response and the blocking issue from
// the earlier resolve; each falls back to its known coordinate when the API
// response omits the repository URL.
func dependencyWriteResult(message string, blockedIssue, blockingIssue *github.Issue, blocked, blocking issueCoordinate) *mcp.CallToolResult {
	blockedRef := issueToDependencyRef(blockedIssue)
	if blockedRef.Repository == "" {
		blockedRef.Repository = blocked.owner + "/" + blocked.repo
	}
	blockingRef := issueToDependencyRef(blockingIssue)
	if blockingRef.Repository == "" {
		blockingRef.Repository = blocking.owner + "/" + blocking.repo
	}
	return MarshalledTextResult(map[string]any{
		"message":        message,
		"blocked_issue":  blockedRef,
		"blocking_issue": blockingRef,
	})
}
