package main

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/spf13/cobra"

	"github.com/atombender/go-jsonschema/pkg/generator"
)

const (
	perm644 = 0o644
	perm755 = 0o755
)

var (
	verbose                   bool
	extraImports              bool
	onlyModels                bool
	defaultPackage            string
	defaultOutput             string
	schemaPackages            []string
	schemaOutputs             []string
	schemaRootTypes           []string
	capitalizations           []string
	resolveExtensions         []string
	yamlExtensions            []string
	tags                      []string
	structNameFromTitle       bool
	minSizedInts              bool
	minimalNames              bool
	disableReadOnlyValidation bool
	disableCustomTypesForMaps bool
	disableOmitEmpty          bool
	disableOmitZero           bool

	errFlagFormat = errors.New("flag must be in the format URI=PACKAGE")

	rootCmd = &cobra.Command{
		Use:   "go-jsonschema FILE ...",
		Short: "Generates Go code from JSON Schema files.",
		Run: func(_ *cobra.Command, args []string) {
			if len(args) == 0 {
				abort("No arguments specified. Run with --help for usage.")
			}

			if defaultPackage == "" && len(schemaPackages) == 0 {
				abort("Package name not specified.")
			}

			schemaPackageMap, err := stringSliceToStringMap(schemaPackages)
			if err != nil {
				abortWithErr(err)
			}

			schemaOutputMap, err := stringSliceToStringMap(schemaOutputs)
			if err != nil {
				abortWithErr(err)
			}

			schemaRootTypeMap, err := stringSliceToStringMap(schemaRootTypes)
			if err != nil {
				abortWithErr(err)
			}

			cfg := generator.Config{
				Warner: func(message string) {
					logf("Warning: %s", message)
				},
				ExtraImports:              extraImports,
				Capitalizations:           capitalizations,
				DefaultOutputName:         defaultOutput,
				DefaultPackageName:        defaultPackage,
				SchemaMappings:            []generator.SchemaMapping{},
				ResolveExtensions:         resolveExtensions,
				YAMLExtensions:            yamlExtensions,
				StructNameFromTitle:       structNameFromTitle,
				Tags:                      tags,
				OnlyModels:                onlyModels,
				MinSizedInts:              minSizedInts,
				MinimalNames:              minimalNames,
				DisableReadOnlyValidation: disableReadOnlyValidation,
				DisableCustomTypesForMaps: disableCustomTypesForMaps,
				DisableOmitEmpty:          disableOmitEmpty,
				DisableOmitZero:           disableOmitZero,
			}

			for _, id := range allKeys(schemaPackageMap, schemaOutputMap, schemaRootTypeMap) {
				mapping := generator.SchemaMapping{SchemaID: id}
				if s, ok := schemaPackageMap[id]; ok {
					mapping.PackageName = s
				} else {
					mapping.PackageName = defaultPackage
				}

				if s, ok := schemaOutputMap[id]; ok {
					mapping.OutputName = s
				} else {
					mapping.OutputName = defaultOutput
				}

				if s, ok := schemaRootTypeMap[id]; ok {
					mapping.RootType = s
				}

				cfg.SchemaMappings = append(cfg.SchemaMappings, mapping)
			}

			generator, err := generator.New(cfg)
			if err != nil {
				abortWithErr(err)
			}

			for _, fileName := range args {
				verboseLogf("Loading %s", fileName)

				if err = generator.DoFile(fileName); err != nil {
					abortWithErr(err)
				}
			}

			sources, err := generator.Sources()
			if err != nil {
				abortWithErr(err)
			}

			for fileName, source := range sources {
				if fileName != "-" {
					verboseLogf("Writing %s", fileName)
				}

				if fileName == "-" {
					if _, err = os.Stdout.Write(source); err != nil {
						abortWithErr(err)
					}
				} else {
					if err := os.MkdirAll(filepath.Dir(fileName), perm755); err != nil {
						abortWithErr(err)
					}

					w, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm644)
					if err != nil {
						abortWithErr(err)
					}

					if _, err := w.Write(source); err != nil {
						abortWithErr(err)
					}

					if err := w.Close(); err != nil {
						abortWithErr(err)
					}
				}
			}

			os.Exit(0)
		},
	}
)

func main() {
	rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false,
		"Verbose output")
	rootCmd.PersistentFlags().BoolVarP(&extraImports, "extra-imports", "e", false,
		"Allow extra imports (non standard library)")
	rootCmd.PersistentFlags().BoolVar(&onlyModels, "only-models", false,
		"Generate only models (no unmarshal methods, no validation)")
	rootCmd.PersistentFlags().StringVarP(&defaultPackage, "package", "p", "",
		`Default name of package to declare Go files under, unless overridden with
--schema-package`)
	rootCmd.PersistentFlags().StringVarP(&defaultOutput, "output", "o", "-",
		"File to write (- for standard output)")
	rootCmd.PersistentFlags().StringSliceVar(&schemaPackages, "schema-package", nil,
		`Name of package to declare Go files for a specific schema ID under;
must be in the format URI=PACKAGE.`)
	rootCmd.PersistentFlags().StringSliceVar(&schemaOutputs, "schema-output", nil,
		`File to write (- for standard output) a specific schema ID to;
must be in the format URI=FILENAME.`)
	rootCmd.PersistentFlags().StringSliceVar(&schemaRootTypes, "schema-root-type", nil,
		`Override name to use for the root type of a specific schema ID;
must be in the format URI=TYPE. By default, it is derived from the file name.`)
	rootCmd.PersistentFlags().StringSliceVar(&capitalizations, "capitalization", nil,
		`Specify a preferred Go capitalization for a string. For example, by default a field
named 'id' becomes 'Id'. With --capitalization ID, it will be generated as 'ID'.`)
	rootCmd.PersistentFlags().StringSliceVar(&resolveExtensions, "resolve-extension", nil,
		`Add a file extension that is used to resolve schema names, e.g. {"$ref": "./foo"} will
also look for foo.json if --resolve-extension json is provided.`)
	rootCmd.PersistentFlags().StringSliceVar(&yamlExtensions, "yaml-extension", []string{".yml", ".yaml"},
		`Add a file extension that should be recognized as YAML. Default are .yml, .yaml.`)
	rootCmd.PersistentFlags().BoolVarP(&structNameFromTitle, "struct-name-from-title", "t", false,
		"Use the schema title as the generated struct name")
	rootCmd.PersistentFlags().StringSliceVar(&tags, "tags", []string{"json", "yaml", "mapstructure"},
		`Specify which struct tags to generate. Defaults are json, yaml, mapstructure`)
	rootCmd.PersistentFlags().BoolVar(&minSizedInts, "min-sized-ints", false,
		"Uses sized int and uint values based on the min and max values for the field")
	rootCmd.PersistentFlags().BoolVar(&disableReadOnlyValidation, "disable-readonly-validation", false,
		"Do not include validation of readonly fields")
	rootCmd.PersistentFlags().BoolVar(&minimalNames, "minimal-names", false,
		"Uses the shortest possible names")
	rootCmd.PersistentFlags().BoolVar(&disableCustomTypesForMaps, "disable-custom-types-for-maps", false,
		"Do not generate custom types when generating maps")
	rootCmd.PersistentFlags().BoolVar(&disableOmitEmpty, "disable-omitempty", false,
		"disable the addition of omitempty tag values")
	rootCmd.PersistentFlags().BoolVar(&disableOmitZero, "disable-omitzero", false,
		"disable the addition of omitzero tag values")

	abortWithErr(rootCmd.Execute())
}

func abortWithErr(err error) {
	if err != nil {
		abort(err.Error())
	}
}

func abort(message string) {
	logf("Failed: %s", message)
	os.Exit(1)
}

func stringSliceToStringMap(s []string) (map[string]string, error) {
	result := make(map[string]string, len(s))

	for _, p := range s {
		i := strings.IndexRune(p, '=')
		if i == -1 {
			return nil, fmt.Errorf("%w: %q", errFlagFormat, p)
		}

		result[p[0:i]] = p[i+1:]
	}

	return result, nil
}

func allKeys(in ...map[string]string) []string {
	type dummy struct{}

	keySet := map[string]dummy{}

	for _, m := range in {
		for k := range m {
			keySet[k] = dummy{}
		}
	}

	result := make([]string, 0, len(keySet))

	for k := range keySet {
		result = append(result, k)
	}

	return result
}

func logf(format string, args ...any) {
	fmt.Fprint(os.Stderr, "go-jsonschema: ")
	fmt.Fprintf(os.Stderr, format, args...)
	fmt.Fprint(os.Stderr, "\n")
}

func verboseLogf(format string, args ...any) {
	if verbose {
		logf(format, args...)
	}
}
