package main

import (
	"flag"
	"fmt"
	"io"
	"math/rand"
	"net/http"
	"net/url"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"

	"github.com/bwmarrin/discordgo"
	"github.com/rudi9719/loggy"
)

var (
	startupTime    time.Time
	setupToken     = fmt.Sprintf("!setup %+v", rand.Intn(9999)+1000)
	rebootToken    = fmt.Sprintf("!reboot %+v", rand.Intn(9999)+1000)
	bump           = true
	config         Config
	log            = loggy.NewLogger(config.LogOpts)
	lastActiveChan string
	lastActiveTime time.Time
	token          string
	configFile     string
	setupMsg       string
	dg             *discordgo.Session
	lastPM         = make(map[string]time.Time)
	quotes         = []string{"The hardest choices require the strongest wills.", "You're strong, but I could snap my fingers and you'd all cease to exist.", "Fun isn't something one considers when balancing the universe. But this... does put a smile on my face.", "Perfectly balanced, as all things should be.", "I am inevitable."}
)

func init() {
	flag.StringVar(&token, "t", "", "Bot Token")
	flag.StringVar(&configFile, "c", "", "Config file")
	flag.Parse()
}

func main() {
	go runWeb()
	defer log.PanicSafe()
	if configFile == "" {
		configFile = "config.json"
	} else {
		loadConfig()
	}
	log = loggy.NewLogger(config.LogOpts)
	startupTime = time.Now()
	lastActiveTime = time.Now()
	lastActiveChan = config.AdminChannel
	if token == "" {
		log.LogPanic("No token provided. Please run: disgord-thanos -t <bot token>")
	}

	log.LogCritical("SetupToken: %+v\nRebootToken: %+v", setupToken, rebootToken)
	dg, err := discordgo.New("Bot " + token)
	if err != nil {
		log.LogErrorType(err)
		log.LogPanic("Unable to create bot using token.")
	}

	dg.AddHandler(ready)
	dg.AddHandler(guildMemberRemove)
	dg.AddHandler(guildMemberAdd)
	dg.AddHandler(guildMemberBanned)
	dg.AddHandler(messageCreate)
	dg.AddHandler(readReaction)
	dg.AddHandler(guildMemberUpdate)
	dg.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAll)

	err = dg.Open()
	if err != nil {
		log.LogErrorType(err)
		log.LogPanic("Unable to open websocket.")
	}

	log.LogInfo("Thanos is now running.  Press CTRL-C to exit.")
	go purgeTimer(dg)
	sc := make(chan os.Signal, 1)
	signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
	<-sc
	saveConfig()
	dg.Close()
}

func exit(s *discordgo.Session) {
	s.Close()
	saveConfig()
	os.Exit(0)
}

func runPurge(s *discordgo.Session) {
	defer log.PanicSafe()
	if time.Since(config.BumpTime) > 2*time.Hour {
		bump = true
	}
	for uid, join := range config.Probations {
		if time.Since(join) > 2*time.Hour {
			delete(config.Probations, uid)
		}
	}
	for k, v := range config.Unverified {
		isUnverified := false
		m, err := s.GuildMember(config.GuildID, k)
		if err != nil {
			delete(config.Unverified, k)
			continue
		}
		for _, role := range m.Roles {
			if role == config.MonitorRole {
				isUnverified = true
			}
		}
		if isUnverified {
			if val, ok := lastPM[k]; ok && time.Since(val) < 5*time.Minute {
				continue
			}
			lastPM[k] = time.Now()
			pmChann, _ := s.UserChannelCreate(k)
			s.ChannelMessageSend(pmChann.ID,
				fmt.Sprintf("This is a reminder that you have not verified with me and will be removed in %+v. You may reply to this message for verification instructions.", time.Until(v.Add(1*time.Hour))))
			if time.Since(v) > (time.Hour * 1) {
				s.ChannelMessageSend(config.AdminChannel, fmt.Sprintf("%+v was removed.", m.Mention()))
				s.GuildMemberDeleteWithReason(config.GuildID, k, fmt.Sprintf("Unverified user %+v.", v))
			}
		} else {
			delete(config.Unverified, k)
		}
	}
	messages, _ := s.ChannelMessages(config.MonitorChann, 100, "", "", "")
	for _, message := range messages {
		found := false
		for user := range config.Unverified {
			if message.Author.ID == user {
				found = true
			}
			for _, mention := range message.Mentions {
				if mention.ID == user {
					found = true
				}
			}
		}
		if !found {
			s.ChannelMessageDelete(config.MonitorChann, message.ID)
		}
	}

	saveConfig()
}

func ready(s *discordgo.Session, event *discordgo.Ready) {
	// Set the playing status.
	s.UpdateStatus(0, "DreamDaddy v1.2")
}

func guildMemberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) {
	defer log.PanicSafe()
	for role := range m.Roles {
		if fmt.Sprintf("%+v", role) == config.MonitorRole {
			s.ChannelMessageSend(config.AdminChannel, "New unverified user detected.")
			s.ChannelMessageSend(config.MonitorChann, fmt.Sprintf("Welcome %+v, you may PM me your verification, or I will ban you in an hour!\nSay \"!rules\" in this channel, without quotes for the rules. You may private/direct message me for verification instructions.\n\nYou will not be able to read/see other channels or users until you verify.", m.User.Mention()))
			config.Unverified[m.User.ID] = time.Now()
			config.Probations[m.User.ID] = time.Now()
			saveConfig()
		}
	}

}

func guildMemberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd) {
	defer log.PanicSafe()
	config.Unverified[m.User.ID] = time.Now()
	config.Probations[m.User.ID] = time.Now()
	s.GuildMemberRoleAdd(config.GuildID, m.User.ID, config.MonitorRole)
	s.ChannelMessageSend(config.MonitorChann, fmt.Sprintf("Welcome %+v, you may PM me your verification, or I will ban you in an hour!\nSay \"!rules\" in this channel, without quotes for the rules. You may private/direct message me for verification instructions.\n\nYou will not be able to read/see other channels or users until you verify.", m.User.Mention()))
	saveConfig()
}

func guildMemberBanned(s *discordgo.Session, m *discordgo.GuildBanAdd) {
	defer log.PanicSafe()
	for uid := range config.Probations {
		if m.User.Email == uid {
			delete(config.Probations, uid)
		}
	}
	saveConfig()
}

func guildMemberRemove(s *discordgo.Session, m *discordgo.GuildMemberRemove) {
	defer log.PanicSafe()
	go runPurge(s)
	banned := false
	for uid, join := range config.Probations {
		if time.Since(join) < 2*time.Hour {
			if m.User.ID == uid {
				banned = true
				s.GuildBanCreateWithReason(config.GuildID, m.User.ID, fmt.Sprintf("Left within 2 hours of joining. %+v", time.Since(join)), 0)
				delete(config.Probations, uid)
			}
		} else {
			delete(config.Probations, uid)
		}
	}
	s.ChannelMessageSend(config.AdminChannel, fmt.Sprintf("%+v (@%+v) has left, ban: %+v", m.User.ID, m.User.Username, banned))
	delete(config.Unverified, m.User.ID)
	for msg, v := range config.Verifications {
		if v.UserID == m.User.ID {
			delete(config.Verifications, msg)
		}
	}
	saveConfig()

}

func verifyMember(s *discordgo.Session, u discordgo.User) {
	defer log.PanicSafe()
	s.GuildMemberRoleAdd(config.GuildID, u.ID, config.VerifiedRole)
	s.GuildMemberRoleRemove(config.GuildID, u.ID, config.MonitorRole)
	st, _ := s.UserChannelCreate(u.ID)
	s.ChannelMessageSend(st.ID, "Your verification has been accepted, welcome!")
	s.ChannelMessageSend(config.IntroChann, fmt.Sprintf("Welcome %+v please introduce yourself! :) feel free to check out <#710557387937022034> to tag your roles. Also please mute any channels you are not interested in!", u.Mention()))
}

func rejectVerification(s *discordgo.Session, u discordgo.User) {
	defer log.PanicSafe()
	st, _ := s.UserChannelCreate(u.ID)
	if st != nil {
		s.ChannelMessageSend(st.ID, fmt.Sprintf("Your verification has been rejected. This means it did not clearly show your face, with your pinkie finger held to the corner of your mouth, or the photo looked edited/filtered. No filters will be accepted.\n\nPlease try again before %+v", time.Until(time.Now().Add(1*time.Hour))))
	}
	config.Unverified[u.ID] = time.Now()
}

func requestAge(s *discordgo.Session, u discordgo.User) {
	defer log.PanicSafe()
	st, _ := s.UserChannelCreate(u.ID)
	s.ChannelMessageSend(st.ID, "What is your ASL? (Age/Sex/Language) Please note, this is NOT requesting your gender, but your biological sex. Gender is a social construct, sex is biology and in the context of pornographic images more important.")

}

func handlePM(s *discordgo.Session, m *discordgo.MessageCreate) {
	defer log.PanicSafe()
	if strings.Contains(m.Content, "Rule") || strings.Contains(m.Content, "rule") {
		s.ChannelMessageSend(m.ChannelID, "I specifically said to say \"!rules\" without quotes in the unverified channel for the rules.")
	}
	for _, uid := range config.Verifications {
		user := userFromID(s, uid.UserID)
		if m.Author.ID == user.ID {
			s.ChannelMessageSend(m.ChannelID, "Your verification is pending. An admin will respond to it when they are available.")
			s.ChannelMessageSend(config.AdminChannel, fmt.Sprintf("%+v said: %+v", m.Author.Mention(), m.Content))
			return
		}
	}
	if len(m.Attachments) != 1 {
		s.ChannelMessageSend(m.ChannelID, "```I am a bot and this is an autoreply.\n\nUntil you send a verification, I will always say the following message:```\nYou may only send me your verification (and nothing else) to be passed to the admins (and no one else). Verification is a clear full face pic, with your pinky finger held to the corner of your mouth.")
		s.ChannelMessageSend(config.AdminChannel, fmt.Sprintf("%+v said: %+v", m.Author.Mention(), m.Content))
		return
	}
	delete(config.Unverified, m.Author.ID)
	var v Verification
	v.Submitted = time.Now()
	v.UserID = m.Author.ID
	v.Username = m.Author.Username
	v.Photo = m.Attachments[0].ProxyURL
	v.Status = "Submitted"
	msg, _ := s.ChannelMessageSend(config.AdminChannel, fmt.Sprintf("%+v\n%+v", v.Username, v.Photo))
	config.Verifications[msg.ID] = v
	s.MessageReactionAdd(config.AdminChannel, msg.ID, "👎")
	s.MessageReactionAdd(config.AdminChannel, msg.ID, "👍")
	s.MessageReactionAdd(config.AdminChannel, msg.ID, "👶")
	s.MessageReactionAdd(config.AdminChannel, msg.ID, "⛔")
}

func readReaction(s *discordgo.Session, m *discordgo.MessageReactionAdd) {
	defer log.PanicSafe()
	if m.ChannelID != config.AdminChannel || m.UserID == s.State.User.ID {
		return
	}
	admin, _ := s.GuildMember(config.GuildID, m.UserID)
	adminInteraction(s, admin.User.ID)
	verification, ok := config.Verifications[m.MessageID]
	if !ok {
		return
	}
	verification.Admin = admin.User.Username
	verification.Closed = time.Now()
	user := userFromID(s, verification.UserID)
	if user.ID == "" {
		s.ChannelMessageSend(config.AdminChannel, fmt.Sprintf("%+v, that user was not found, they might have left.", admin.Mention()))
		delete(config.Verifications, m.MessageID)
		return
	}
	if m.Emoji.Name == "👎" {
		rejectVerification(s, user)
		verification.Status = "Rejected"
	} else if m.Emoji.Name == "👍" {
		verifyMember(s, user)
		verification.Status = "Accepted"
		go storeVerification(verification)
	} else if m.Emoji.Name == "👶" {
		requestAge(s, user)
		log.LogInfo("%+v has requested ASL for user %+v.", admin.User.Username, user.Username)
		return
	} else if m.Emoji.Name == "⛔" {
		s.GuildBanCreateWithReason(config.GuildID, user.ID, fmt.Sprintf("Underage or too many failed verifications. %+v", admin.User.Username), 5)
		verification.Status = "Banned"
	} else {
		return
	}
	log.LogInfo("%+v", verification.prettyPrint())
	delete(config.Verifications, m.MessageID)
}
func storeVerification(v Verification) {
	defer log.PanicSafe()
	fileURL, _ := url.Parse(v.Photo)
	path := fileURL.Path
	segments := strings.Split(path, "/")

	fileName := segments[len(segments)-1]
	file, _ := os.Create(fmt.Sprintf("./verifications/%s-%s-%s", v.UserID, v.Username, fileName))
	client := http.Client{
		CheckRedirect: func(r *http.Request, via []*http.Request) error {
			r.URL.Opaque = r.URL.Path
			return nil
		},
	}
	resp, err := client.Get(v.Photo)
	if err != nil {
		log.LogError("Unable to download verification %s-%s-%s", v.UserID, v.Username, fileName)
	}
	defer resp.Body.Close()
	defer file.Close()
	_, err = io.Copy(file, resp.Body)
	if err != nil {
		log.LogError("Unable to store verification %s-%s-%s", v.UserID, v.Username, fileName)
	}
}

func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
	defer log.PanicSafe()
	if m.Author.ID == s.State.User.ID || m.Author.Bot {
		return
	}
	if m.GuildID == "" {
		handlePM(s, m)
		return
	}
	if m.ChannelID == config.MonitorChann {
		if strings.Contains(m.Content, "erif") && !m.Author.Bot {
			s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("%+v send me a private message for verification.", m.Author.Mention()))
		}
		return
	}
	for role := range m.Member.Roles {
		if fmt.Sprintf("%+v", role) == config.AdminRole {
			adminInteraction(s, m.Author.ID)
		}
	}
	if m.ChannelID != config.AdminChannel {
		lastActiveChan = m.ChannelID
		lastActiveTime = time.Now()
	}
	if strings.HasPrefix(m.Content, "!d bump") {
		if time.Since(config.BumpTime) < 2*time.Hour {
			s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("Sorry, <@%+v> already claimed the bump. Better luck next time!", config.LastBumper))
			return
		}
		config.LastBumper = m.Author.ID
		go bumpTimer(s)
		return
	}
	if time.Since(config.BumpTime) > 2*time.Hour {
		s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("%+v please say \"!d bump\" without the quotes to bump our server :)", m.Author.Mention()))
	}
	if m.ChannelID == config.AdminChannel {
		if strings.HasPrefix(m.Content, rebootToken) {
			exit(s)
		}
		if strings.HasPrefix(m.Content, "!quote") {
			quotes = append(quotes, strings.ReplaceAll(m.Content, "!quote", ""))
		}
		if strings.HasPrefix(m.Content, "!snap") || strings.HasPrefix(m.Content, "!purge") {
			go runPurge(s)
			s.ChannelMessageSend(config.AdminChannel, quotes[rand.Intn(len(quotes))])
		}
		if strings.HasPrefix(m.Content, "!st") {
			go status(s)
			saveConfig()
		}
	}
}