// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.

package main

import (
	"context"
	"io"
  "os"
  "flag"
  "log"
	"log/slog"
_	"errors"
	"fmt"
_ "gopkg.in/yaml.v3"
  "decl/internal/resource"
  "decl/internal/source"
  "decl/internal/target"
)

const (
	FormatYaml = "yaml"
	FormatJson = "json"
)

var (
	version string
	commit	string
	date		string
)

var GlobalOformat *string
var GlobalOutput string
var GlobalQuiet *bool

var ImportMerge *bool
var ImportResource *string


var ctx context.Context = context.Background()

type RunCommand func(cmd *flag.FlagSet, output io.Writer) error

type SubCommand struct {
	Name string
	Run RunCommand
}

var jxSubCommands = []SubCommand {
	{
		Name: "diff",
		Run: DiffSubCommand,
	},
	{
		Name: "apply",
		Run: ApplySubCommand,
	},
	{
		Name: "import",
		Run: ImportSubCommand,
	},
}

func VersionUsage() {
	fmt.Println("jx")
	fmt.Printf("version: %s\n", version)
	fmt.Printf("commit: %s\n", commit)
	fmt.Printf("date: %s\n", date)
}

func LoggerConfig() {
	var programLevel = new(slog.LevelVar)
	logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel}))
	slog.SetDefault(logger)
	if debugLogging,ok := os.LookupEnv("DECL_DEBUG"); ok && debugLogging != "" {
		programLevel.Set(slog.LevelDebug)
	} else {
		programLevel.Set(slog.LevelError)
	}
}

func LoadSourceURI(uri string) []*resource.Document {
	slog.Info("loading ", "uri", uri)
	if uri != "" {
		ds, err := source.SourceTypes.New(uri)
		if err != nil {
			log.Fatal(err)
		}
		extractDocuments, extractErr := ds.ExtractResources(nil)
		if extractErr != nil {
			log.Fatal(extractErr)
		}
		return extractDocuments
	}
	return []*resource.Document{ resource.NewDocument() }
}

func ImportSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
	ImportResource = cmd.String("resource", "", "(uri) Add a resource to the document.")
	ImportMerge = cmd.Bool("merge", false, "Merge resources into a single document.")
	e := cmd.Parse(os.Args[2:])
	if e != nil { // returns ErrHelp
		return e
	}

	merged := resource.NewDocument()
	documents := make([]*resource.Document, 0, 100)
	for _,source := range cmd.Args() {
		loaded :=  LoadSourceURI(source)
		if loaded != nil {
			documents = append(documents, loaded...)
		}
	}

/*
	switch *GlobalOformat {
	case FormatYaml:
		encoder = resource.NewYAMLEncoder(output)
	case FormatJson:
		encoder = resource.NewJSONEncoder(output)
	}
*/

	slog.Info("main.ImportResource", "args", os.Args, "output", GlobalOutput)
	outputTarget, err := target.TargetTypes.New(GlobalOutput)
	if err != nil {
		log.Fatal(err)
	}

	if len(documents) == 0 {
		documents = append(documents, resource.NewDocument())
	}

	for _,d := range documents {
		if d != nil {

			if *ImportResource != "" {
				slog.Info("ImportResource", "resource", ImportResource)
				if addResourceErr := d.AddResource(*ImportResource); addResourceErr != nil {
					log.Fatal(addResourceErr)
				}
			}

			if *GlobalQuiet {
				for _, dr := range d.Resources() {
					if _,e := output.Write([]byte(dr.Resource().URI())); e != nil {
						return e
					}
				}
			} else {
				if *ImportMerge {
					merged.ResourceDecls = append(merged.ResourceDecls, d.ResourceDecls...)
				} else {
					slog.Info("main.ImportResource", "outputTarget", outputTarget, "type", outputTarget.Type())
					if outputErr := outputTarget.EmitResources([]*resource.Document{d}, nil); outputErr != nil {
						return outputErr
					}
				}
			}
		}
	}
	if *ImportMerge {
		if outputErr := outputTarget.EmitResources([]*resource.Document{merged}, nil); outputErr != nil {
			return outputErr
		}
	}
	return err
}

func ApplySubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
	if e := cmd.Parse(os.Args[2:]); e != nil {
		return e
	}
	var encoder resource.Encoder
	documents := make([]*resource.Document, 0, 100)
	for _,source := range cmd.Args() {
		loaded :=  LoadSourceURI(source)
		if loaded != nil {
			documents = append(documents, loaded...)
		}
	}

	slog.Info("main.Apply()", "documents", documents)
	for _,d := range documents {
		slog.Info("main.Appl()", "doc", d)
		if e := d.Apply(); e != nil {
			return e
		}

		switch *GlobalOformat {
		case FormatYaml:
			encoder = resource.NewYAMLEncoder(output)
		case FormatJson:
			encoder = resource.NewJSONEncoder(output)
		}
		if *GlobalQuiet {
			for _, dr := range d.Resources() {
				if _,e := output.Write([]byte(dr.Resource().URI())); e != nil {
					return e
				}
			}
		} else {
			if documentGenerateErr := encoder.Encode(d); documentGenerateErr != nil {
				return documentGenerateErr
			}
		}
	}
	return err
}

func DiffSubCommand(cmd *flag.FlagSet, output io.Writer) (err error) {
	if e := cmd.Parse(os.Args[2:]); e != nil {
		return e
	}
	leftSource := cmd.Arg(0)
	rightSource := cmd.Arg(1)
	leftDocuments := make([]*resource.Document, 0, 100)
	rightDocuments := make([]*resource.Document, 0, 100)

	slog.Info("jx diff subcommand", "left", leftSource, "right", rightSource, "flagset", cmd)

	if rightSource == "" {
		rightDocuments = append(rightDocuments, LoadSourceURI(leftSource)...)
		slog.Info("jx diff clone", "docs", rightDocuments)
		for i, doc := range rightDocuments {
			if doc != nil {
				leftDocuments = append(leftDocuments, doc.Clone())
				for _,resourceDeclaration := range leftDocuments[i].Resources() {
					if _, e := resourceDeclaration.Resource().Read(ctx); e != nil {
						slog.Info("jx diff ", "err", e)
						//return e
					}
				}
			}
		}
	} else {
		leftDocuments = append(leftDocuments, LoadSourceURI(leftSource)...)
		rightDocuments = append(rightDocuments, LoadSourceURI(rightSource)...)
	}

	slog.Info("jx diff ", "right", rightDocuments, "left", leftDocuments)
	index := 0
	for {
		if index >= len(rightDocuments) && index >= len(leftDocuments) {
			break
		}
		if index >= len(rightDocuments) {
			if _,e := leftDocuments[index].Diff(resource.NewDocument(), output); e != nil {
				return e
			}
			index++
			continue
		}
		if index >= len(leftDocuments) {
			if _,e := resource.NewDocument().Diff(rightDocuments[index], output); e != nil {
				return e
			}
			index++
			continue
		}
		if _,e := leftDocuments[index].Diff(rightDocuments[index], output); e != nil {
			return e
		}
		index++
	}
	return err
}

func main() {

	LoggerConfig()

	if len(os.Args) < 2 {
		fmt.Println("expected subcommands: diff, apply, import")
		os.Exit(1)
	}

	for _,subCmd := range jxSubCommands {
		cmdFlagSet := flag.NewFlagSet(subCmd.Name, flag.ExitOnError)
		GlobalOformat = cmdFlagSet.String("oformat", "yaml", "Output serialization format")
		cmdFlagSet.StringVar(&GlobalOutput, "output", "-", "Output target (default stdout)")
		cmdFlagSet.StringVar(&GlobalOutput, "o", "-", "Output target (default stdout)")
		GlobalQuiet = cmdFlagSet.Bool("quiet", false, "Generate terse output.")

		switch subCmd.Name {
		case "diff":
			cmdFlagSet.Usage = func() {
				fmt.Println("jx diff source [source2]")
				cmdFlagSet.PrintDefaults()
				VersionUsage()
			}
		case "apply":
			cmdFlagSet.Usage = func() {
				fmt.Println("jx diff source [source2]")
				cmdFlagSet.PrintDefaults()
				VersionUsage()
			}
		case "import":
			cmdFlagSet.Usage = func() {
				fmt.Println("jx import source...")
				cmdFlagSet.PrintDefaults()
				VersionUsage()
			}
		}
		if os.Args[1] == subCmd.Name {
			if e := subCmd.Run(cmdFlagSet, os.Stdout); e != nil {
				log.Fatal(e)
			}
			return
		}
	}
	flag.PrintDefaults()
	VersionUsage()
	os.Exit(1)
}
