package main

//go:generate sh -c "cd _examples && go generate ./..."

import (
	"bytes"
	_ "embed"
	"errors"
	"fmt"
	"html/template"
	"io"
	"io/fs"
	"log"
	"os"
	"path/filepath"

	"github.com/urfave/cli/v2"

	"github.com/99designs/gqlgen/api"
	"github.com/99designs/gqlgen/codegen/config"
	"github.com/99designs/gqlgen/graphql"
	"github.com/99designs/gqlgen/internal/code"
	"github.com/99designs/gqlgen/plugin/servergen"
)

//go:embed init-templates/schema.graphqls
var schemaFileContent string

//go:embed init-templates/gqlgen.yml.gotmpl
var configFileTemplate string

func getConfigFileContent(pkgName string) string {
	var buf bytes.Buffer
	if err := template.Must(template.New("gqlgen.yml").Parse(configFileTemplate)).Execute(&buf, pkgName); err != nil {
		panic(err)
	}
	return buf.String()
}

func fileExists(filename string) bool {
	_, err := os.Stat(filename)
	return !errors.Is(err, fs.ErrNotExist)
}

// see Go source code:
// https://github.com/golang/go/blob/f57ebed35132d02e5cf016f324853217fb545e91/src/cmd/go/internal/modload/init.go#L1283
func findModuleRoot(dir string) (roots string) {
	if dir == "" {
		panic("dir not set")
	}
	dir = filepath.Clean(dir)

	// Look for enclosing go.mod.
	for {
		if fi, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil && !fi.IsDir() {
			return dir
		}
		d := filepath.Dir(dir)
		if d == dir { // the parent of the root is itself, so we can go no further
			break
		}
		dir = d
	}
	return ""
}

func initFile(filename, contents string) error {
	if err := os.MkdirAll(filepath.Dir(filename), 0o755); err != nil {
		return fmt.Errorf("unable to create directory for file '%s': %w", filename, err)
	}
	if err := os.WriteFile(filename, []byte(contents), 0o644); err != nil {
		return fmt.Errorf("unable to write file '%s': %w", filename, err)
	}

	return nil
}

var initCmd = &cli.Command{
	Name:  "init",
	Usage: "create a new gqlgen project",
	Flags: []cli.Flag{
		&cli.BoolFlag{Name: "verbose, v", Usage: "show logs"},
		&cli.StringFlag{Name: "config, c", Usage: "the config filename", Value: "gqlgen.yml"},
		&cli.StringFlag{
			Name:  "server",
			Usage: "where to write the server stub to",
			Value: "server.go",
		},
		&cli.StringFlag{
			Name:  "schema",
			Usage: "where to write the schema stub to",
			Value: "graph/schema.graphqls",
		},
	},
	Action: func(ctx *cli.Context) error {
		configFilename := ctx.String("config")
		serverFilename := ctx.String("server")
		schemaFilename := ctx.String("schema")

		cwd, err := os.Getwd()
		if err != nil {
			return fmt.Errorf("unable to determine current directory: %w", err)
		}
		pkgName := code.ImportPathForDir(cwd)
		if pkgName == "" {
			return errors.New(
				"unable to determine import path for current directory, you probably need to run 'go mod init' first",
			)
		}
		modRoot := findModuleRoot(cwd)
		if modRoot == "" {
			return cli.Exit("go.mod is missing. Please, do 'go mod init' first\n", 1)
		}

		// check schema and config don't already exist
		for _, filename := range []string{configFilename, schemaFilename, serverFilename} {
			if fileExists(filename) {
				return fmt.Errorf("%s already exists", filename)
			}
		}
		_, err = config.LoadConfigFromDefaultLocations()
		if err == nil {
			return cli.Exit("gqlgen.yml already exists in a parent directory\n", 1)
		}

		// create config
		fmt.Println("Creating", configFilename)
		if err := initFile(configFilename, getConfigFileContent(pkgName)); err != nil {
			return cli.Exit(err.Error()+"\n", 1)
		}

		// create schema
		fmt.Println("Creating", schemaFilename)

		if err := initFile(schemaFilename, schemaFileContent); err != nil {
			return cli.Exit(err.Error()+"\n", 1)
		}

		// create the package directory with a temporary file so that go recognises it as a package
		// and autobinding doesn't error out
		tmpPackageNameFile := "graph/model/_tmp_gqlgen_init.go"
		if err := initFile(tmpPackageNameFile, "package model"); err != nil {
			return cli.Exit(err.Error()+"\n", 1)
		}
		defer os.Remove(tmpPackageNameFile)

		var cfg *config.Config
		if cfg, err = config.LoadConfig(configFilename); err != nil {
			panic(err)
		}

		fmt.Println("Creating", serverFilename)
		fmt.Println("Generating...")
		if err := api.Generate(cfg, api.AddPlugin(servergen.New(serverFilename))); err != nil {
			return err
		}

		fmt.Printf("\nExec \"go run ./%s\" to start GraphQL server\n", serverFilename)
		return nil
	},
}

var generateCmd = &cli.Command{
	Name:  "generate",
	Usage: "generate a graphql server based on schema",
	Flags: []cli.Flag{
		&cli.BoolFlag{Name: "verbose, v", Usage: "show logs"},
		&cli.StringFlag{Name: "config, c", Usage: "the config filename"},
	},
	Action: func(ctx *cli.Context) error {
		var cfg *config.Config
		var err error
		if configFilename := ctx.String("config"); configFilename != "" {
			cfg, err = config.LoadConfig(configFilename)
			if err != nil {
				return err
			}
		} else {
			cfg, err = config.LoadConfigFromDefaultLocations()
			if errors.Is(err, fs.ErrNotExist) {
				cfg, err = config.LoadDefaultConfig()
			}

			if err != nil {
				return err
			}
		}

		return api.Generate(cfg)
	},
}

var versionCmd = &cli.Command{
	Name:  "version",
	Usage: "print the version string",
	Action: func(ctx *cli.Context) error {
		fmt.Println(graphql.Version)
		return nil
	},
}

func main() {
	app := cli.NewApp()
	app.Name = "gqlgen"
	app.Usage = generateCmd.Usage
	app.Description = "This is a library for quickly creating strictly typed graphql servers in golang. See https://gqlgen.com/ for a getting started guide."
	app.HideVersion = true
	app.Flags = generateCmd.Flags
	app.Version = graphql.Version
	app.Before = func(context *cli.Context) error {
		if context.Bool("verbose") {
			log.SetFlags(0)
		} else {
			log.SetOutput(io.Discard)
		}
		return nil
	}

	app.Action = generateCmd.Action
	app.Commands = []*cli.Command{
		generateCmd,
		initCmd,
		versionCmd,
	}

	if err := app.Run(os.Args); err != nil {
		fmt.Fprint(os.Stderr, err.Error()+"\n")
		os.Exit(1)
	}
}
