// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2016 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

// Gitea (git with a cup of tea) is a painless self-hosted Git Service.
package main // import "code.gitea.io/gitea"

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	"code.gitea.io/gitea/cmd"
	"code.gitea.io/gitea/cmd/forgejo"
	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/setting"

	// register supported doc types
	_ "code.gitea.io/gitea/modules/markup/asciicast"
	_ "code.gitea.io/gitea/modules/markup/console"
	_ "code.gitea.io/gitea/modules/markup/csv"
	_ "code.gitea.io/gitea/modules/markup/markdown"
	_ "code.gitea.io/gitea/modules/markup/orgmode"

	"github.com/urfave/cli"
)

var (
	// Version holds the current Gitea version
	Version = "development"
	// Tags holds the build tags used
	Tags = ""
	// MakeVersion holds the current Make version if built with make
	MakeVersion = ""
)

var ForgejoVersion = "1.0.0"

func init() {
	setting.AppVer = Version
	setting.ForgejoVersion = ForgejoVersion
	setting.AppBuiltWith = formatBuiltWith()
	setting.AppStartTime = time.Now().UTC()
}

// cmdHelp is our own help subcommand with more information
// test cases:
// ./gitea help
// ./gitea -h
// ./gitea web help
// ./gitea web -h (due to cli lib limitation, this won't call our cmdHelp, so no extra info)
// ./gitea admin
// ./gitea admin help
// ./gitea admin auth help
// ./gitea -c /tmp/app.ini -h
// ./gitea -c /tmp/app.ini help
// ./gitea help -c /tmp/app.ini
// GITEA_WORK_DIR=/tmp ./gitea help
// GITEA_WORK_DIR=/tmp ./gitea help --work-path /tmp/other
// GITEA_WORK_DIR=/tmp ./gitea help --config /tmp/app-other.ini
var cmdHelp = cli.Command{
	Name:      "help",
	Aliases:   []string{"h"},
	Usage:     "Shows a list of commands or help for one command",
	ArgsUsage: "[command]",
	Action: func(c *cli.Context) (err error) {
		args := c.Args()
		if args.Present() {
			err = cli.ShowCommandHelp(c, args.First())
		} else {
			err = cli.ShowAppHelp(c)
		}
		_, _ = fmt.Fprintf(c.App.Writer, `
DEFAULT CONFIGURATION:
   AppPath:    %s
   WorkPath:   %s
   CustomPath: %s
   ConfigFile: %s

`, setting.AppPath, setting.AppWorkPath, setting.CustomPath, setting.CustomConf)
		return err
	},
}

func forgejoEnv() {
	for _, k := range []string{"CUSTOM", "WORK_DIR"} {
		if v, ok := os.LookupEnv("FORGEJO_" + k); ok {
			os.Setenv("GITEA_"+k, v)
		}
	}
}

func main() {
	path, err := os.Executable()
	if err != nil {
		panic(err)
	}
	executable := filepath.Base(path)

	var subCmds []cli.Command

	//
	// If the executable is forgejo-cli, provide a Forgejo specific CLI
	// that is NOT compatible with Gitea.
	//
	if executable == "forgejo-cli" {
		subCmds = []cli.Command{
			forgejo.CmdActions(context.Background()),
		}
	} else {
		//
		// Otherwise provide a Gitea compatible CLI which includes Forgejo
		// specific additions under the forgejo-cli subcommand. It allows
		// admins to migration from Gitea to Forgejo by replacing the gitea
		// binary and rename it to forgejo if they want.
		//
		subCmds = []cli.Command{
			forgejo.CmdForgejo(context.Background()),
			cmd.CmdActions,
		}
	}

	mainApp(subCmds...)
}

func mainApp(subCmds ...cli.Command) {
	forgejoEnv()
	app := cli.NewApp()
	app.Name = "Forgejo"
	app.Usage = "Beyond coding. We forge."
	app.Description = `By default, forgejo will start serving using the web-server with no
argument - which can alternatively be run by running the subcommand web.`
	app.Version = Version + formatBuiltWith()
	app.EnableBashCompletion = true

	// these sub-commands need to use config file
	subCmdWithIni := []cli.Command{
		cmd.CmdWeb,
		cmd.CmdServ,
		cmd.CmdHook,
		cmd.CmdDump,
		cmd.CmdAdmin,
		cmd.CmdMigrate,
		cmd.CmdKeys,
		cmd.CmdConvert,
		cmd.CmdDoctor,
		cmd.CmdManager,
		cmd.CmdEmbedded,
		cmd.CmdMigrateStorage,
		cmd.CmdDumpRepository,
		cmd.CmdRestoreRepository,
		cmdHelp, // TODO: the "help" sub-command was used to show the more information for "work path" and "custom config", in the future, it should avoid doing so
	}
	subCmdWithIni = append(subCmdWithIni, subCmds...)
	// these sub-commands do not need the config file, and they do not depend on any path or environment variable.
	subCmdStandalone := []cli.Command{
		cmd.CmdCert,
		cmd.CmdGenerate,
		cmd.CmdDocs,
	}

	// shared configuration flags, they are for global and for each sub-command at the same time
	// eg: such command is valid: "./gitea --config /tmp/app.ini web --config /tmp/app.ini", while it's discouraged indeed
	// keep in mind that the short flags like "-C", "-c" and "-w" are globally polluted, they can't be used for sub-commands anymore.
	globalFlags := []cli.Flag{
		cli.HelpFlag,
		cli.StringFlag{
			Name:  "custom-path, C",
			Usage: "Set custom path (defaults to '{WorkPath}/custom')",
		},
		cli.StringFlag{
			Name:  "config, c",
			Value: setting.CustomConf,
			Usage: "Set custom config file (defaults to '{WorkPath}/custom/conf/app.ini')",
		},
		cli.StringFlag{
			Name:  "work-path, w",
			Usage: "Set Gitea's working path (defaults to the Gitea's binary directory)",
		},
	}

	// Set the default to be equivalent to cmdWeb and add the default flags
	app.Flags = append(app.Flags, globalFlags...)
	app.Flags = append(app.Flags, cmd.CmdWeb.Flags...) // TODO: the web flags polluted the global flags, they are not really global flags
	app.Action = prepareWorkPathAndCustomConf(cmd.CmdWeb.Action)
	app.HideHelp = true // use our own help action to show helps (with more information like default config)
	app.Before = cmd.PrepareConsoleLoggerLevel(log.INFO)
	for i := range subCmdWithIni {
		prepareSubcommands(&subCmdWithIni[i], globalFlags)
	}
	app.Commands = append(app.Commands, subCmdWithIni...)
	app.Commands = append(app.Commands, subCmdStandalone...)

	cli.OsExiter = func(code int) {
		log.GetManager().Close()
		os.Exit(code)
	}
	app.ErrWriter = os.Stderr

	_ = cmd.RunMainApp(app, os.Args...) // all errors should have been handled by the RunMainApp
	log.GetManager().Close()
}

func prepareSubcommands(command *cli.Command, defaultFlags []cli.Flag) {
	command.Flags = append(command.Flags, defaultFlags...)
	command.Action = prepareWorkPathAndCustomConf(command.Action)
	command.HideHelp = true
	if command.Name != "help" {
		command.Subcommands = append(command.Subcommands, cmdHelp)
	}
	for i := range command.Subcommands {
		prepareSubcommands(&command.Subcommands[i], defaultFlags)
	}
}

// prepareWorkPathAndCustomConf wraps the Action to prepare the work path and custom config
// It can't use "Before", because each level's sub-command's Before will be called one by one, so the "init" would be done multiple times
func prepareWorkPathAndCustomConf(action any) func(ctx *cli.Context) error {
	return func(ctx *cli.Context) error {
		var args setting.ArgWorkPathAndCustomConf
		curCtx := ctx
		for curCtx != nil {
			if curCtx.IsSet("work-path") && args.WorkPath == "" {
				args.WorkPath = curCtx.String("work-path")
			}
			if curCtx.IsSet("custom-path") && args.CustomPath == "" {
				args.CustomPath = curCtx.String("custom-path")
			}
			if curCtx.IsSet("config") && args.CustomConf == "" {
				args.CustomConf = curCtx.String("config")
			}
			curCtx = curCtx.Parent()
		}
		setting.InitWorkPathAndCommonConfig(os.Getenv, args)
		if ctx.Bool("help") || action == nil {
			// the default behavior of "urfave/cli": "nil action" means "show help"
			return cmdHelp.Action.(func(ctx *cli.Context) error)(ctx)
		}
		return action.(func(*cli.Context) error)(ctx)
	}
}

func formatBuiltWith() string {
	version := runtime.Version()
	if len(MakeVersion) > 0 {
		version = MakeVersion + ", " + runtime.Version()
	}
	if len(Tags) == 0 {
		return " built with " + version
	}

	return " built with " + version + " : " + strings.ReplaceAll(Tags, " ", ", ")
}
