package main

import (
	"bytes"
	"database/sql"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"os"
	"regexp"
	"slices"
	"strings"
	"time"

	"git.tilde.town/tildetown/town/external/lockingwriter"
	"git.tilde.town/tildetown/town/models"
	"git.tilde.town/tildetown/town/signup"
	"github.com/MakeNowJust/heredoc/v2"
	"github.com/gdamore/tcell/v2"
	_ "github.com/mattn/go-sqlite3"
	"github.com/rivo/tview"
)

const (
	maxInputLength = 10000
)

type scene struct {
	Name        string
	Description string
	Host        *character
	SorryMsg    string
	Input       *bytes.Buffer
	OnAdvance   func(*scene)
	OnMsg       func(*scene, *tview.TextView, string)
}

func newScene(name, desc, sorryMsg string, host *character, onAdvance func(*scene), onMsg func(*scene, *tview.TextView, string)) *scene {
	return &scene{
		Name:        name,
		Description: desc,
		SorryMsg:    sorryMsg,
		Host:        host,
		Input:       bytes.NewBuffer([]byte{}),
		OnAdvance:   onAdvance,
		OnMsg:       onMsg,
	}
}

type sceneManager struct {
	scenes  []*scene
	Current *scene
	index   int
	output  io.Writer
	Save    func()
}

func (m *sceneManager) Advance() bool {
	if m.Current.Name == "done" {
		return false
	}
	if m.Current.Input.Len() == 0 {
		fmt.Fprintln(m.output, m.Current.Host.Say(m.Current.SorryMsg))
		return false
	}
	m.Current.OnAdvance(m.Current)
	m.index++
	m.Current = m.scenes[m.index]
	if m.Current.Name == "done" {
		m.Save()
	}
	fmt.Fprintln(m.output, heredoc.Doc(`

			[purple]----------[-:-:-]

		`))
	fmt.Fprintln(m.output, m.Current.Description)
	return true
}

func newSceneManager(output io.Writer, scenes []*scene) *sceneManager {
	return &sceneManager{
		output:  output,
		scenes:  scenes,
		Current: scenes[0],
	}
}

type character struct {
	Name        string
	Description string
}

func newCharacter(name, description string) *character {
	return &character{
		Name:        name,
		Description: description,
	}
}

func (c *character) Say(msg string) string {
	verb := "says"
	if c.Name == "you" {
		verb = "say"
	}
	return fmt.Sprintf(`[-:-:b]%s %s:[-:-:-]
    %s
`,
		c.Name,
		verb,
		strings.TrimSpace(msg))
}

var ErrNoSuchDomain = errors.New("no host found for email address")
var ErrNoSuchMailserver = errors.New("no mail server found for email address")

// DigMX does some grubbing around to attempt to find valid email hosts, and
// then runs then through [net.LookupMX] and returns their mailserver domains.
// may return [ErrNoSuchDomain] or [ErrNoSuchMailserver].
func DigMX(raw string) (domains []string, err error) {
	re := regexp.MustCompile(`@[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)+\b`) // good enough
	candidates := re.FindAllString(raw, -1)

	// the error checking tries to be very generous: if anything comes up
	// positive we will throw no errors and just assume the rest was a fluke.
	ok := false
	for _, host := range candidates {
		records, e := net.LookupMX(host[1:])
		if e != nil {
			err = ErrNoSuchDomain
		} else if len(records) == 0 {
			err = ErrNoSuchMailserver
		} else {
			ok = true
			for _, record := range records {
				domains = append(domains, record.Host)
			}
		}
	}

	if ok {
		return domains, nil
	}
	return
}

func main() {
	lw := lockingwriter.New()
	logger := log.New(lw, "signup: ", log.Ldate|log.Ltime|log.LUTC|log.Lshortfile|log.Lmsgprefix)

	db, err := signup.ConnectDB()
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(2)
	}

	err = _main(logger, db)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(3)
	}
}

func _main(l *log.Logger, db *sql.DB) error {
	pages := tview.NewPages()
	mainFlex := tview.NewFlex()
	input := tview.NewTextArea()
	input.SetBorder(true).SetBorderColor(tcell.ColorPaleTurquoise)
	input.SetTitle("press enter to send")
	input.SetMaxLength(2000)

	title := tview.NewTextView()
	title.SetDynamicColors(true)
	title.SetTextAlign(tview.AlignCenter)
	title.SetText("[purple]the tilde town sign up portal[-]")

	footer := tview.NewTextView()
	footer.SetText("type /help and press enter to get help. type /quit and press enter to leave.")

	msgScroll := tview.NewTextView()
	msgScroll.SetDynamicColors(true)
	msgScroll.SetBackgroundColor(tcell.ColorBlack)
	msgScroll.SetTextColor(tcell.ColorWhite)

	mainFlex.SetDirection(tview.FlexRow)
	mainFlex.AddItem(title, 1, -1, false)
	mainFlex.AddItem(msgScroll, 0, 1, false)
	mainFlex.AddItem(input, 4, -1, true)
	mainFlex.AddItem(footer, 1, -1, false)

	pages.AddPage("main", mainFlex, true, true)

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

	player := newCharacter("you", "it's you. how are you?")

	su := &models.TownSignup{ID: -1}

	save := func() {
		su.Created = time.Now()
		err := su.Insert(db)

		for _, note := range su.Notes {
			note.SignupID = su.ID
			err = note.Insert(db)
		}

		if err != nil {
			l.Printf("failed to write to db: %s", err.Error())
			l.Printf("dumping values: %#v", su)
			return
		}
	}

	scenes := []*scene{
		newScene("start", heredoc.Doc(`
				You open your eyes.

				You're in some kind of workshop. 
				Wires and computers in various state of disrepair are strewn across
				tables and shelves. It smells faintly of burnt cedar.

				The wires and components before you slowly drag 
				themselves into the shape of a small humanoid.

				[-:-:b]wire guy says:[-:-:-]
					hello, welcome to the application for membership in tilde town.
					first, please let me know what a good [-:-:b]email address[-:-:-] is for you?

					just say it out loud. as many times as you need. to get it right.

					once you've told me your email you can [-:-:b]/nod[-:-:-] to move on.
			`),
			"i'm sorry, before going further could you share an email with me?",
			newCharacter("wire guy", "a lil homonculus made of discarded computer cables"),
			func(s *scene) {
				su.Email = string(s.Input.Bytes())
				suspiciousHosts, err := models.SuspiciousHosts(db)
				if err != nil {
					l.Println("could not connect to suspicious hosts db")
					return
				}
				var shDomains []string
				for _, host := range suspiciousHosts {
					shDomains = append(shDomains, host.Domain)
				}
				if records, err := DigMX(su.Email); err == nil {
					for _, record := range records {
						if slices.Contains(shDomains, record) {
							su.Notes = append(su.Notes, models.SignupNote{
								Author:   "dns",
								Content:  fmt.Sprintf("email address has suspicious host %s", record),
								SignupID: su.ID,
							})
						}
					}
				}
			},
			func(s *scene, tv *tview.TextView, msg string) {
				// TODO could check and see if it's email shaped and admonish if not
				// NOTE(nbsp): DigMX call can see if email is invalid but this isn't used yet
				trimmed := strings.TrimSpace(msg)
				fmt.Fprintln(tv, s.Host.Say(fmt.Sprintf("I heard '%s'. Is that right? if so, /nod", trimmed)))
			}),
		newScene("how", heredoc.Doc(`
				The workshop fades away. You hear the sound of a dial up modem 
				in the distance.

				Trees spring up out of the ground around you: birches, oaks, maples,
				firs, yews, pines, cypresses complete with tiny swamps around their trunks,
				junipers, redwoods, cedars, towering palms waving gently in a breeze, eucalyptus, 
				banyan. the smell is riotous like a canvas with all the colors splashed on. birds 
				start to sing.

				a shrike alights on a branch in front of you.

				[-:-:b]the shrike says:[-:-:-]
					phweeturpff. how did you find out about the town? did anyone refer you?

					just say your answer out loud. when you've said what you want, [-:-:b]/nod[-:-:-]
					to continue.
			`),
			"phweeturpff",
			newCharacter("the shrike", "a little grey bird. it has a pretty song."),
			func(s *scene) { su.How = string(s.Input.Bytes()) }, nil),
		newScene("what", heredoc.Doc(`
				You sink down into soft mossy floor of the forest. You find yourself floating in darkness.

				At the far reaches of your vision you can make out a faint neon grid. Around you
				float pieces of consumer electronic appliances from 1980s Earth. A VCR approaches
				you and speaks, flapping its tape slot cover with each word.

				[-:-:b]the vcr says:[-:-:-]
					welcome! thank you for coming this far. just two questions left. what about
					tilde town interests you? what kind of stuff might you want to get up to here?

					as usual, just say your answer. when you're satisfied, please [-:-:b]/nod[-:-:-]
			`),
			"hmm did you say something?",
			newCharacter("the vcr", "a black and grey VCR from 1991"),
			func(s *scene) { su.Why = string(s.Input.Bytes()) }, nil),
		newScene("link", heredoc.Doc(`
				You realize your eyes have been shut. You open them and, in an instant,
				the neon grid and polygons are gone. You're in a convenience store. Outside
				it's dark besides a single pool of light coming from a street lamp. it's illuminating
				some litter and a rusty, blue 1994 pontiac grand am.

				The shelves around you are stocked with products you've never heard of before like
				Visible Pants, Petty Burgers, Gentle Rice, Boo Sponge, Power Banjo, Superware, Kneephones,
				and Diet Coagulator. A mop is mopping the floor and turns to you.

				[-:-:b]the mop says:[-:-:-]
					swishy slop. last question. where online can we get to know you? do you have a personal 
					website or social media presence? we'll take whatever.

					say some links and words out loud, you know the drill.

					when you're happy you can submit this whole experience with a [-:-:b]/nod[-:-:-]
			`),
			"just the one last thing please",
			newCharacter("the mop", "a greying mop with a wooden handle."),
			func(s *scene) { su.Links = string(s.Input.Bytes()) }, nil),
		newScene("done", heredoc.Doc(`
				thank you for applying to tilde.town! 

				please be on the look out for an email from [-:-:b]root@tilde.town[-:-:-]. 
				it's almost certain that it will end up in your spam filter, unfortunately.

				every application is reviewed by a human. it can take up to 30 days for
				an application to be reviewed. not every application is approved.

				you can [-:-:b]/quit[-:-:-] now

				ok bye have a good one~
			`),
			"",
			newCharacter("the æther", "the very air around you"),
			nil, nil),
	}

	sm := newSceneManager(msgScroll, scenes)
	sm.Save = save

	handleInput := func(msg string) {
		msg = strings.TrimSpace(msg)
		if msg == "" {
			return
		}
		if strings.HasPrefix(msg, "/") {
			split := strings.Split(msg, " ")
			if len(split) > 0 {
				msg = split[0]
			}
			switch strings.TrimPrefix(msg, "/") {
			case "help":
				fmt.Fprintln(msgScroll, sm.Current.Host.Say(`some artificial beings will guide you through applying to tilde.town.
    type things, then press enter. to take an action, put a "/" before a word.
    for example typing:
    /nod
    and pressing enter will cause you to nod. some other verbs: /quit /look`))
			case "quit":
				app.Stop()
			case "look":
				fmt.Fprintln(msgScroll, "")
				fmt.Fprintln(msgScroll, sm.Current.Description)
			case "nod":
				if !sm.Advance() {
					fmt.Fprintln(msgScroll, "you nod, but nothing happens.")
					fmt.Fprintln(msgScroll)
				}
			}
			return
		}
		if sm.Current.Input.Len() > maxInputLength {
			fmt.Fprintln(msgScroll,
				sm.Current.Host.Say(
					"sorry I've heard more than I can remember :( maybe it's time to move on"))
			return
		}
		fmt.Fprintln(msgScroll, player.Say(msg))
		fmt.Fprintln(sm.Current.Input, msg)
		if sm.Current.OnMsg != nil {
			sm.Current.OnMsg(sm.Current, msgScroll, msg)
		}
		msgScroll.ScrollToEnd()
	}

	defer db.Close()

	app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
		switch event.Key() {
		case tcell.KeyEnter:
			handleInput(input.GetText())
			input.SetText("", false)
			return nil
		}

		return event
	})

	app.SetAfterDrawFunc(func(_ tcell.Screen) {
		fmt.Fprintln(msgScroll, sm.Current.Description)
		app.SetAfterDrawFunc(nil)
	})

	return app.Run()
}
