package main

import (
	"bytes"
	"database/sql"
	"errors"
	"fmt"
	"math/rand"
	"os"
	"os/exec"
	"os/user"
	"strconv"
	"strings"
	"time"

	"git.tilde.town/tildetown/town/invites"
	"git.tilde.town/tildetown/town/models"
	"git.tilde.town/tildetown/town/signup"
	tuser "git.tilde.town/tildetown/town/user"
	"github.com/gdamore/tcell/v2"
	"github.com/rivo/tview"
)

func getTitle() string {
	titles := []string{
		"yo bum rush the show",
		"can i kick it?",
		"super nintendo sega genesis",
		"birthdays was the worst days",
		"where were you when we were getting high?",
		"it's real time, real time, time to get real",
	}
	return titles[rand.Intn(len(titles))]
}

type reviewer struct {
	db        *sql.DB
	adminName string
}

func newReviewer(db *sql.DB, adminName string) *reviewer {
	return &reviewer{db: db, adminName: adminName}
}

func (r *reviewer) Review(s *models.TownSignup, decision models.SignupDecision) error {
	s.DecisionTime = time.Now()
	s.Decision = decision
	s.DecidedBy = r.adminName
	return s.Review(r.db)
}

func (r *reviewer) AddNote(s *models.TownSignup, content string) error {
	note := &models.SignupNote{
		Author:   r.adminName,
		Content:  content,
		SignupID: s.ID,
	}

	return note.Insert(r.db)
}

func renderSignup(s models.TownSignup) string {
	out := ""

	pairs := [][]string{
		{"submitted", s.Created.Format("2006-01-02 15:04")},
		{"e-mail", s.Email},
		{"how found / referral", s.How},
		{"why like town / plans", s.Why},
		{"links", s.Links},
	}

	for _, v := range pairs {
		out += fmt.Sprintf("[-:-:b]%s[-:-:-]\n", v[0])
		out += strings.TrimSpace(v[1])
		out += "\n\n"
	}

	return out
}

func renderNotes(s models.TownSignup) string {
	out := ""
	for _, note := range s.Notes {
		out += fmt.Sprintf(`%s said on %s:
  %s`, note.Author, note.Created.Format("2006-01-02 15:04"), note.Content)

		out += "\n\n"

	}
	return out
}

func searchSignups(signups []*models.TownSignup) (int, error) {
	escapeNuls := func(str string) string {
		return strings.ReplaceAll(str, "\000", " ")
	}

	buf := new(bytes.Buffer)

	for ix, signup := range signups {
		fmt.Fprintf(buf, "%d\t%s\000", ix, escapeNuls(signup.Email))
		fmt.Fprintf(buf, "%d\t%s\000", ix, escapeNuls(signup.How))
		fmt.Fprintf(buf, "%d\t%s\000", ix, escapeNuls(signup.Links))
		fmt.Fprintf(buf, "%d\t%s\000", ix, escapeNuls(signup.Why))
	}

	cmd := exec.Command("fzf", "--read0", "--delimiter=\t", "--tac", "--with-nth=2..")
	cmd.Stdin = buf
	cmd.Stderr = os.Stderr

	out, err := cmd.Output()
	if err != nil {
		return -1, err
	}

	if len(out) == 0 {
		return -1, nil
	}

	s := strings.Split(string(out[:]), "\t")[0]
	n, err := strconv.Atoi(s)
	if err != nil {
		return -1, err
	}

	return n, nil
}

func _main() error {
	inviteDB, err := invites.ConnectDB()
	if err != nil {
		return fmt.Errorf("could not connect to invites database: %w", err)
	}

	signupDB, err := signup.ConnectDB()
	if err != nil {
		return fmt.Errorf("could not connect to signups database: %w", err)
	}

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

	if !isAdmin {
		return errors.New("this command can only be run by a town admin")
	}

	r := newReviewer(signupDB, u.Username)

	su := models.TownSignup{}

	signups, err := su.All(signupDB)
	if err != nil {
		return fmt.Errorf("could not fetch signups: %w", err)
	}

	signupIx := 0

	title := tview.NewTextView()
	title.SetText(getTitle())
	title.SetTextAlign(tview.AlignCenter)
	title.SetTextColor(tcell.ColorPurple)
	title.SetBackgroundColor(tcell.ColorBlack)

	appView := tview.NewTextView()
	appView.SetScrollable(true)
	appView.SetDynamicColors(true)

	legend := tview.NewTextView()
	legend.SetText("s/S: next/prev r: random F: find A: approve R: reject N: notate Q: quit")
	legend.SetTextColor(tcell.ColorPurple)
	legend.SetTextAlign(tview.AlignCenter)
	legend.SetBackgroundColor(tcell.ColorBlack)

	count := tview.NewTextView()
	count.SetDynamicColors(true)
	updateCount := func() {
		count.SetText(fmt.Sprintf("[-:-:b]%d of %d[-:-:-]", signupIx+1, len(signups)))
		if len(signups) == 0 {
			count.SetText("")
		}
	}
	updateCount()

	notesView := tview.NewTextView()
	notesView.SetDynamicColors(true)
	notesView.SetBorder(true).SetBorderColor(tcell.ColorPurple)

	bottomFlex := tview.NewFlex()
	bottomFlex.SetDirection(tview.FlexColumn)
	bottomFlex.AddItem(count, 0, 1, false)
	bottomFlex.AddItem(legend, 0, 10, false)

	innerFlex := tview.NewFlex()
	innerFlex.SetDirection(tview.FlexColumn)
	innerFlex.AddItem(appView, 0, 2, true)
	innerFlex.AddItem(notesView, 0, 1, true)

	mainFlex := tview.NewFlex()
	mainFlex.SetDirection(tview.FlexRow)
	mainFlex.AddItem(title, 1, -1, false)
	mainFlex.AddItem(innerFlex, 0, 1, false)
	mainFlex.AddItem(bottomFlex, 1, -1, false)
	// set scrollable
	mainFlex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
		appView.InputHandler()(event, func(p tview.Primitive) {})
		return nil
	})

	pages := tview.NewPages()

	errorModal := tview.NewModal()
	errorModal.AddButtons([]string{"damn"})
	errorModal.SetDoneFunc(func(ix int, _ string) {
		pages.SwitchToPage("main")
	})

	render := func() {
		if len(signups) == 0 {
			appView.SetText("no signups")
			return
		}
		currSignup := signups[signupIx]
		err := currSignup.RefreshNotes(signupDB)
		if err != nil {
			errorModal.SetText(fmt.Sprintf("error! failed to add note: %s", err.Error()))
			pages.SwitchToPage("error")
		}

		appView.SetText(renderSignup(*currSignup))
		notesView.SetText(renderNotes(*currSignup))
	}

	render()

	notate := tview.NewForm()
	notate.AddTextArea("note", "", 80, 10, 1000, func(string) {})
	notate.AddButton("submit", func() {
		fi := notate.GetFormItemByLabel("note").(*tview.TextArea)
		err = r.AddNote(signups[signupIx], fi.GetText())
		if err != nil {
			errorModal.SetText(fmt.Sprintf("error! failed to add note: %s", err.Error()))
			pages.SwitchToPage("error")
			return
		}

		render()

		pages.SwitchToPage("main")
	})
	notate.AddButton("cancel", func() {
		pages.SwitchToPage("main")
	})

	reviewModal := tview.NewFlex().SetDirection(tview.FlexRow)

	providedEmailView := tview.NewTextView()
	providedEmailView.SetTitle("provided email input")

	reviewForm := tview.NewForm()
	decisionFI := tview.NewDropDown().SetLabel("decision")
	decisionFI.SetOptions([]string{"accepted", "rejected"}, func(_ string, _ int) {})

	cleanEmailInput := tview.NewInputField()
	cleanEmailInput.SetLabel("clean email    ")
	cleanEmailInput.SetAcceptanceFunc(func(tx string, _ rune) bool { return len(tx) > 0 })

	reviewForm.AddFormItem(decisionFI)
	reviewForm.AddFormItem(cleanEmailInput)
	reviewForm.AddButton("submit", func() {
		currSignup := signups[signupIx]
		cleanEmail := cleanEmailInput.GetText()
		currSignup.CleanEmail = cleanEmail

		decision := models.SignupRejected
		_, d := decisionFI.GetCurrentOption()
		if d == "accepted" {
			decision = models.SignupAccepted
		}

		err := r.Review(currSignup, decision)
		if err != nil {
			errorModal.SetText(fmt.Sprintf("error! failed to submit review: %s", err.Error()))
			pages.SwitchToPage("error")
			return
		}

		newSignups := []*models.TownSignup{}
		for ix, s := range signups {
			if ix != signupIx {
				newSignups = append(newSignups, s)
			}
		}
		signups = newSignups
		if len(signups) > 0 {
			if signupIx >= len(signups) {
				signupIx = 0
			}
		}
		updateCount()
		render()
		if decision == models.SignupAccepted {
			invite := &invites.Invite{
				Email: currSignup.CleanEmail,
			}

			if err = invite.Insert(inviteDB); err != nil {
				errorModal.SetText(fmt.Sprintf("error! failed to create invite: %s", err.Error()))
				pages.SwitchToPage("error")
			}

			if err = sendInviteEmail(*invite); err != nil {
				errorModal.SetText(fmt.Sprintf("error! failed to send welcome email: %s", err.Error()))
				pages.SwitchToPage("error")
			}
		}
		pages.SwitchToPage("main")
	})
	reviewForm.AddButton("cancel", func() {
		pages.SwitchToPage("main")
	})

	reviewModal.AddItem(tview.NewTextView().SetText("provided email input"), 1, 1, false)
	reviewModal.AddItem(providedEmailView, 0, 1, false)
	reviewModal.AddItem(reviewForm, 0, 1, true)

	pages.AddPage("main", mainFlex, true, true)
	pages.AddPage("error", errorModal, false, false)
	pages.AddPage("notate", notate, true, false)
	pages.AddPage("review", reviewModal, true, false)

	app := tview.NewApplication()
	app.SetRoot(pages, true)

	app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
		currPage, _ := pages.GetFrontPage()
		if currPage == "notate" || currPage == "review" {
			return event
		}
		switch event.Rune() {
		case 's':
			signupIx++
			if signupIx == len(signups) {
				signupIx = 0
			}
			updateCount()
			render()
			return nil
		case 'S':
			signupIx--
			if signupIx < 0 {
				signupIx = len(signups) - 1
			}
			updateCount()
			render()
			return nil
		case 'r':
			if len(signups) > 0 {
				signupIx = rand.Intn(len(signups))
				updateCount()
				render()
			}
		// TODO: there's a bunch of messy state management.
		// should we generate this pane functionally?
		case 'A':
			if len(signups) == 0 {
				return nil
			}
			emailVal := signups[signupIx].Email
			providedEmailView.SetText(emailVal)
			cleanEmailInput.SetLabel("clean email   ")
			cleanEmailInput.SetText("")
			/*
				TODO the placeholder doesn't appear to become the default text which is
				what I wanted it to do. Just taking this out so the blank box beckons
				input. Also, it seems like the AcceptanceFunc didn't work since a blank
				value got through.
				cleanEmailInput.SetPlaceholder(
					strings.TrimSpace(strings.ReplaceAll(emailVal, "\n", " ")))
			*/
			cleanEmailInput.SetChangedFunc(func(text string) {
				if strings.Contains(emailVal, text) {
					cleanEmailInput.SetLabel("clean email   ")
				} else {
					cleanEmailInput.SetLabel("[red]clean email :(")
				}
			})
			decisionFI.SetCurrentOption(0)
			pages.SwitchToPage("review")
			app.SetFocus(cleanEmailInput)
			return nil
		case 'R':
			if len(signups) == 0 {
				return nil
			}
			emailVal := signups[signupIx].Email
			providedEmailView.SetText(emailVal)
			cleanEmailInput.SetLabel("clean email   ")
			cleanEmailInput.SetText("")
			/*
				TODO the placeholder doesn't appear to become the default text which is
				what I wanted it to do. Just taking this out so the blank box beckons
				input. Also, it seems like the AcceptanceFunc didn't work since a blank
				value got through.
				cleanEmailInput.SetPlaceholder(
					strings.TrimSpace(strings.ReplaceAll(emailVal, "\n", " ")))
			*/
			cleanEmailInput.SetChangedFunc(func(text string) {
				if strings.Contains(emailVal, text) {
					cleanEmailInput.SetLabel("clean email   ")
				} else {
					cleanEmailInput.SetLabel("[red]clean email :(")
				}
			})
			decisionFI.SetCurrentOption(1)
			pages.SwitchToPage("review")
			app.SetFocus(cleanEmailInput)
			return nil
		case 'N':
			if len(signups) == 0 {
				return nil
			}
			pages.SwitchToPage("notate")
			return nil
		case 'F':
			app.Suspend(func() {
				ix, err := searchSignups(signups)
				if err != nil {
					if exiterr, ok := err.(*exec.ExitError); ok {
						// no match or interrupt. who cares
						switch exiterr.ExitCode() {
						case 1:
						case 130:
							return
						}
					}

					errorModal.SetText(fmt.Sprintf("error! failed to search: %s", err.Error()))
					pages.SwitchToPage("error")
				} else if ix >= 0 {
					signupIx = ix
				}
			})

			updateCount()
			render()
			return nil
		case 'Q':
			app.Stop()
		}

		return event
	})

	return app.Run()
}

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