package main

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/spf13/cobra"
	"github.com/spf13/cobra/doc"
	"github.com/spf13/pflag"

	gitlab "gitlab.com/gitlab-org/api/client-go/v2"

	"gitlab.com/gitlab-org/cli/cmd/gen-docs/urlwrapper"
	"gitlab.com/gitlab-org/cli/internal/api"
	"gitlab.com/gitlab-org/cli/internal/cmdutils"
	"gitlab.com/gitlab-org/cli/internal/commands"
	"gitlab.com/gitlab-org/cli/internal/config"
	"gitlab.com/gitlab-org/cli/internal/git"
	"gitlab.com/gitlab-org/cli/internal/glinstance"
	"gitlab.com/gitlab-org/cli/internal/glrepo"
	"gitlab.com/gitlab-org/cli/internal/iostreams"
)

// quotedSegment matches a single-quoted segment such as
// 'glab config set browser mybrowser' or 'client_id'.
var quotedSegment = regexp.MustCompile(`'([^'\n]+)'`)

// codeWrapQuoted converts single-quoted segments into backtick-wrapped
// code so command and config-key references render with code formatting
// in the generated documentation.
func codeWrapQuoted(s string) string {
	return quotedSegment.ReplaceAllString(s, "`$1`")
}

func main() {
	var flagErr pflag.ErrorHandling
	docsCmd := pflag.NewFlagSet("", flagErr)
	manpage := docsCmd.BoolP("manpage", "m", false, "Generate manual pages instead of web docs.")
	path := docsCmd.StringP("path", "p", "./docs/source/", "Path where you want the generated docs saved.")
	navPath := docsCmd.StringP("nav-path", "n", "./docs/navigation-glab.yaml", "Path where the generated navigation YAML is saved.")
	help := docsCmd.BoolP("help", "h", false, "Help about any command.")

	if err := docsCmd.Parse(os.Args); err != nil {
		fatal(err)
	}
	if *help {
		_, err := fmt.Fprintf(os.Stderr, "Usage of %s:\n\n%s", os.Args[0], docsCmd.FlagUsages())
		if err != nil {
			fatal(err)
		}
		os.Exit(1)
	}
	err := os.MkdirAll(*path, 0o750)
	if err != nil {
		fatal(err)
	}

	glabCli := commands.NewCmdRoot(&factory{io: iostreams.New()})
	glabCli.DisableAutoGenTag = true
	if *manpage {
		if err := genManPage(glabCli, *path); err != nil {
			fatal(err)
		}
	} else {
		if err := genWebDocs(glabCli, *path); err != nil {
			fatal(err)
		}
		if err := genNav(glabCli, *navPath); err != nil {
			fatal(err)
		}
	}
}

func genManPage(glabCli *cobra.Command, path string) error {
	header := &doc.GenManHeader{
		Title:   "glab",
		Section: "1",
		Source:  "",
		Manual:  "",
	}
	err := doc.GenManTree(glabCli, header, path)
	if err != nil {
		return err
	}
	return nil
}

func genWebDocs(glabCli *cobra.Command, basePath string) error {
	// Generate docs for root command with special handling
	if err := genRootCommandDocs(glabCli, basePath); err != nil {
		return err
	}

	// Generate docs for all top-level commands
	for _, cmd := range glabCli.Commands() {
		if err := genCommandDocs(cmd, basePath, []string{}); err != nil {
			return err
		}
	}
	return nil
}

// genNav writes the GitLab CLI (glab) navigation submenu consumed by the
// GitLab Docs site (docs-gitlab-com): a standalone YAML list of the glab
// submenu's children at zero base indentation, which the docs site splices
// between sentinel comments. It walks the same command tree as the web docs,
// so the nav and the generated pages always agree on what exists.
func genNav(glabCli *cobra.Command, navPath string) error {
	buf := new(strings.Builder)
	buf.WriteString("# This file is auto-generated by `make gen-docs` in gitlab-org/cli.\n")
	buf.WriteString("# Do not edit it directly. It is spliced into the GitLab CLI (glab)\n")
	buf.WriteString("# section of data/en-us/navigation.yaml in docs-gitlab-com.\n")
	walk(glabCli, []string{}, 0, buf)
	return config.WriteFile(navPath, []byte(buf.String()), 0o644)
}

// walk recursively emits a YAML list entry for each available subcommand of
// cmd, mirroring the directory tree that the web docs produce.
//
// For depth d (top-level commands are depth 0):
//   - the "- title:" line is indented d*4 spaces
//   - the "url:"/"submenu:" keys are indented d*4+2 spaces
//   - children are emitted at depth d+1
func walk(cmd *cobra.Command, path []string, depth int, buf *strings.Builder) {
	for _, c := range cmd.Commands() {
		// Skip help commands and unavailable (hidden/deprecated) commands,
		// matching the web docs so the nav and docs stay aligned.
		if c.Name() == "help" || !c.IsAvailableCommand() {
			continue
		}

		p := append(append([]string{}, path...), c.Name())
		dash := strings.Repeat(" ", depth*4)

		fmt.Fprintf(buf, "%s- title: glab %s\n", dash, strings.Join(p, " "))
		fmt.Fprintf(buf, "%s  url: 'cli/%s/'\n", dash, strings.Join(p, "/"))

		if c.HasAvailableSubCommands() {
			fmt.Fprintf(buf, "%s  submenu:\n", dash)
			walk(c, p, depth+1, buf)
		}
	}
}

// genCommandDocs recursively generates documentation for a command and all its subcommands
// Top-level commands always get their own folder with _index.md
// Non-top-level commands with subcommands get their own folder with _index.md
// Non-top-level commands without subcommands get a .md file in their parent's directory
// and do not create their own directory
func genCommandDocs(cmd *cobra.Command, basePath string, parentPath []string) error {
	// Skip help commands and unavailable commands (hidden/deprecated)
	if cmd.Name() == "help" || !cmd.IsAvailableCommand() {
		return nil
	}

	// Build the current command path
	currentPath := append(parentPath, cmd.Name())
	fullPath := filepath.Join(append([]string{basePath}, currentPath...)...)

	fmt.Println("Generating docs for " + strings.Join(currentPath, " "))

	// Only create directory if this command has subcommands or is top-level
	if cmd.HasAvailableSubCommands() || len(parentPath) == 0 {
		if err := os.MkdirAll(fullPath, 0o750); err != nil {
			return err
		}
	}

	// Generate the command documentation
	out := new(bytes.Buffer)
	if err := GenMarkdownCustom(cmd, out); err != nil {
		return err
	}

	// Write the documentation file
	// - Top-level commands always get folder/_index.md (even without subcommands)
	// - Non-top-level commands with subcommands get folder/_index.md
	// - Non-top-level commands without subcommands get parent/command.md
	var filename string
	if cmd.HasAvailableSubCommands() || len(parentPath) == 0 {
		// Commands with subcommands OR top-level commands always get folder/_index.md
		filename = filepath.Join(fullPath, "_index.md")
	} else {
		// Non-top-level leaf commands (no subcommands) write as a .md file in the parent directory
		parentDir := filepath.Join(append([]string{basePath}, parentPath...)...)
		filename = filepath.Join(parentDir, cmd.Name()+".md")
	}

	if err := config.WriteFile(filename, out.Bytes(), 0o644); err != nil {
		return err
	}

	// Recursively generate docs for all subcommands
	for _, subCmd := range cmd.Commands() {
		if subCmd.Name() != "help" && subCmd.IsAvailableCommand() {
			if err := genCommandDocs(subCmd, basePath, currentPath); err != nil {
				return err
			}
		}
	}

	return nil
}

// genRootCommandDocs generates documentation for the root command with custom content preservation
func genRootCommandDocs(cmd *cobra.Command, basePath string) error {
	fmt.Println("Generating docs for root command")

	// Generate the root command documentation with custom intro content
	out := new(bytes.Buffer)
	if err := GenRootMarkdownCustom(cmd, out); err != nil {
		return err
	}

	// Write to _index.md in the base path
	filename := filepath.Join(basePath, "_index.md")
	if err := config.WriteFile(filename, out.Bytes(), 0o644); err != nil {
		return err
	}

	return nil
}

func printSubcommands(cmd *cobra.Command, buf *bytes.Buffer) {
	if len(cmd.Commands()) < 1 {
		return
	}

	var subcommands strings.Builder
	// Generate children commands
	for _, cmdC := range cmd.Commands() {
		if cmdC.Name() != "help" && cmdC.IsAvailableCommand() {
			if cmdC.HasAvailableSubCommands() {
				subcommands.WriteString(fmt.Sprintf("- [`%s`](%s/_index.md)\n", cmdC.Name(), cmdC.Name()))
			} else {
				subcommands.WriteString(fmt.Sprintf("- [`%s`](%s.md)\n", cmdC.Name(), cmdC.Name()))
			}
		}
	}

	if subcommands.String() != "" {
		buf.WriteString("\n## Subcommands\n\n")
		buf.WriteString(subcommands.String())
	}
}

func printRootSubcommands(cmd *cobra.Command, buf *bytes.Buffer) {
	if len(cmd.Commands()) < 1 {
		return
	}

	var subcommands strings.Builder
	// Generate children commands for root
	// All top-level commands get directories and _index.md files based on the generation logic
	for _, cmdC := range cmd.Commands() {
		if cmdC.Name() != "help" && cmdC.IsAvailableCommand() {
			subcommands.WriteString(fmt.Sprintf("- [`glab %s`](%s/_index.md)\n", cmdC.Name(), cmdC.Name()))
		}
	}

	if subcommands.String() != "" {
		buf.WriteString("\n## Commands\n\n")
		buf.WriteString(subcommands.String())
	}
}

// printRootOptions is like printOptions but without the leading newline for root command
func printRootOptions(buf *bytes.Buffer, cmd *cobra.Command) error {
	flags := cmd.NonInheritedFlags()
	flags.SetOutput(buf)
	if flags.HasAvailableFlags() {
		buf.WriteString("## Options\n\n```plaintext\n")
		flags.PrintDefaults()
		buf.WriteString("```\n")
	}

	parentFlags := cmd.InheritedFlags()
	parentFlags.SetOutput(buf)
	if parentFlags.HasAvailableFlags() {
		buf.WriteString("\n## Options inherited from parent commands\n\n```plaintext\n")
		parentFlags.PrintDefaults()
		buf.WriteString("```\n")
	}
	return nil
}

func fatal(err error) {
	fmt.Fprintln(os.Stderr, err)
	os.Exit(1)
}

type factory struct {
	io *iostreams.IOStreams
}

func (f *factory) RepoOverride(repo string) error {
	return nil
}

func (f *factory) ApiClient(repoHost string) (*api.Client, error) {
	return nil, errors.New("not implemented")
}

func (f *factory) GitLabClient() (*gitlab.Client, error) {
	return nil, errors.New("not implemented")
}

func (f *factory) BaseRepo() (glrepo.Interface, error) {
	return nil, errors.New("not implemented")
}

func (f *factory) Remotes() (glrepo.Remotes, error) {
	return nil, errors.New("not implemented")
}

func (f *factory) Config() config.Config {
	return nil
}

func (f *factory) Branch() (string, error) {
	return "", errors.New("not implemented")
}

func (f *factory) IO() *iostreams.IOStreams {
	return f.io
}

func (f *factory) DefaultHostname() string {
	return glinstance.DefaultHostname
}

func (f *factory) BuildInfo() api.BuildInfo {
	return api.BuildInfo{}
}

func (f *factory) Executor() cmdutils.Executor {
	return nil
}

func (f *factory) GitRunner() git.GitRunner {
	return git.StandardGitCommand{}
}

// adapted from: github.com/spf13/cobra/blob/main/doc/md_docs.go
// GenMarkdownTreeCustom is the same as GenMarkdownTree, but
// with custom filePrepender and linkHandler.
func GenMarkdownTreeCustom(cmd *cobra.Command, dir string) error {
	for _, c := range cmd.Commands() {
		if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
			continue
		}
		if err := GenMarkdownTreeCustom(c, dir); err != nil {
			return err
		}
	}

	basename := strings.ReplaceAll(cmd.Name(), " ", "_") + ".md"
	filename := filepath.Join(dir, basename)
	f, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer f.Close()

	if err := GenMarkdownCustom(cmd, f); err != nil {
		return err
	}
	return nil
}

// GenMarkdownCustom creates custom Markdown output. github.com/spf13/cobra/blob/main/doc/md_docs.go
func GenMarkdownCustom(cmd *cobra.Command, w io.Writer) error {
	cmd.InitDefaultHelpCmd()
	cmd.InitDefaultHelpFlag()

	buf := new(bytes.Buffer)
	name := cmd.CommandPath()
	// GitLab Specific Docs Metadata
	buf.WriteString("---" + "\n")
	buf.WriteString("title: '`" + name + "`'\n")
	buf.WriteString("stage: AI Coding" + "\n")
	buf.WriteString("group: Code Review" + "\n")
	buf.WriteString("info: To determine the technical writer assigned to the Stage/Group associated with this page, see <https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments>" + "\n")
	buf.WriteString("---" + "\n\n")

	// Generated by a script
	buf.WriteString("<!--" + "\n")
	buf.WriteString("This documentation is auto generated by a script." + "\n")
	buf.WriteString("Please do not edit this file directly. Run `make gen-docs` instead." + "\n")
	buf.WriteString("-->" + "\n\n")

	// Wrap URLs in Short description
	shortDesc := urlwrapper.MDWrap(cmd.Short)
	buf.WriteString(shortDesc + "\n")

	if len(cmd.Long) > 0 {
		// Skipping `help` commands until Long description can be revised
		if cmd.Name() != "help" {
			buf.WriteString("\n## Synopsis\n\n")
			// Wrap URLs in Long description
			longDesc := urlwrapper.MDWrap(cmd.Long)
			buf.WriteString(longDesc)
		}
	}

	if cmd.Runnable() {
		fmt.Fprintf(buf, "\n```plaintext\n%s\n```\n", cmd.UseLine())
	}

	if len(cmd.Aliases) > 0 {
		buf.WriteString("\n## Aliases\n\n")
		fmt.Fprintf(buf, "```plaintext\n%s\n```\n", strings.Join(cmd.Aliases, "\n"))
	}

	if len(cmd.Example) > 0 {
		buf.WriteString("\n## Examples\n\n")
		fmt.Fprintf(buf, "```console\n%s\n```\n", cmd.Example)
	}

	if err := printOptions(buf, cmd); err != nil {
		return err
	}

	printSubcommands(cmd, buf)

	_, err := buf.WriteTo(w)
	return err
}

// adapted from: github.com/spf13/cobra/blob/main/doc/md_docs.go
func printOptions(buf *bytes.Buffer, cmd *cobra.Command) error {
	flags := cmd.NonInheritedFlags()
	flags.SetOutput(buf)
	if flags.HasAvailableFlags() {
		buf.WriteString("\n## Options\n\n```plaintext\n")
		flags.PrintDefaults()
		buf.WriteString("```\n")
	}

	parentFlags := cmd.InheritedFlags()
	parentFlags.SetOutput(buf)
	if parentFlags.HasAvailableFlags() {
		buf.WriteString("\n## Options inherited from parent commands\n\n```plaintext\n")
		parentFlags.PrintDefaults()
		buf.WriteString("```\n")
	}
	return nil
}

// GenRootMarkdownCustom creates custom markdown for the root command with preserved intro content
func GenRootMarkdownCustom(cmd *cobra.Command, w io.Writer) error {
	buf := new(bytes.Buffer)
	// GitLab Specific Docs Metadata
	buf.WriteString("---" + "\n")
	buf.WriteString("title: GitLab CLI (glab)\n")
	buf.WriteString("stage: Create" + "\n")
	buf.WriteString("group: Code Review" + "\n")
	buf.WriteString("info: To determine the technical writer assigned to the Stage/Group associated with this page, see <https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments>" + "\n")
	buf.WriteString("---" + "\n\n")

	// Generated by a script comment
	buf.WriteString("<!--" + "\n")
	buf.WriteString("This documentation is auto generated by a script." + "\n")
	buf.WriteString("Please do not edit this file directly. Run `make gen-docs` instead." + "\n")
	buf.WriteString("-->" + "\n\n")

	// Add details shortcode
	buf.WriteString("{{< details >}}\n\n")
	buf.WriteString("- Tier: Free, Premium, Ultimate\n")
	buf.WriteString("- Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated\n\n")
	buf.WriteString("{{< /details >}}\n\n")

	// Custom title and intro content (from README)
	buf.WriteString("GLab is an open source GitLab CLI tool. It brings GitLab to your terminal, next to where you are already working with `git` and your code, without switching between windows and browser tabs. While it's powerful for issues and merge requests, `glab` does even more:\n\n")
	buf.WriteString("- View, manage, and retry CI/CD pipelines directly from your CLI.\n")
	buf.WriteString("- Create changelogs.\n")
	buf.WriteString("- Create and manage releases.\n")
	buf.WriteString("- Ask GitLab Duo Chat (Classic) questions about Git.\n")
	buf.WriteString("- Manage GitLab agents for Kubernetes.\n\n")
	buf.WriteString("`glab` is available for repositories hosted on GitLab.com, GitLab Dedicated, and GitLab Self-Managed. It supports multiple authenticated GitLab instances, and automatically detects the authenticated hostname from the remotes available in your working Git directory.\n\n")
	buf.WriteString("![command example](img/getting-started.gif)\n\n")

	// Install and authenticate sections (moved up, with README links)
	buf.WriteString("## Install the CLI\n\n")
	buf.WriteString("Installation instructions are available in the GLab\n")
	buf.WriteString("[`README`](https://gitlab.com/gitlab-org/cli/#installation).\n\n")

	buf.WriteString("## Authenticate with GitLab\n\n")
	buf.WriteString("GLab supports multiple authentication methods including OAuth and personal access tokens.\n")
	buf.WriteString("To get started, run `glab auth login` and follow the interactive setup.\n\n")
	buf.WriteString("For detailed authentication instructions, see the [Authentication section](https://gitlab.com/gitlab-org/cli#authentication)\n")
	buf.WriteString("in the main README.\n\n")

	// Pagination section
	buf.WriteString("## Pagination\n\n")
	buf.WriteString("Commands that return lists of items are paginated. Use `--page` and `--per-page` to\n")
	buf.WriteString("navigate results. These commands respect the pagination limits configured on your\n")
	buf.WriteString("GitLab instance.\n\n")

	if cmd.Runnable() {
		fmt.Fprintf(buf, "```plaintext\n%s\n```\n\n", cmd.UseLine())
	}

	// Generate environment variables section from annotations with table formatting
	if envHelp, ok := cmd.Annotations["help:environment"]; ok {
		buf.WriteString("## Environment Variables\n\n")
		buf.WriteString("<!-- markdownlint-disable MD044 MD034 -->\n\n")
		buf.WriteString("| Variable | Description |\n")
		buf.WriteString("|----------|-------------|\n")

		// Parse the environment variables from the annotation
		for envBlock := range strings.SplitSeq(strings.TrimSpace(envHelp), "\n\n") {
			lines := strings.Split(envBlock, "\n")
			if len(lines) > 0 {
				firstLine := strings.TrimSpace(lines[0])
				if strings.Contains(firstLine, ":") {
					parts := strings.SplitN(firstLine, ":", 2)
					if len(parts) == 2 {
						varName := strings.TrimSpace(parts[0])
						description := strings.TrimSpace(parts[1])

						// Add additional lines to description
						for i := 1; i < len(lines); i++ {
							additionalLine := strings.TrimSpace(lines[i])
							if additionalLine != "" {
								description += " " + additionalLine
							}
						}

						// Wrap single-quoted segments (command and config-key
						// references) in backticks so they render as code.
						description = codeWrapQuoted(description)

						// Wrap any bare URLs in the description with markdown link syntax
						description = urlwrapper.MDWrap(description)

						buf.WriteString(fmt.Sprintf("| `%s` | %s |\n", varName, description))
					}
				}
			}
		}
		buf.WriteString("\n<!-- markdownlint-enable MD044 MD034 -->\n")
		buf.WriteString("\n")
	}
	if err := printRootOptions(buf, cmd); err != nil {
		return err
	}

	// Generate subcommands (replaces manual "Core commands")
	printRootSubcommands(cmd, buf)

	// Add troubleshooting section before the report issues call to action
	buf.WriteString("\n## Troubleshooting\n\n")
	buf.WriteString("When working with `glab`, you might encounter the following issues.\n\n")
	buf.WriteString("### Error: `invalid_client` during OAuth login\n\n")
	buf.WriteString("When authenticating with an OAuth application, `glab auth login` might fail with an error:\n\n")
	buf.WriteString("```plaintext\n")
	buf.WriteString("Oauth2: \"invalid_client\" \"Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.\".\n")
	buf.WriteString("```\n\n")
	buf.WriteString("This happens when the OAuth application has the **Confidential** option enabled. `glab` is a\n")
	buf.WriteString("public client and cannot keep a client secret, so it authenticates without one. Confidential\n")
	buf.WriteString("applications require a client secret, which causes GitLab to reject the request.\n\n")
	buf.WriteString("To resolve this issue, edit your OAuth application and clear the **Confidential** checkbox.\n\n")
	buf.WriteString("After saving the change, run `glab auth login` again. For the full list of required application\n")
	buf.WriteString("settings, see the [OAuth (GitLab Self-Managed, GitLab Dedicated)](https://gitlab.com/gitlab-org/cli#oauth-gitlab-self-managed-gitlab-dedicated)\n")
	buf.WriteString("section in the main README.\n")

	// Add report issues section at the end
	buf.WriteString("\n## Report issues\n\n")
	buf.WriteString("Open an issue in the [`gitlab-org/cli` repository](https://gitlab.com/gitlab-org/cli/issues/new)\n")
	buf.WriteString("to send us feedback.\n")

	_, err := buf.WriteTo(w)
	return err
}
