package main

import (
	"errors"
	"fmt"
	"os"
	"os/user"
	"path"
	"path/filepath"
	"time"

	"git.tilde.town/tildetown/town/email"
	tuser "git.tilde.town/tildetown/town/user"
	"github.com/AlecAivazis/survey/v2"
	"github.com/spf13/cobra"
	"gopkg.in/yaml.v3"
)

const (
	contribRequestPath = "/town/requests/contrib"
	submitOp           = "submit"
	delOp              = "del"
	updateOp           = "update"
)

type contribOpts struct {
	CmdName   string
	Force     bool   // instantly install, do not make a request
	Operation string // submit, delete, update
	ExecPath  string
}

type contrib struct {
	CmdName       string
	ExecPath      string
	Maintainer    string
	MaintainerUID string
	Category      string
	ShortDesc     string
	LongDesc      string
	Comments      string
}

func runContrib(opts *contribOpts) error {
	var action func(*contribOpts) error
	switch opts.Operation {
	case submitOp:
		action = submit
	case delOp:
		action = delete
	case updateOp:
		action = update
	}

	return action(opts)
}

func validExec(execPath string) error {
	if !path.IsAbs(execPath) {
		return fmt.Errorf("'%s' needs to be an absolute path", execPath)
	}
	fi, err := os.Stat(execPath)
	if err != nil {
		return fmt.Errorf("could not stat '%s'; does it exist? error: %w", execPath, err)
	}
	if fi.Mode()&0001 == 0 {
		return fmt.Errorf("'%s' is not executable", execPath)
	}
	return nil
}

func submit(opts *contribOpts) error {
	var cmdName string
	var category string
	var shortDesc string
	var longDesc string
	var comments string

	u, err := user.Current()
	if err != nil {
		return fmt.Errorf("that's my purse. I don't know you! %w", err)
	}

	// TODO should commands be markable as for admins only? i feel like that can be a parallel, more manual system.

	isAdmin, err := tuser.IsAdmin(u)
	if err != nil {
		return fmt.Errorf("that's my purse. I don't know you! %w", err)
	}

	if opts.Force && !isAdmin {
		return errors.New("must be admin to use --force")
	}

	if err := validExec(opts.ExecPath); err != nil {
		return err
	}
	_, defaultName := filepath.Split(opts.ExecPath)
	if err := survey.AskOne(&survey.Input{
		Message: "what is the command's name?",
		Help:    "for example if it's called 'cats' it will be invoked when users run 'town cats'",
		Default: defaultName}, &cmdName); err != nil {
		return err
	}

	categories := []string{
		"art",
		"social",
		"game",
		"utility",
		"programming",
		"community",
		"misc",
	}
	var choice int
	if err := survey.AskOne(&survey.Select{
		Message: "what category best describes this command?",
		Options: categories,
	}, &choice); err != nil {
		return err
	}
	category = categories[choice]

	if err := survey.AskOne(&survey.Input{
		Message: "in one brief sentence, what does this command do?",
	}, &shortDesc); err != nil {
		return err
	}

	if err := survey.AskOne(&survey.Editor{
		Message: "yr editor is gonna open. in there please write a long description for this command. not like, novel long, but maybe a sentence or two and some examples."}, &longDesc); err != nil {
		return nil
	}

	if err := survey.AskOne(&survey.Multiline{
		Message: "any comments for the admins? this won't be public, it's just to give admins any additional context about this command.",
	}, &comments); err != nil {
		return nil
	}

	// TODO be able to set a maintainer other than caller. this might only be if an admin.
	// TODO would be fun if it was a Select using a user list -- extract that from stats cmd

	c := contrib{
		CmdName:   cmdName,
		Category:  category,
		ShortDesc: shortDesc,
		LongDesc:  longDesc,
		ExecPath:  opts.ExecPath,
		// for later validation against file owner
		Maintainer:    u.Username,
		MaintainerUID: u.Uid,
		Comments:      comments,
	}

	bs, err := yaml.Marshal(c)
	if err != nil {
		return fmt.Errorf("failed to serialize contrib: %w", err)
	}

	fname := fmt.Sprintf("%d.yml", time.Now().Unix())
	f, err := os.Create(path.Join(contribRequestPath, fname))
	if err != nil {
		return fmt.Errorf("failed to open contrib file for writing: %w", err)
	}

	if _, err = f.Write(bs); err != nil {
		return fmt.Errorf("failed to write contrib file: %w", err)
	}

	err = email.SendLocalEmail("vilmibm", fmt.Sprintf("contrib: %s", cmdName), string(bs))
	if err != nil {
		return fmt.Errorf("could not email vilmibm. ping them on IRC or something. the error was: %w", err)
	}

	fmt.Printf("submitted %s for review! thank you~\n", cmdName)

	return nil
}

func update(opts *contribOpts) error {
	return nil
}

func delete(opts *contribOpts) error {
	return nil
}

func rootCmd() *cobra.Command {
	var updateName string
	var delName string
	var force bool

	rc := &cobra.Command{
		Use:   "contrib [path to executable]",
		Short: "Submit new commands to the town launcher",
		RunE: func(cmd *cobra.Command, args []string) error {
			// submit - requires path arg
			// update - requres path arg
			// delete - no arg needed
			if updateName != "" && delName != "" {
				return errors.New("-u and -d are mutually exclusive")
			}

			var cmdName string
			operation := "submit"
			if updateName != "" {
				operation = "update"
				cmdName = updateName
			} else if delName != "" {
				operation = "delete"
				cmdName = delName
			}

			if (operation == "update" || operation == "submit") && len(args) == 0 {
				return errors.New("path to executable required when submitting or updating")
			}

			if operation == "delete" && len(args) > 0 {
				return fmt.Errorf("no arguments expected when deleting; got %d", len(args))
			}

			if len(args) > 1 {
				return fmt.Errorf("at most one argument is accepted; got %d", len(args))
			}

			var execPath string
			if len(args) == 1 {
				execPath = args[0]
			}

			opts := &contribOpts{
				Operation: operation,
				Force:     force,
				ExecPath:  execPath,
				CmdName:   cmdName,
			}

			return runContrib(opts)
		},
		// TODO longer example
	}

	rc.Flags().StringVarP(&updateName, "update", "u", "", "Name of command to update")
	rc.Flags().StringVarP(&delName, "delete", "d", "", "Name of command to delete")
	// TODO hiding this until i decide i want it or not
	//rc.Flags().BoolVarP(&force, "force", "f", false, "skip request, just install the command")

	return rc
}

func main() {
	if err := rootCmd().Execute(); err != nil {
		fmt.Fprintln(os.Stderr, err.Error())
		os.Exit(1)
	}
}