package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"strings"

	"gopkg.in/yaml.v2"
)

func mergeIn(tgt, add map[any]any) {
mergeLoop:
	for k, v := range add {
		switch v := v.(type) {
		case map[any]any:
			if tgtV, ok := tgt[k]; ok {
				switch tgtV := tgtV.(type) {
				case map[any]any:
					mergeIn(tgtV, v)
					continue mergeLoop
				}
			}
		}

		tgt[k] = v
	}
}

func assemble(path string) (yamlBytes []byte, err error) {
	obj := map[any]any{}

	if Debug {
		log.Printf("assemble %q", path)
	}

	err = eachFragment(path, searchList, func(r io.Reader) (err error) {
		m := map[any]any{}
		err = yaml.NewDecoder(r).Decode(&m)
		if err != nil {
			return
		}

		mergeIn(obj, m)
		return
	})
	if err != nil {
		err = fmt.Errorf("failed to assemble %q: %w", path, err)
		return
	}

	yamlBytes, err = yaml.Marshal(obj)
	if err != nil {
		return
	}

	if Debug {
		log.Printf("assemble %q result:\n%s", path, yamlBytes)
	}

	return
}

func eachFragment(path string, searchList []FS, walk func(io.Reader) error) (err error) {
	var r io.ReadCloser

	for len(searchList) != 0 {
		fs := searchList[0]

		r, err = fs.Open(path + ".yaml")
		if os.IsNotExist(err) {
			searchList = searchList[1:]
			continue
		}

		if err != nil {
			return
		}

		// found and open
		break
	}

	if r == nil {
		err = fmt.Errorf("%s: %w", path, os.ErrNotExist)
		return
	}

	ba, err := io.ReadAll(r)
	r.Close()

	if err != nil {
		return
	}

	if Debug {
		log.Print("fragment:\n", string(ba))
	}

	in := bytes.NewBuffer(ba)

	for {
		var line string
		line, err = in.ReadString('\n')
		if err == io.EOF {
			break
		}
		if err != nil {
			return
		}

		line = strings.TrimSpace(line)
		if len(line) == 0 {
			continue
		}

		genCmd, found := strings.CutPrefix(line, "#!gen ")
		if found {
			cmdArgs := strings.Fields(genCmd)
			if Debug {
				log.Print("#!gen ", cmdArgs)
			}

			cmd := *dir + "/gen/" + cmdArgs[0]
			args := cmdArgs[1:]
			genOutput, err := exec.Command(cmd, args...).Output()
			if err != nil {
				return fmt.Errorf("gen %v: %w", cmdArgs, err)
			}
			walk(bytes.NewBuffer(genOutput))
			continue
		}

		includePath, found := strings.CutPrefix(line, "#!include ")
		if !found {
			continue
		}

		includePath = strings.TrimSpace(includePath)
		if Debug {
			log.Print("#!include ", includePath)
		}
		err = eachFragment(includePath, searchList, walk)
		if err != nil {
			return fmt.Errorf("include %q: %w", includePath, err)
		}
	}

	in = bytes.NewBuffer(ba)
	err = walk(in)
	return
}
