package main

import (
	"bytes"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"local/pt-todo-server/pttodo"
	"log"
	"os"
	"os/exec"
	"path"
	"sort"
	"strings"
	"syscall"

	"gopkg.in/yaml.v2"
)

const (
	DUMP_ALL       = "all"
	DUMP_TODO      = "todo"
	DUMP_SCHEDULED = "scheduled"
	DUMP_DONE      = "done"
)

type config struct {
	targets []string
	mergeme string
	root    string
	tags    []string
	search  string
	edit    bool
	dry     bool
}

func main() {
	if err := _main(); err != nil {
		panic(err)
	}
}

func _main() error {
	config := getConfig()
	if config.mergeme != "" {
		if err := merge(config.dry, config.targets[0], config.mergeme); err != nil {
			return err
		}
	}
	if config.edit {
		if err := edit(config.dry, config.targets); err != nil {
			return err
		}
	}
	return dump(config.dry, os.Stdout, config.targets, config.tags, config.search, config.root)
}

func getConfig() config {
	defaultFilepath, ok := os.LookupEnv("PTTODO_FILE")
	if !ok {
		defaultFilepath = "./todo.yaml"
	}

	var config config
	var target string
	flag.StringVar(&target, "f", defaultFilepath, "($PTTODO_FILE) path to yaml file or dir (starting with root then alphabetical for dir)")
	flag.StringVar(&config.mergeme, "g", "", "path to yaml file to merge into -f (modifies f)")
	flag.StringVar(&config.root, "root", DUMP_TODO, "path to pretty print ("+fmt.Sprint([]string{DUMP_ALL, DUMP_TODO, DUMP_SCHEDULED, DUMP_DONE})+")")
	var tagss string
	flag.StringVar(&tagss, "tags", "", "csv of all tags to find, -tag to invert")
	flag.StringVar(&config.search, "search", "", "fts case insensitive")
	flag.BoolVar(&config.edit, "e", false, "edit file")
	flag.BoolVar(&config.dry, "dry", false, "dry run")
	flag.Parse()

	config.tags = strings.Split(tagss, ",")
	config.targets = []string{target}
	if stat, err := os.Stat(target); err == nil && stat.IsDir() {
		config.targets, _ = listDir(target)
	}

	return config
}

func verifyRoot(root pttodo.Root) error {
	f, err := ioutil.TempFile(os.TempDir(), "tmp")
	if err != nil {
		return err
	}
	f.Close()
	tempFile := f.Name()
	b, err := yaml.Marshal(root)
	if err != nil {
		return err
	}
	if err := ioutil.WriteFile(tempFile, b, os.ModePerm); err != nil {
		return err
	}
	defer os.Remove(tempFile)
	return verifyFile(tempFile)
}

func verifyFile(path string) error {
	if err := dump(true, io.Discard, []string{path}, nil, "", DUMP_ALL); err != nil {
		return fmt.Errorf("failed verifying file %s: %w", path, err)
	}
	return nil
}

func edit(dry bool, filepaths []string) error {
	tempDir, err := ioutil.TempDir(os.TempDir(), "edit-pttodo-*")
	if err != nil {
		return err
	}
	originals := map[string]pttodo.Root{}
	for _, target := range filepaths {
		var original pttodo.Root
		if b, err := ioutil.ReadFile(target); err != nil && !os.IsNotExist(err) {
			return err
		} else if err := yaml.Unmarshal(b, &original); err != nil {
			return err
		} else if c, err := yaml.Marshal(original.Todo); err != nil {
			return err
		} else if err := ioutil.WriteFile(path.Join(tempDir, path.Base(target)), c, os.ModePerm); err != nil {
			return err
		}
		originals[target] = original
	}
	if err := vimd(tempDir); err != nil {
		return err
	}
	for _, target := range filepaths {
		for {
			err := func() error {
				var todos []pttodo.Todo
				if b, err := ioutil.ReadFile(path.Join(tempDir, path.Base(target))); err != nil {
					return err
				} else if err := yaml.Unmarshal(b, &todos); err != nil {
					return err
				}
				return nil
			}()
			if err == nil {
				break
			}
			log.Printf("%s, press <Enter> to resume editing", err)
			b := make([]byte, 1)
			if _, err := os.Stdin.Read(b); err != nil {
				return err
			}
			if err := vimd(tempDir); err != nil {
				return err
			}
		}
	}
	if dry {
		return nil
	}
	for _, target := range filepaths {
		b, err := ioutil.ReadFile(path.Join(tempDir, path.Base(target)))
		if err != nil {
			return err
		}
		var newTodos []pttodo.Todo
		if err := yaml.Unmarshal(b, &newTodos); err != nil {
			return err
		}
		original := originals[target]
		original.Todo = newTodos
		original.AutoMove()
		c, err := yaml.Marshal(original)
		if err != nil {
			return err
		}
		if err := ioutil.WriteFile(target, c, os.ModePerm); err != nil {
			return err
		}
	}
	return nil
}

func _edit(dry bool, filepaths []string) error {
	tempDir, err := ioutil.TempDir(os.TempDir(), "edit-pttodo")
	if err != nil {
		return err
	}
	cpOne := func(filepath string) error {
		f, err := os.Create(path.Join(tempDir, path.Base(filepath)))
		if err != nil {
			return err
		}
		if _, err := os.Stat(filepath); err == nil {
			g, err := os.Open(filepath)
			if err != nil {
				return err
			}
			if _, err := io.Copy(f, g); err != nil {
				return err
			}
			g.Close()
		}
		f.Close()
		return nil
	}
	cp := func() error {
		for _, filepath := range filepaths {
			if err := cpOne(filepath); err != nil {
				return err
			}
		}
		return nil
	}
	vi := func() error {
		return vimd(tempDir)
	}
	verifyOne := func(tempFile string) error {
		for {
			err := verifyFile(tempFile)
			if err == nil {
				break
			}
			log.Printf("%v, press <Enter> to resume editing", err)
			b := make([]byte, 1)
			if _, err := os.Stdin.Read(b); err != nil {
				break
			}
			if err := vi(); err != nil {
				return err
			}
		}
		return verifyFile(tempFile)
	}
	verify := func() error {
		for _, filepath := range filepaths {
			tempFile := path.Join(tempDir, path.Base(filepath))
			if err := verifyOne(tempFile); err != nil {
				return err
			}
		}
		return nil
	}
	saveOne := func(filepath string) error {
		tempFile := path.Join(tempDir, path.Base(filepath))
		var rootTemp, rootOld pttodo.Root
		if a, err := ioutil.ReadFile(tempFile); err != nil {
			return err
		} else if err := yaml.Unmarshal(a, &rootTemp); err != nil {
			return err
		} else if b, err := ioutil.ReadFile(filepath); err != nil {
			return err
		} else if err := yaml.Unmarshal(b, &rootOld); err != nil {
			return err
		} else if rootTemp.Equals(rootOld) {
			//log.Printf("no changes to %s", filepath)
			return nil
		}
		if dry {
			log.Printf("would've saved %s as %s", tempFile, filepath)
			return nil
		}
		return os.Rename(tempFile, filepath)
	}
	save := func() error {
		for _, filepath := range filepaths {
			if err := saveOne(filepath); err != nil {
				return err
			}
		}
		return nil
	}

	for _, foo := range []func() error{cp, vi, verify, save} {
		if err := foo(); err != nil {
			return err
		}
	}
	if !dry {
		os.RemoveAll(tempDir)
	}

	return nil
}

func vimd(d string) error {
	bin := "vim"
	editorbin, err := exec.LookPath(bin)
	if err != nil {
		editorbin, err = exec.LookPath("vi")
	}
	if err != nil {
		return err
	}
	args := []string{editorbin, "-p"}
	files, err := listDir(d)
	if err != nil {
		return err
	}
	args = append(args, files...)
	cpid, err := syscall.ForkExec(
		editorbin,
		args,
		&syscall.ProcAttr{
			Dir:   d,
			Env:   os.Environ(),
			Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
			Sys:   nil,
		},
	)
	if err != nil {
		return err
	}
	proc, err := os.FindProcess(cpid)
	if err != nil {
		return err
	}
	state, err := proc.Wait()
	if err != nil {
		return err
	}
	if exitCode := state.ExitCode(); exitCode != 0 {
		return fmt.Errorf("bad exit code on vim: %d, state: %+v", exitCode, state)
	}
	return nil
}

func merge(dry bool, filepath string, mergeTargetFilePath string) error {
	baseReader, err := filePathReader(filepath)
	if err != nil {
		return err
	}
	baseB, err := ioutil.ReadAll(baseReader)
	if err != nil {
		return err
	}

	mergingReader, err := filePathReader(mergeTargetFilePath)
	if err != nil {
		return err
	}
	mergingB, err := ioutil.ReadAll(mergingReader)
	if err != nil {
		return err
	}

	var base, merging pttodo.Root
	if err := yaml.Unmarshal(baseB, &base); err != nil {
		return err
	}
	if err := yaml.Unmarshal(mergingB, &merging); err != nil {
		return err
	}

	base.MergeIn(merging)

	if err := verifyRoot(base); err != nil {
		return err
	}
	tmppath, err := marshalRootToTempFile(base)
	if err != nil {
		return err
	}
	if dry {
		log.Printf("would've moved %s to %s when adding %s", tmppath, filepath, mergeTargetFilePath)
		return nil
	}
	return os.Rename(tmppath, filepath)
}

func marshalRootToTempFile(root pttodo.Root) (string, error) {
	f, err := ioutil.TempFile(os.TempDir(), "tmp")
	if err != nil {
		return "", err
	}
	f.Close()
	os.Remove(f.Name())
	b, err := yaml.Marshal(root)
	if err != nil {
		return "", err
	}
	filepath := f.Name() + ".yaml"
	err = ioutil.WriteFile(filepath, b, os.ModePerm)
	return filepath, err
}

func dump(dry bool, writer io.Writer, filepaths []string, tags []string, search, rootDisplay string) error {
	var root pttodo.Root

	for _, filepath := range filepaths {
		reader, err := filePathReader(filepath)
		if err != nil {
			return err
		}

		b, err := ioutil.ReadAll(reader)
		if err != nil {
			return err
		}

		var root2, root2post pttodo.Root
		if err := yaml.Unmarshal(b, &root2); err != nil {
			return err
		}
		if err := yaml.Unmarshal(b, &root2post); err != nil {
			return err
		}

		root2.MoveScheduledToTodo()

		if !dry {
			if !root2.Equals(root2post) {
				log.Printf("refreshing %s", filepath)
				b3, err := yaml.Marshal(root2)
				if err != nil {
					return err
				}
				if err := os.WriteFile(filepath, b3, os.ModePerm); err != nil {
					return err
				}
			} else {
				//log.Printf("not refreshing %s", filepath)
			}
		}

		root.MergeIn(root2)
	}

	root.MoveScheduledToTodo()

	var v interface{} = root
	switch rootDisplay {
	case DUMP_ALL:
	case DUMP_TODO:
		v = root.Todo
	case DUMP_SCHEDULED:
		v = root.Scheduled
	case DUMP_DONE:
		v = root.Done
	}
	if todos, ok := v.([]pttodo.Todo); ok {
		if len(tags) > 0 {
			result := make([]pttodo.Todo, 0, len(todos))
			for _, todo := range todos {
				skip := false
				for _, tag := range tags {
					positiveTag := strings.TrimLeft(tag, "-")
					hasTag := strings.Contains(todo.Tags, positiveTag)
					wantToHaveTag := !strings.HasPrefix(tag, "-")
					skip = skip || !(hasTag == wantToHaveTag)
				}
				if !skip {
					result = append(result, todo)
				}
			}
			todos = result
		}
		if len(search) > 0 {
			result := make([]pttodo.Todo, 0, len(todos))
			for _, todo := range todos {
				if strings.Contains(strings.ToLower(fmt.Sprint(todo)), strings.ToLower(search)) {
					result = append(result, todo)
				}
			}
			todos = result
		}
		v = todos
	}

	b2, err := yaml.Marshal(v)
	if err != nil {
		return err
	}
	fmt.Fprintf(writer, "%s\n", b2)
	return nil
}

func filePathReader(path string) (io.Reader, error) {
	var reader io.Reader
	if path == "-" {
		reader = os.Stdin
	} else {
		b, err := ioutil.ReadFile(path)
		if err != nil {
			return nil, err
		}
		reader = bytes.NewReader(b)
	}
	return reader, nil
}

func listDir(dname string) ([]string, error) {
	entries, err := os.ReadDir(dname)
	if err != nil {
		return nil, err
	}
	paths := make([]string, 0, len(entries))
	for i := range entries {
		if entries[i].IsDir() {
			continue
		}
		paths = append(paths, path.Join(dname, entries[i].Name()))
	}
	sort.Slice(paths, func(i, j int) bool {
		if path.Base(paths[i]) == "root.yaml" {
			return true
		}
		return paths[i] < paths[j]
	})
	return paths, nil
}
