package github

import (
	"context"

	"github.com/github/github-mcp-server/pkg/ifc"
	"github.com/google/go-github/v89/github"
	"github.com/modelcontextprotocol/go-sdk/mcp"
)

// setIFCLabel writes the given IFC security label into a tool result's _meta
// under the "ifc" key, allocating the Meta map if necessary.
func setIFCLabel(r *mcp.CallToolResult, label ifc.SecurityLabel) {
	if r.Meta == nil {
		r.Meta = mcp.Meta{}
	}
	r.Meta["ifc"] = label
}

func shouldAttachIFCLabel(ctx context.Context, deps ToolDependencies, r *mcp.CallToolResult) bool {
	return r != nil && !r.IsError && deps.IsFeatureEnabled(ctx, FeatureFlagIFCLabels)
}

// attachStaticIFCLabel attaches a fixed IFC label to a successful tool result
// when IFC labels are enabled. It is used by tools whose label does not depend
// on any repository visibility lookup (e.g. security alerts, global
// advisories, team membership, notification subjects).
//
// Error results are left untouched, and the label is omitted entirely when the
// IFC feature flag is disabled.
func attachStaticIFCLabel(ctx context.Context, deps ToolDependencies, r *mcp.CallToolResult, label ifc.SecurityLabel) *mcp.CallToolResult {
	if !shouldAttachIFCLabel(ctx, deps, r) {
		return r
	}
	setIFCLabel(r, label)
	return r
}

// attachRepoVisibilityIFCLabel attaches an IFC label derived from a single
// repository's visibility to a successful tool result when IFC labels are
// enabled. The concrete label is produced by labelFn, which receives whether
// the repository is private.
//
// The repository visibility is resolved via FetchRepoIsPrivate. Consistent
// with the other IFC-labeled tools, if the visibility lookup fails the label
// is omitted rather than risking a misclassification. Error results and the
// disabled-feature case are left untouched.
func attachRepoVisibilityIFCLabel(
	ctx context.Context,
	deps ToolDependencies,
	client *github.Client,
	owner, repo string,
	r *mcp.CallToolResult,
	labelFn func(isPrivate bool) ifc.SecurityLabel,
) *mcp.CallToolResult {
	if !shouldAttachIFCLabel(ctx, deps, r) {
		return r
	}
	isPrivate, err := FetchRepoIsPrivate(ctx, client, owner, repo)
	if err != nil {
		return r
	}
	setIFCLabel(r, labelFn(isPrivate))
	return r
}

// ifcSearchPostProcessOption returns a searchOption that attaches IFC labels to
// a multi-repository search result. The feature-flag check is centralized here
// (mirroring the attach* helpers above) rather than in each search tool
// handler: when IFC labels are disabled it returns a no-op option, so callers
// can pass it unconditionally to searchHandler.
func ifcSearchPostProcessOption(ctx context.Context, deps ToolDependencies) searchOption {
	if !deps.IsFeatureEnabled(ctx, FeatureFlagIFCLabels) {
		return func(*searchConfig) {}
	}
	return withSearchPostProcess(searchIssuesIFCPostProcess(deps))
}

// attachRepoVisibilityIFCLabelLazy is like attachRepoVisibilityIFCLabel but
// resolves the REST client itself, only when IFC labels are enabled. It is used
// by tools whose handler holds a GraphQL client (or no client yet) and would
// otherwise have to acquire a REST client solely to compute the label. The
// feature-flag check is centralized here so callers can invoke it
// unconditionally; if the client cannot be obtained or the visibility lookup
// fails, the label is omitted rather than risking a misclassification.
func attachRepoVisibilityIFCLabelLazy(
	ctx context.Context,
	deps ToolDependencies,
	owner, repo string,
	r *mcp.CallToolResult,
	labelFn func(isPrivate bool) ifc.SecurityLabel,
) *mcp.CallToolResult {
	if !shouldAttachIFCLabel(ctx, deps, r) {
		return r
	}
	client, err := deps.GetClient(ctx)
	if err != nil {
		return r
	}
	return attachRepoVisibilityIFCLabel(ctx, deps, client, owner, repo, r, labelFn)
}

// attachJoinedIFCLabel attaches an IFC label computed by joining a set of
// per-item visibilities (true == private) when IFC labels are enabled. joinFn
// is the lattice join for the relevant item kind (e.g. ifc.LabelSearchIssues or
// ifc.LabelProjectList). The visibility slice is cheap to build from an
// already-fetched response, so callers may construct it unconditionally and let
// this helper own the feature-flag gate.
func attachJoinedIFCLabel(
	ctx context.Context,
	deps ToolDependencies,
	r *mcp.CallToolResult,
	visibilities []bool,
	joinFn func([]bool) ifc.SecurityLabel,
) *mcp.CallToolResult {
	if !shouldAttachIFCLabel(ctx, deps, r) {
		return r
	}
	setIFCLabel(r, joinFn(visibilities))
	return r
}

func attachProjectVisibilityIFCLabel(
	ctx context.Context,
	deps ToolDependencies,
	r *mcp.CallToolResult,
	isPrivate bool,
	labelFn func(isPrivate bool) ifc.SecurityLabel,
) *mcp.CallToolResult {
	if !shouldAttachIFCLabel(ctx, deps, r) {
		return r
	}
	setIFCLabel(r, labelFn(isPrivate))
	return r
}

// newRepoVisibilityIFCLabeler returns a closure that attaches a repo-visibility
// IFC label to a tool result, for handlers that have several return paths and
// want to label each one. The returned function owns the feature-flag gate (so
// callers invoke it unconditionally) and caches the repository visibility
// lookup across calls, so a handler that returns from many branches only pays
// for one FetchRepoIsPrivate call. A failed visibility lookup is not cached, so
// a later return path can retry; on persistent failure the label is omitted
// rather than risking a misclassification.
func newRepoVisibilityIFCLabeler(
	ctx context.Context,
	deps ToolDependencies,
	client *github.Client,
	owner, repo string,
	labelFn func(isPrivate bool) ifc.SecurityLabel,
) func(*mcp.CallToolResult) *mcp.CallToolResult {
	var (
		known     bool
		isPrivate bool
	)
	return func(r *mcp.CallToolResult) *mcp.CallToolResult {
		if r == nil || r.IsError || !deps.IsFeatureEnabled(ctx, FeatureFlagIFCLabels) {
			return r
		}
		if !known {
			p, err := FetchRepoIsPrivate(ctx, client, owner, repo)
			if err != nil {
				return r
			}
			isPrivate = p
			known = true
		}
		setIFCLabel(r, labelFn(isPrivate))
		return r
	}
}
