package main

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"regexp"
	"slices"
	"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`")
}

// generatedMarker identifies pages produced by this generator. Pruning only
// removes Markdown files that carry it, so hand-maintained files and images
// under the docs directory are never deleted.
const generatedMarker = "This documentation is auto generated by a script."

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 {
	// Remove previously generated pages first so that pages for commands that no
	// longer exist are not left behind. Only marker-bearing Markdown files are
	// pruned; images and any hand-maintained files are preserved. Regenerating
	// below recreates every page that is still documented.
	if err := pruneGeneratedPages(basePath); err != nil {
		return err
	}

	// Generate docs for root command with special handling
	if err := genRootCommandDocs(glabCli, basePath); err != nil {
		return err
	}

	// Generate the landing page that lists every top-level command
	if err := genCommandsPage(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
		}
	}

	// Drop directories left empty after pruning, for example a removed
	// command's folder.
	return removeEmptyDirs(basePath)
}

// pruneGeneratedPages deletes every Markdown file under basePath that carries
// the generated-by-a-script marker. Because genWebDocs regenerates all
// documented commands afterwards, a page whose command was removed from the
// tree stays deleted. Only marker-bearing .md files are touched, so images and
// any hand-maintained files are left in place.
func pruneGeneratedPages(basePath string) error {
	return filepath.WalkDir(basePath, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() || !strings.HasSuffix(d.Name(), ".md") {
			return nil
		}
		content, err := os.ReadFile(path)
		if err != nil {
			return err
		}
		if !bytes.Contains(content, []byte(generatedMarker)) {
			return nil
		}
		return os.Remove(path)
	})
}

// removeEmptyDirs deletes directories left empty under basePath, deepest first
// so that parents emptied by removing their children are also cleaned up.
// basePath itself is never removed.
func removeEmptyDirs(basePath string) error {
	var dirs []string
	err := filepath.WalkDir(basePath, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if d.IsDir() && path != basePath {
			dirs = append(dirs, path)
		}
		return nil
	})
	if err != nil {
		return err
	}

	// Remove the deepest directories first so a parent that becomes empty once
	// its children are gone is caught in the same pass.
	slices.SortFunc(dirs, func(a, b string) int { return len(b) - len(a) })
	for _, dir := range dirs {
		entries, err := os.ReadDir(dir)
		if err != nil {
			return err
		}
		if len(entries) == 0 {
			if err := os.Remove(dir); 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
}

// writeGeneratedHeader writes the GitLab docs metadata front matter and the
// generated-by-a-script comment that every page this generator produces starts
// with. The comment carries generatedMarker, so pages written through this
// helper are pruned and rewritten on each run.
func writeGeneratedHeader(buf *bytes.Buffer, title, stage string) {
	buf.WriteString("---" + "\n")
	buf.WriteString("title: " + title + "\n")
	buf.WriteString("stage: " + stage + "\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")

	buf.WriteString("<!--" + "\n")
	buf.WriteString(generatedMarker + "\n")
	buf.WriteString("Please do not edit this file directly. Run `make gen-docs` instead." + "\n")
	buf.WriteString("-->" + "\n\n")
}

// genCommandsPage writes the commands landing page, which lists every
// top-level command. The GitLab Docs navigation gives every entry a URL, so the
// Commands parent of the generated command reference needs a real page at
// cli/commands/.
func genCommandsPage(cmd *cobra.Command, basePath string) error {
	fmt.Println("Generating docs for the commands landing page")

	buf := new(bytes.Buffer)
	writeGeneratedHeader(buf, "Commands", "AI Coding")

	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")

	buf.WriteString("`glab` provides these top-level commands. Each page describes the command's\n")
	buf.WriteString("subcommands, options, and examples.\n\n")
	buf.WriteString(rootSubcommandList(cmd))

	filename := filepath.Join(basePath, "commands.md")
	return config.WriteFile(filename, buf.Bytes(), 0o644)
}

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())
	}
}

// rootSubcommandList returns a Markdown bullet list of the root command's
// available top-level commands. All top-level commands get directories and
// _index.md files based on the generation logic, and the links are relative to
// the docs base directory, so the list is valid from any page written there.
func rootSubcommandList(cmd *cobra.Command) string {
	var subcommands strings.Builder
	for _, cmdC := range cmd.Commands() {
		if cmdC.Name() != "help" && cmdC.IsAvailableCommand() {
			fmt.Fprintf(&subcommands, "- [`glab %s`](%s/_index.md)\n", cmdC.Name(), cmdC.Name())
		}
	}
	return 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()
	writeGeneratedHeader(buf, "'`"+name+"`'", "AI Coding")

	// 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)
	writeGeneratedHeader(buf, "GitLab CLI (glab)", "Create")

	// 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\n")
	buf.WriteString("[Authenticate with GitLab](authentication.md).\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
	}

	// Point at the commands landing page, which holds the full list
	buf.WriteString("\n## Commands\n\n")
	buf.WriteString("For the list of top-level `glab` commands, see [Commands](commands.md).\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
}
