package main

import (
	"encoding/json"
	"flag"
	"io/fs"
	"log"
	"os"
	"path"
	"path/filepath"

	"github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing"
	yaml "gopkg.in/yaml.v2"

	"novit.tech/direktil/pkg/localconfig"

	"novit.tech/direktil/local-server/pkg/clustersconfig"
)

var Version = "dev"

var (
	Debug = false

	dir     = flag.String("in", ".", "Source directory")
	outPath = flag.String("out", "config.yaml", "Output file")

	base fs.FS

	src *clustersconfig.Config
	dst *localconfig.Config
)

func init() {
	flag.BoolVar(&Debug, "debug", Debug, "debug")
}

func loadSrc() {
	var err error
	src, err = clustersconfig.FromDir(read, assemble, listBase, listMerged)
	if err != nil {
		log.Fatal("failed to load config from dir: ", err)
	}
}

func main() {
	flag.Parse()

	log.SetFlags(log.Ltime | log.Lmicroseconds | log.Lshortfile)

	base = os.DirFS(*dir)
	searchList = append(searchList, fsFS{base})

	openIncludes()

	loadSrc()

	dst = &localconfig.Config{
		SSLConfig: src.SSLConfig,
	}

	// ----------------------------------------------------------------------
	for _, cluster := range src.Clusters {
		dst.Clusters = append(dst.Clusters, &localconfig.Cluster{
			Name:   cluster.Name,
			Addons: renderAddons(cluster),
		})
	}

	// ----------------------------------------------------------------------
	for _, host := range src.Hosts {
		log.Print("rendering host ", host.Name)
		ctx, err := newRenderContext(host, src)

		if err != nil {
			log.Fatal("failed to create render context for host ", host.Name, ": ", err)
		}

		macs := make([]string, 0)
		if host.MAC != "" {
			macs = append(macs, host.MAC)
		}

		ips := make([]string, 0)
		if len(host.IP) != 0 {
			ips = append(ips, host.IP)
		}
		ips = append(ips, host.IPs...)

		if ctx.Host.Versions["modules"] == "" {
			// default modules' version to kernel's version
			ctx.Host.Versions["modules"] = ctx.Host.Kernel
		}

		renderedHost := &localconfig.Host{
			Name: host.Name,

			ClusterName: ctx.Cluster.Name,

			Labels:      ctx.Labels,
			Annotations: ctx.Annotations,

			MACs: macs,
			IPs:  ips,

			IPXE: ctx.Host.IPXE, // TODO render

			Kernel:   ctx.Host.Kernel,
			Initrd:   ctx.Host.Initrd,
			Versions: ctx.Host.Versions,

			BootstrapConfig: ctx.BootstrapConfig(),
			Config:          ctx.Config(),
		}

		if host.Template {
			dst.HostTemplates = append(dst.HostTemplates, renderedHost)
		} else {
			dst.Hosts = append(dst.Hosts, renderedHost)
		}
	}

	// ----------------------------------------------------------------------
	out, err := os.Create(*outPath)
	if err != nil {
		log.Fatal("failed to create output: ", err)
	}

	defer out.Close()

	switch ext := path.Ext(*outPath); ext {
	case ".yaml":
		out.Write([]byte("# dkl-dir2config " + Version + "\n"))

		if err = yaml.NewEncoder(out).Encode(dst); err != nil {
			log.Fatal("failed to render output: ", err)
		}

	case ".json":
		if err = json.NewEncoder(out).Encode(dst); err != nil {
			log.Fatal("failed to render output: ", err)
		}

	default:
		log.Fatal("unknown output file extension: ", ext)
	}
}

func cfgPath(subPath string) string { return filepath.Join(*dir, subPath) }

func openIncludes() {
	includesFile, err := base.Open("includes.yaml")

	if os.IsNotExist(err) {
		return
	}
	if err != nil {
		log.Fatal("failed to open includes: ", err)
	}

	includes := make([]struct {
		Path   string
		Branch string
		Tag    string
	}, 0)

	err = yaml.NewDecoder(includesFile).Decode(&includes)
	if err != nil {
		log.Fatal("failed to parse includes: ", err)
	}

	for _, include := range includes {
		switch {
		case include.Branch != "" || include.Tag != "":
			p := cfgPath(include.Path) // FIXME parse git path to allow remote repos

			var rev plumbing.Revision

			switch {
			case include.Branch != "":
				log.Printf("opening include path %q as git, branch %q", p, include.Branch)
				rev = plumbing.Revision(plumbing.NewBranchReferenceName(include.Branch))

			case include.Tag != "":
				log.Printf("opening include path %q as git, tag %q", p, include.Branch)
				rev = plumbing.Revision(plumbing.NewTagReferenceName(include.Branch))
			}

			repo, err := git.PlainOpen(p)
			if err != nil {
				log.Fatal("failed to open: ", err)
			}

			revH, err := repo.ResolveRevision(rev)
			if err != nil {
				log.Fatalf("failed to resolve revision %s: %v", rev, err)
			}

			log.Print("  -> resolved to commit ", *revH)

			commit, err := repo.CommitObject(*revH)
			if err != nil {
				log.Fatal("failed to get commit object: ", err)
			}

			tree, err := commit.Tree()
			if err != nil {
				log.Fatal("failed to open git tree: ", err)
			}

			searchList = append(searchList, gitFS{tree})

		default:
			p := cfgPath(include.Path)
			log.Printf("opening include path %q as raw dir", p)

			searchList = append(searchList, fsFS{os.DirFS(p)})
		}
	}
}
