package main

import (
	"context"
	"fmt"
	"net/url"
	"os"
	"slices"
	"sort"
	"strings"

	"github.com/github/github-mcp-server/pkg/github"
	"github.com/github/github-mcp-server/pkg/inventory"
	"github.com/github/github-mcp-server/pkg/translations"
	"github.com/google/jsonschema-go/jsonschema"
	"github.com/spf13/cobra"
)

var generateDocsCmd = &cobra.Command{
	Use:   "generate-docs",
	Short: "Generate documentation for tools and toolsets",
	Long:  `Generate the automated sections of README.md and docs/remote-server.md with current tool and toolset information.`,
	RunE: func(_ *cobra.Command, _ []string) error {
		return generateAllDocs()
	},
}

func init() {
	rootCmd.AddCommand(generateDocsCmd)
}

// noFeatureFlagsChecker reports every feature flag as disabled. It models the
// default user experience used by the generated documentation.
func noFeatureFlagsChecker(_ context.Context, _ string) (bool, error) {
	return false, nil
}

func generateAllDocs() error {
	for _, doc := range []struct {
		path string
		fn   func(string) error
	}{
		// File to edit, function to generate its docs
		{"README.md", generateReadmeDocs},
		{"docs/remote-server.md", generateRemoteServerDocs},
		{"docs/insiders-features.md", generateInsidersFeaturesDocs},
		{"docs/feature-flags.md", generateFeatureFlagsDocs},
		{"docs/tool-renaming.md", generateDeprecatedAliasesDocs},
	} {
		if err := doc.fn(doc.path); err != nil {
			return fmt.Errorf("failed to generate docs for %s: %w", doc.path, err)
		}
		fmt.Printf("Successfully updated %s with automated documentation\n", doc.path)
	}
	return nil
}

func generateReadmeDocs(readmePath string) error {
	// Create translation helper
	t, _ := translations.TranslationHelper()

	// The README documents the default user experience: tools that are
	// enabled with no special flags set. Installing a checker that reports
	// every flag as disabled excludes tools gated by FeatureFlagEnable and
	// keeps the legacy variants of tools gated by FeatureFlagDisable, so
	// flag-gated duplicates don't appear twice.
	// Build() can only fail if WithTools specifies invalid tools - not used here
	r, _ := github.NewInventory(t).
		WithToolsets([]string{"all"}).
		WithFeatureChecker(noFeatureFlagsChecker).
		Build()

	// Generate toolsets documentation
	toolsetsDoc := generateToolsetsDoc(r)

	// Generate tools documentation
	toolsDoc := generateToolsDoc(r)

	// Read the current README.md
	// #nosec G304 - readmePath is controlled by command line flag, not user input
	content, err := os.ReadFile(readmePath)
	if err != nil {
		return fmt.Errorf("failed to read README.md: %w", err)
	}

	// Replace toolsets section
	updatedContent, err := replaceSection(string(content), "START AUTOMATED TOOLSETS", "END AUTOMATED TOOLSETS", toolsetsDoc)
	if err != nil {
		return err
	}

	// Replace tools section
	updatedContent, err = replaceSection(updatedContent, "START AUTOMATED TOOLS", "END AUTOMATED TOOLS", toolsDoc)
	if err != nil {
		return err
	}

	// Write back to file
	err = os.WriteFile(readmePath, []byte(updatedContent), 0600)
	if err != nil {
		return fmt.Errorf("failed to write README.md: %w", err)
	}

	return nil
}

func generateRemoteServerDocs(docsPath string) error {
	content, err := os.ReadFile(docsPath) //#nosec G304
	if err != nil {
		return fmt.Errorf("failed to read docs file: %w", err)
	}

	toolsetsDoc := generateRemoteToolsetsDoc()

	// Replace content between markers
	updatedContent, err := replaceSection(string(content), "START AUTOMATED TOOLSETS", "END AUTOMATED TOOLSETS", toolsetsDoc)
	if err != nil {
		return err
	}

	// Also generate remote-only toolsets section
	remoteOnlyDoc := generateRemoteOnlyToolsetsDoc()
	updatedContent, err = replaceSection(updatedContent, "START AUTOMATED REMOTE TOOLSETS", "END AUTOMATED REMOTE TOOLSETS", remoteOnlyDoc)
	if err != nil {
		return err
	}

	return os.WriteFile(docsPath, []byte(updatedContent), 0600) //#nosec G306
}

// octiconImg returns an img tag for an Octicon that works with GitHub's light/dark theme.
// Uses picture element with prefers-color-scheme for automatic theme switching.
// References icons from the repo's pkg/octicons/icons directory.
// Optional pathPrefix for files in subdirectories (e.g., "../" for docs/).
func octiconImg(name string, pathPrefix ...string) string {
	if name == "" {
		return ""
	}
	prefix := ""
	if len(pathPrefix) > 0 {
		prefix = pathPrefix[0]
	}
	// Use picture element with media queries for light/dark mode support
	// GitHub renders these correctly in markdown
	lightIcon := fmt.Sprintf("%spkg/octicons/icons/%s-light.png", prefix, name)
	darkIcon := fmt.Sprintf("%spkg/octicons/icons/%s-dark.png", prefix, name)
	return fmt.Sprintf(`<picture><source media="(prefers-color-scheme: dark)" srcset="%s"><source media="(prefers-color-scheme: light)" srcset="%s"><img src="%s" width="20" height="20" alt="%s"></picture>`, darkIcon, lightIcon, lightIcon, name)
}

func generateToolsetsDoc(i *inventory.Inventory) string {
	var buf strings.Builder

	// Add table header and separator (with icon column)
	buf.WriteString("|     | Toolset                 | Description                                                   |\n")
	buf.WriteString("| --- | ----------------------- | ------------------------------------------------------------- |\n")

	// Add the context toolset row with custom description (strongly recommended)
	// Get context toolset for its icon
	contextIcon := octiconImg("person")
	fmt.Fprintf(&buf, "| %s | `context`               | **Strongly recommended**: Tools that provide context about the current user and GitHub context you are operating in |\n", contextIcon)

	// AvailableToolsets() returns toolsets that have tools, sorted by ID
	// Exclude context (custom description above)
	for _, ts := range i.AvailableToolsets("context") {
		icon := octiconImg(ts.Icon)
		fmt.Fprintf(&buf, "| %s | `%s` | %s |\n", icon, ts.ID, ts.Description)
	}

	return strings.TrimSuffix(buf.String(), "\n")
}

func generateToolsDoc(r *inventory.Inventory) string {
	tools := r.ToolsForRegistration(context.Background())
	if len(tools) == 0 {
		return ""
	}

	var buf strings.Builder
	var toolBuf strings.Builder
	var currentToolsetID inventory.ToolsetID
	var currentToolsetIcon string
	firstSection := true

	writeSection := func() {
		if toolBuf.Len() == 0 {
			return
		}
		if !firstSection {
			buf.WriteString("\n\n")
		}
		firstSection = false
		sectionName := formatToolsetName(string(currentToolsetID))
		icon := octiconImg(currentToolsetIcon)
		if icon != "" {
			icon += " "
		}
		fmt.Fprintf(&buf, "<details>\n\n<summary>%s%s</summary>\n\n%s\n\n</details>", icon, sectionName, strings.TrimSuffix(toolBuf.String(), "\n\n"))
		toolBuf.Reset()
	}

	for _, tool := range tools {
		// When toolset changes, emit the previous section
		if tool.Toolset.ID != currentToolsetID {
			writeSection()
			currentToolsetID = tool.Toolset.ID
			currentToolsetIcon = tool.Toolset.Icon
		}
		writeToolDoc(&toolBuf, tool)
		toolBuf.WriteString("\n\n")
	}

	// Emit the last section
	writeSection()

	return buf.String()
}

func writeToolDoc(buf *strings.Builder, tool inventory.ServerTool) {
	// Tool name (no icon - section header already has the toolset icon)
	fmt.Fprintf(buf, "- **%s** - %s\n", tool.Tool.Name, tool.Tool.Annotations.Title)

	// OAuth scopes if present
	if len(tool.RequiredScopes) > 0 {
		// Scope filtering uses "any of" semantics (see scopes.HasRequiredScopes),
		// so when multiple required scopes are listed, render them as alternatives
		// rather than implying all are required.
		scopeList := "`" + strings.Join(tool.RequiredScopes, "`, `") + "`"
		if len(tool.RequiredScopes) > 1 {
			fmt.Fprintf(buf, "  - **Required OAuth Scopes (any of)**: %s\n", scopeList)
		} else {
			fmt.Fprintf(buf, "  - **Required OAuth Scopes**: %s\n", scopeList)
		}

		// Only show accepted scopes if they differ from required scopes
		if len(tool.AcceptedScopes) > 0 && !scopesEqual(tool.RequiredScopes, tool.AcceptedScopes) {
			fmt.Fprintf(buf, "  - **Accepted OAuth Scopes**: `%s`\n", strings.Join(tool.AcceptedScopes, "`, `"))
		}
	}

	// MCP App UI metadata (only rendered when the remote_mcp_ui_apps flag
	// applied to the inventory; for the no-flags README this section is
	// stripped by inventory.ToolsForRegistration before rendering).
	if ui, ok := tool.Tool.Meta["ui"].(map[string]any); ok {
		if uri, ok := ui["resourceUri"].(string); ok && uri != "" {
			fmt.Fprintf(buf, "  - **MCP App UI**: `%s`\n", uri)
		}
	}

	// Parameters
	if tool.Tool.InputSchema == nil {
		buf.WriteString("  - No parameters required")
		return
	}
	schema, ok := tool.Tool.InputSchema.(*jsonschema.Schema)
	if !ok || schema == nil {
		buf.WriteString("  - No parameters required")
		return
	}

	if len(schema.Properties) > 0 {
		// Get parameter names and sort them for deterministic order
		var paramNames []string
		for propName := range schema.Properties {
			paramNames = append(paramNames, propName)
		}
		sort.Strings(paramNames)

		for i, propName := range paramNames {
			prop := schema.Properties[propName]
			required := slices.Contains(schema.Required, propName)
			requiredStr := "optional"
			if required {
				requiredStr = "required"
			}

			var typeStr string

			// Get the type and description
			switch prop.Type {
			case "array":
				if prop.Items != nil {
					typeStr = prop.Items.Type + "[]"
				} else {
					typeStr = "array"
				}
			default:
				typeStr = prop.Type
			}

			// Indent any continuation lines in the description to maintain markdown formatting
			description := indentMultilineDescription(prop.Description, "    ")

			fmt.Fprintf(buf, "  - `%s`: %s (%s, %s)", propName, description, typeStr, requiredStr)
			if i < len(paramNames)-1 {
				buf.WriteString("\n")
			}
		}
	} else {
		buf.WriteString("  - No parameters required")
	}
}

// scopesEqual checks if two scope slices contain the same elements (order-independent)
func scopesEqual(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}

	// Create a map for quick lookup
	aMap := make(map[string]bool, len(a))
	for _, scope := range a {
		aMap[scope] = true
	}

	// Check if all elements in b are in a
	for _, scope := range b {
		if !aMap[scope] {
			return false
		}
	}

	return true
}

// indentMultilineDescription adds the specified indent to all lines after the first line.
// This ensures that multi-line descriptions maintain proper markdown list formatting.
func indentMultilineDescription(description, indent string) string {
	if !strings.Contains(description, "\n") {
		return description
	}
	var buf strings.Builder
	lines := strings.Split(description, "\n")
	buf.WriteString(lines[0])
	for i := 1; i < len(lines); i++ {
		buf.WriteString("\n")
		buf.WriteString(indent)
		buf.WriteString(lines[i])
	}
	return buf.String()
}

func replaceSection(content, startMarker, endMarker, newContent string) (string, error) {
	start := fmt.Sprintf("<!-- %s -->", startMarker)
	end := fmt.Sprintf("<!-- %s -->", endMarker)

	before, _, ok := strings.Cut(content, start)
	endIdx := strings.Index(content, end)
	if !ok || endIdx == -1 {
		return "", fmt.Errorf("markers not found: %s / %s", start, end)
	}

	var buf strings.Builder
	buf.WriteString(before)
	buf.WriteString(start)
	buf.WriteString("\n")
	buf.WriteString(newContent)
	buf.WriteString("\n")
	buf.WriteString(content[endIdx:])
	return buf.String(), nil
}

func generateRemoteToolsetsDoc() string {
	var buf strings.Builder

	// Create translation helper
	t, _ := translations.TranslationHelper()

	// Build inventory - stateless
	// Build() can only fail if WithTools specifies invalid tools - not used here
	r, _ := github.NewInventory(t).Build()

	// Generate table header (icon is combined with Name column)
	buf.WriteString("| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |\n")
	buf.WriteString("| ---- | ----------- | ------- | ------------------------- | -------------- | ----------------------------------- |\n")

	// Add "default" and "all" meta toolsets first (special cases). The base
	// URL serves the default toolset; /x/all enables every toolset at once.
	metaIcon := octiconImg("apps", "../")
	fmt.Fprintf(&buf, "| %s<br>`default` | Default toolset | https://api.githubcopilot.com/mcp/ | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%%7B%%22type%%22%%3A%%20%%22http%%22%%2C%%22url%%22%%3A%%20%%22https%%3A%%2F%%2Fapi.githubcopilot.com%%2Fmcp%%2F%%22%%7D) | [read-only](https://api.githubcopilot.com/mcp/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%%7B%%22type%%22%%3A%%20%%22http%%22%%2C%%22url%%22%%3A%%20%%22https%%3A%%2F%%2Fapi.githubcopilot.com%%2Fmcp%%2Freadonly%%22%%7D) |\n", metaIcon)
	fmt.Fprintf(&buf, "| %s<br>`all` | All available GitHub MCP tools | https://api.githubcopilot.com/mcp/x/all | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-all&config=%%7B%%22type%%22%%3A%%20%%22http%%22%%2C%%22url%%22%%3A%%20%%22https%%3A%%2F%%2Fapi.githubcopilot.com%%2Fmcp%%2Fx%%2Fall%%22%%7D) | [read-only](https://api.githubcopilot.com/mcp/x/all/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-all&config=%%7B%%22type%%22%%3A%%20%%22http%%22%%2C%%22url%%22%%3A%%20%%22https%%3A%%2F%%2Fapi.githubcopilot.com%%2Fmcp%%2Fx%%2Fall%%2Freadonly%%22%%7D) |\n", metaIcon)

	// AvailableToolsets() returns toolsets that have tools, sorted by ID
	// Exclude context (handled separately)
	for _, ts := range r.AvailableToolsets("context") {
		idStr := string(ts.ID)

		apiURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s", idStr)
		readonlyURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s/readonly", idStr)

		// Create install config JSON (URL encoded)
		installConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, apiURL))
		readonlyConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, readonlyURL))

		// Fix URL encoding to use %20 instead of + for spaces
		installConfig = strings.ReplaceAll(installConfig, "+", "%20")
		readonlyConfig = strings.ReplaceAll(readonlyConfig, "+", "%20")

		installLink := fmt.Sprintf("[Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", idStr, installConfig)
		readonlyInstallLink := fmt.Sprintf("[Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", idStr, readonlyConfig)

		icon := octiconImg(ts.Icon, "../")
		fmt.Fprintf(&buf, "| %s<br>`%s` | %s | %s | %s | [read-only](%s) | %s |\n",
			icon,
			idStr,
			ts.Description,
			apiURL,
			installLink,
			readonlyURL,
			readonlyInstallLink,
		)
	}

	return strings.TrimSuffix(buf.String(), "\n")
}

func generateRemoteOnlyToolsetsDoc() string {
	var buf strings.Builder

	// Generate table header (icon is combined with Name column)
	buf.WriteString("| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |\n")
	buf.WriteString("| ---- | ----------- | ------- | ------------------------- | -------------- | ----------------------------------- |\n")

	// Use RemoteOnlyToolsets from github package
	for _, ts := range github.RemoteOnlyToolsets() {
		idStr := string(ts.ID)

		apiURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s", idStr)
		readonlyURL := fmt.Sprintf("https://api.githubcopilot.com/mcp/x/%s/readonly", idStr)

		// Create install config JSON (URL encoded)
		installConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, apiURL))
		readonlyConfig := url.QueryEscape(fmt.Sprintf(`{"type": "http","url": "%s"}`, readonlyURL))

		// Fix URL encoding to use %20 instead of + for spaces
		installConfig = strings.ReplaceAll(installConfig, "+", "%20")
		readonlyConfig = strings.ReplaceAll(readonlyConfig, "+", "%20")

		installLink := fmt.Sprintf("[Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", idStr, installConfig)
		readonlyInstallLink := fmt.Sprintf("[Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-%s&config=%s)", idStr, readonlyConfig)

		icon := octiconImg(ts.Icon, "../")
		fmt.Fprintf(&buf, "| %s<br>`%s` | %s | %s | %s | [read-only](%s) | %s |\n",
			icon,
			idStr,
			ts.Description,
			apiURL,
			installLink,
			readonlyURL,
			readonlyInstallLink,
		)
	}

	return strings.TrimSuffix(buf.String(), "\n")
}

func generateDeprecatedAliasesDocs(docsPath string) error {
	// Read the current file
	content, err := os.ReadFile(docsPath) //#nosec G304
	if err != nil {
		return fmt.Errorf("failed to read docs file: %w", err)
	}

	// Generate the table
	aliasesDoc := generateDeprecatedAliasesTable()

	// Replace content between markers
	updatedContent, err := replaceSection(string(content), "START AUTOMATED ALIASES", "END AUTOMATED ALIASES", aliasesDoc)
	if err != nil {
		return err
	}

	// Write back to file
	err = os.WriteFile(docsPath, []byte(updatedContent), 0600)
	if err != nil {
		return fmt.Errorf("failed to write deprecated aliases docs: %w", err)
	}

	return nil
}

func generateDeprecatedAliasesTable() string {
	var buf strings.Builder

	// Add table header
	buf.WriteString("| Old Name | New Name |\n")
	buf.WriteString("|----------|----------|\n")

	aliases := github.DeprecatedToolAliases
	if len(aliases) == 0 {
		buf.WriteString("| *(none currently)* | |")
	} else {
		// Sort keys for deterministic output
		var oldNames []string
		for oldName := range aliases {
			oldNames = append(oldNames, oldName)
		}
		sort.Strings(oldNames)

		for i, oldName := range oldNames {
			newName := aliases[oldName]
			fmt.Fprintf(&buf, "| `%s` | `%s` |", oldName, newName)
			if i < len(oldNames)-1 {
				buf.WriteString("\n")
			}
		}
	}

	return buf.String()
}
