package inventory

import (
	"context"
	"errors"
	"fmt"
	"maps"
	"slices"
	"strings"
)

var (
	// ErrUnknownTools is returned when tools specified via WithTools() are not recognized.
	ErrUnknownTools = errors.New("unknown tools specified in WithTools")
)

// mcpAppsFeatureFlag is the feature flag name that controls MCP Apps UI metadata.
// This is defined here to avoid importing pkg/github (which imports pkg/inventory).
// The value must match github.MCPAppsFeatureFlag.
const mcpAppsFeatureFlag = "remote_mcp_ui_apps"

// ToolFilter is a function that determines if a tool should be included.
// Returns true if the tool should be included, false to exclude it.
type ToolFilter func(ctx context.Context, tool *ServerTool) (bool, error)

// Builder builds a Registry with the specified configuration.
// Use NewBuilder to create a builder, chain configuration methods,
// then call Build() to create the final inventory.
//
// Example:
//
//	reg := NewBuilder().
//	    SetTools(tools).
//	    SetResources(resources).
//	    SetPrompts(prompts).
//	    WithDeprecatedAliases(aliases).
//	    WithReadOnly(true).
//	    WithToolsets([]string{"repos", "issues"}).
//	    WithFeatureChecker(checker).
//	    WithFilter(myFilter).
//	    Build()
type Builder struct {
	tools             []ServerTool
	resourceTemplates []ServerResourceTemplate
	prompts           []ServerPrompt
	deprecatedAliases map[string]string

	// Configuration options (processed at Build time)
	readOnly             bool
	toolsetIDs           []string // raw input, processed at Build()
	toolsetIDsIsNil      bool     // tracks if nil was passed (nil = defaults)
	additionalTools      []string // raw input, processed at Build()
	featureChecker       FeatureFlagChecker
	filters              []ToolFilter // filters to apply to all tools
	generateInstructions bool
}

// NewBuilder creates a new Builder.
func NewBuilder() *Builder {
	return &Builder{
		deprecatedAliases: make(map[string]string),
		toolsetIDsIsNil:   true, // default to nil (use defaults)
	}
}

// SetTools sets the tools for the inventory. Returns self for chaining.
func (b *Builder) SetTools(tools []ServerTool) *Builder {
	b.tools = tools
	return b
}

// SetResources sets the resource templates for the inventory. Returns self for chaining.
func (b *Builder) SetResources(resources []ServerResourceTemplate) *Builder {
	b.resourceTemplates = resources
	return b
}

// SetPrompts sets the prompts for the inventory. Returns self for chaining.
func (b *Builder) SetPrompts(prompts []ServerPrompt) *Builder {
	b.prompts = prompts
	return b
}

// WithDeprecatedAliases adds deprecated tool name aliases that map to canonical names.
// Returns self for chaining.
func (b *Builder) WithDeprecatedAliases(aliases map[string]string) *Builder {
	maps.Copy(b.deprecatedAliases, aliases)
	return b
}

// WithReadOnly sets whether only read-only tools should be available.
// When true, write tools are filtered out. Returns self for chaining.
func (b *Builder) WithReadOnly(readOnly bool) *Builder {
	b.readOnly = readOnly
	return b
}

func (b *Builder) WithServerInstructions() *Builder {
	b.generateInstructions = true
	return b
}

// WithToolsets specifies which toolsets should be enabled.
// Special keywords:
//   - "all": enables all toolsets
//   - "default": expands to toolsets marked with Default: true in their metadata
//
// Input strings are trimmed of whitespace and duplicates are removed.
// Pass nil to use default toolsets. Pass an empty slice to disable all toolsets.
// Returns self for chaining.
func (b *Builder) WithToolsets(toolsetIDs []string) *Builder {
	b.toolsetIDs = toolsetIDs
	b.toolsetIDsIsNil = toolsetIDs == nil
	return b
}

// WithTools specifies additional tools that bypass toolset filtering.
// These tools are additive - they will be included even if their toolset is not enabled.
// Read-only filtering still applies to these tools.
// Input is cleaned (trimmed, deduplicated) during Build().
// Deprecated tool aliases are automatically resolved to their canonical names during Build().
// Returns self for chaining.
func (b *Builder) WithTools(toolNames []string) *Builder {
	b.additionalTools = toolNames
	return b
}

// WithFeatureChecker sets the feature flag checker function.
// The checker receives a context (for actor extraction) and feature flag name,
// and returns (enabled, error). Errors are logged and treated as "not enabled".
//
// When the checker is non-nil, Build() installs a feature-flag ToolFilter
// at the head of the filter pipeline so that tools annotated with
// FeatureFlagEnable / FeatureFlagDisable are gated accordingly. Resources
// and prompts use the same checker via an explicit guard at their iteration
// site.
//
// When the checker is nil, no feature-flag filter is installed; tools,
// resources, and prompts pass through feature-flag gating unchanged. The
// per-request inventory in HTTP mode must always install a checker so that
// MCP registration (which can only serve a given tool name once) sees a
// deduplicated set of dual-name variants.
//
// Returns self for chaining.
func (b *Builder) WithFeatureChecker(checker FeatureFlagChecker) *Builder {
	b.featureChecker = checker
	return b
}

// WithFilter adds a filter function that will be applied to all tools.
// Multiple filters can be added and are evaluated in order.
// If any filter returns false or an error, the tool is excluded.
// Returns self for chaining.
func (b *Builder) WithFilter(filter ToolFilter) *Builder {
	b.filters = append(b.filters, filter)
	return b
}

// WithExcludeTools specifies tools that should be disabled regardless of other settings.
// These tools will be excluded even if their toolset is enabled or they are in the
// additional tools list. This takes precedence over all other tool enablement settings.
// Input is cleaned (trimmed, deduplicated) before applying.
// Returns self for chaining.
func (b *Builder) WithExcludeTools(toolNames []string) *Builder {
	cleaned := cleanTools(toolNames)
	if len(cleaned) > 0 {
		b.filters = append(b.filters, CreateExcludeToolsFilter(cleaned))
	}
	return b
}

// CreateExcludeToolsFilter creates a ToolFilter that excludes tools by name.
// Any tool whose name appears in the excluded list will be filtered out.
// The input slice should already be cleaned (trimmed, deduplicated).
func CreateExcludeToolsFilter(excluded []string) ToolFilter {
	set := make(map[string]struct{}, len(excluded))
	for _, name := range excluded {
		set[name] = struct{}{}
	}
	return func(_ context.Context, tool *ServerTool) (bool, error) {
		_, blocked := set[tool.Tool.Name]
		return !blocked, nil
	}
}

// cleanTools trims whitespace and removes duplicates from tool names.
// Empty strings after trimming are excluded.
func cleanTools(tools []string) []string {
	seen := make(map[string]bool)
	var cleaned []string
	for _, name := range tools {
		trimmed := strings.TrimSpace(name)
		if trimmed == "" {
			continue
		}
		if !seen[trimmed] {
			seen[trimmed] = true
			cleaned = append(cleaned, trimmed)
		}
	}
	return cleaned
}

// Build creates the final Inventory with all configuration applied.
// This processes toolset filtering, tool name resolution, and sets up
// the inventory for use. The returned Inventory is ready for use with
// AvailableTools(), RegisterAll(), etc.
//
// Build returns an error if any tools specified via WithTools() are not recognized
// (i.e., they don't exist in the tool set and are not deprecated aliases).
// This ensures invalid tool configurations fail fast at build time.
func (b *Builder) Build() (*Inventory, error) {
	tools := b.tools

	// Install the feature-flag filter at the head of the pipeline so that
	// flag-gated tools are excluded before any user-supplied WithFilter sees
	// them. Doing this in Build() (rather than inside WithFeatureChecker)
	// keeps the install idempotent — repeated WithFeatureChecker calls
	// replace the checker without stacking duplicate filters.
	filters := b.filters
	if b.featureChecker != nil {
		filters = append([]ToolFilter{createFeatureFlagFilter(b.featureChecker)}, filters...)
	}

	r := &Inventory{
		tools:             tools,
		resourceTemplates: b.resourceTemplates,
		prompts:           b.prompts,
		deprecatedAliases: b.deprecatedAliases,
		readOnly:          b.readOnly,
		featureChecker:    b.featureChecker,
		filters:           filters,
	}

	// Process toolsets and pre-compute metadata in a single pass
	r.enabledToolsets, r.unrecognizedToolsets, r.toolsetIDs, r.toolsetIDSet, r.defaultToolsetIDs, r.toolsetDescriptions = b.processToolsets()

	// Build set of valid tool names for validation
	validToolNames := make(map[string]bool, len(tools))
	for i := range tools {
		validToolNames[tools[i].Tool.Name] = true
	}

	// Process additional tools (clean, resolve aliases, and track unrecognized)
	if len(b.additionalTools) > 0 {
		cleanedTools := cleanTools(b.additionalTools)

		r.additionalTools = make(map[string]bool, len(cleanedTools))
		var unrecognizedTools []string
		for _, name := range cleanedTools {
			// Always include the original name - this handles the case where
			// the tool exists but is controlled by a feature flag that's OFF.
			r.additionalTools[name] = true
			// Also include the canonical name if this is a deprecated alias.
			// This handles the case where the feature flag is ON and only
			// the new consolidated tool is available.
			if canonical, isAlias := b.deprecatedAliases[name]; isAlias {
				r.additionalTools[canonical] = true
			} else if !validToolNames[name] {
				// Not a valid tool and not a deprecated alias - track as unrecognized
				unrecognizedTools = append(unrecognizedTools, name)
			}
		}

		// Error out if there are unrecognized tools
		if len(unrecognizedTools) > 0 {
			return nil, fmt.Errorf("%w: %s", ErrUnknownTools, strings.Join(unrecognizedTools, ", "))
		}
	}

	if b.generateInstructions {
		r.instructions = generateInstructions(r)
	}

	return r, nil
}

// processToolsets processes the toolsetIDs configuration and returns:
// - enabledToolsets map (nil means all enabled)
// - unrecognizedToolsets list for warnings
// - allToolsetIDs sorted list of all toolset IDs
// - toolsetIDSet map for O(1) HasToolset lookup
// - defaultToolsetIDs sorted list of default toolset IDs
// - toolsetDescriptions map of toolset ID to description
func (b *Builder) processToolsets() (map[ToolsetID]bool, []string, []ToolsetID, map[ToolsetID]bool, []ToolsetID, map[ToolsetID]string) {
	// Single pass: collect all toolset metadata together
	validIDs := make(map[ToolsetID]bool)
	defaultIDs := make(map[ToolsetID]bool)
	descriptions := make(map[ToolsetID]string)

	for i := range b.tools {
		t := &b.tools[i]
		validIDs[t.Toolset.ID] = true
		if t.Toolset.Default {
			defaultIDs[t.Toolset.ID] = true
		}
		if t.Toolset.Description != "" {
			descriptions[t.Toolset.ID] = t.Toolset.Description
		}
	}
	for i := range b.resourceTemplates {
		r := &b.resourceTemplates[i]
		validIDs[r.Toolset.ID] = true
		if r.Toolset.Default {
			defaultIDs[r.Toolset.ID] = true
		}
		if r.Toolset.Description != "" {
			descriptions[r.Toolset.ID] = r.Toolset.Description
		}
	}
	for i := range b.prompts {
		p := &b.prompts[i]
		validIDs[p.Toolset.ID] = true
		if p.Toolset.Default {
			defaultIDs[p.Toolset.ID] = true
		}
		if p.Toolset.Description != "" {
			descriptions[p.Toolset.ID] = p.Toolset.Description
		}
	}

	// Build sorted slices from the collected maps
	allToolsetIDs := make([]ToolsetID, 0, len(validIDs))
	for id := range validIDs {
		allToolsetIDs = append(allToolsetIDs, id)
	}
	slices.Sort(allToolsetIDs)

	defaultToolsetIDList := make([]ToolsetID, 0, len(defaultIDs))
	for id := range defaultIDs {
		defaultToolsetIDList = append(defaultToolsetIDList, id)
	}
	slices.Sort(defaultToolsetIDList)

	toolsetIDs := b.toolsetIDs

	// Check for "all" keyword - enables all toolsets
	for _, id := range toolsetIDs {
		if strings.TrimSpace(id) == "all" {
			return nil, nil, allToolsetIDs, validIDs, defaultToolsetIDList, descriptions // nil means all enabled
		}
	}

	// nil means use defaults, empty slice means no toolsets
	if b.toolsetIDsIsNil {
		toolsetIDs = []string{"default"}
	}

	// Expand "default" keyword, trim whitespace, collect other IDs, and track unrecognized
	seen := make(map[ToolsetID]bool)
	expanded := make([]ToolsetID, 0, len(toolsetIDs))
	var unrecognized []string

	for _, id := range toolsetIDs {
		trimmed := strings.TrimSpace(id)
		if trimmed == "" {
			continue
		}
		if trimmed == "default" {
			for _, defaultID := range defaultToolsetIDList {
				if !seen[defaultID] {
					seen[defaultID] = true
					expanded = append(expanded, defaultID)
				}
			}
		} else {
			tsID := ToolsetID(trimmed)
			if !seen[tsID] {
				seen[tsID] = true
				expanded = append(expanded, tsID)
				// Track if this toolset doesn't exist
				if !validIDs[tsID] {
					unrecognized = append(unrecognized, trimmed)
				}
			}
		}
	}

	if len(expanded) == 0 {
		return make(map[ToolsetID]bool), unrecognized, allToolsetIDs, validIDs, defaultToolsetIDList, descriptions
	}

	enabledToolsets := make(map[ToolsetID]bool, len(expanded))
	for _, id := range expanded {
		enabledToolsets[id] = true
	}
	return enabledToolsets, unrecognized, allToolsetIDs, validIDs, defaultToolsetIDList, descriptions
}

// mcpAppsMetaKeys lists the Meta keys controlled by the remote_mcp_ui_apps feature flag.
var mcpAppsMetaKeys = []string{
	"ui", // MCP Apps UI metadata
}

// stripMCPAppsMetadata removes MCP Apps UI metadata from tools when the
// remote_mcp_ui_apps feature flag is not enabled.
func stripMCPAppsMetadata(tools []ServerTool) []ServerTool {
	result := make([]ServerTool, 0, len(tools))
	for _, tool := range tools {
		if stripped := stripMetaKeys(tool, mcpAppsMetaKeys); stripped != nil {
			result = append(result, *stripped)
		} else {
			result = append(result, tool)
		}
	}
	return result
}

// stripMetaKeys removes the specified Meta keys from a single tool.
// Returns a modified copy if changes were made, nil otherwise.
func stripMetaKeys(tool ServerTool, keys []string) *ServerTool {
	if tool.Tool.Meta == nil || len(keys) == 0 {
		return nil
	}

	// Check if any of the specified keys exist
	hasKeys := false
	for _, key := range keys {
		if _, ok := tool.Tool.Meta[key]; ok {
			hasKeys = true
			break
		}
	}
	if !hasKeys {
		return nil
	}

	// Make a shallow copy and remove specified keys
	toolCopy := tool
	newMeta := make(map[string]any, len(tool.Tool.Meta))
	for k, v := range tool.Tool.Meta {
		if !slices.Contains(keys, k) {
			newMeta[k] = v
		}
	}

	if len(newMeta) == 0 {
		toolCopy.Tool.Meta = nil
	} else {
		toolCopy.Tool.Meta = newMeta
	}
	return &toolCopy
}
