package irc

import (
	"bufio"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"math/rand"
	"net"
	"net/textproto"
	"regexp"
	"strings"
	"time"

	data "git.martyn.berlin/martyn/twitchsingstools/internal/data"
	rgb "github.com/foresthoffman/rgblog"
)

const UTCFormat = "Jan 2 15:04:05 UTC"

// Regex for parsing connection messages
//
// First matched group is our real username - twitch doesn't complain at using NICK command but doesn't honor it.
var ConnectRegex *regexp.Regexp = regexp.MustCompile(`^:tmi.twitch.tv 001 ([^ ]+) .*`)

// Regex for parsing PRIVMSG strings.
//
// First matched group is the user's name, second is the channel? and the third matched group is the content of the
// user's message.
var MsgRegex *regexp.Regexp = regexp.MustCompile(`^:(\w+)!\w+@\w+\.tmi\.twitch\.tv (PRIVMSG) #(\w+)(?: :(.*))?$`)
var DirectMsgRegex *regexp.Regexp = regexp.MustCompile(`^:(\w+)!\w+@\w+\.tmi\.twitch\.tv (PRIVMSG) (\w+)(?: :(.*))?$`)

// Regex for parsing user commands, from already parsed PRIVMSG strings.
//
// First matched group is the command name and the second matched group is the argument for the
// command.
var CmdRegex *regexp.Regexp = regexp.MustCompile(`^!(\w+)\s?(\w+)?`)

type IRCOAuthCred struct {

	// The bot account's OAuth password.
	Password string `json:"password,omitempty"`

	// The developer application client ID. Used for API calls to Twitch.
	Nick string `json:"nick,omitempty"`
}

type AppOAuthCred struct {

	// The bot account's OAuth password.
	ClientID string `json:"client_id,omitempty"`

	// The developer application client ID. Used for API calls to Twitch.
	ClientSecret string `json:"client_secret,omitempty"`
}

type KardBot struct {
	Channel        string
	conn           net.Conn
	IrcCredentials *IRCOAuthCred
	AppCredentials *AppOAuthCred
	MsgRate        time.Duration
	Name           string
	Port           string
	IrcPrivatePath string
	AppPrivatePath string
	Server         string
	startTime      time.Time
	Prompts        []string
	GlobalData     data.GlobalData
}

// Connects the bot to the Twitch IRC server. The bot will continue to try to connect until it
// succeeds or is forcefully shutdown.
func (bb *KardBot) Connect() {
	var err error
	rgb.YPrintf("[%s] Connecting to %s...\n", TimeStamp(), bb.Server)

	// makes connection to Twitch IRC server
	bb.conn, err = net.Dial("tcp", bb.Server+":"+bb.Port)
	if nil != err {
		rgb.YPrintf("[%s] Cannot connect to %s, retrying.\n", TimeStamp(), bb.Server)
		bb.Connect()
		return
	}
	rgb.YPrintf("[%s] Connected to %s!\n", TimeStamp(), bb.Server)
	bb.startTime = time.Now()
}

// Officially disconnects the bot from the Twitch IRC server.
func (bb *KardBot) Disconnect() {
	bb.conn.Close()
	upTime := time.Now().Sub(bb.startTime).Seconds()
	rgb.YPrintf("[%s] Closed connection from %s! | Live for: %fs\n", TimeStamp(), bb.Server, upTime)
}

// Look at the channels I'm actually in
func (bb *KardBot) ActiveChannels() int {
	count := 0
	for _, channel := range bb.GlobalData.ChannelData {
		if !channel.HasLeft {
			count = count + 1
		}
	}
	return count
}

// Listens for and logs messages from chat. Responds to commands from the channel owner. The bot
// continues until it gets disconnected, told to shutdown, or forcefully shutdown.
func (bb *KardBot) HandleChat() error {
	rgb.YPrintf("[%s] Watching #%s...\n", TimeStamp(), bb.Channel)

	// reads from connection
	tp := textproto.NewReader(bufio.NewReader(bb.conn))

	// listens for chat messages
	for {
		line, err := tp.ReadLine()
		if nil != err {

			// officially disconnects the bot from the server
			bb.Disconnect()

			return errors.New("bb.Bot.HandleChat: Failed to read line from channel. Disconnected.")
		}

		// logs the response from the IRC server
		rgb.YPrintf("[%s] %s\n", TimeStamp(), line)

		if "PING :tmi.twitch.tv" == line {

			// respond to PING message with a PONG message, to maintain the connection
			bb.conn.Write([]byte("PONG :tmi.twitch.tv\r\n"))
			continue
		} else {
			matches := ConnectRegex.FindStringSubmatch(line)
			if nil != matches {
				realUserName := matches[1]
				bb.GlobalData.UpdateJoined(realUserName, false)
				bb.JoinChannel(realUserName)
			}

			matches = DirectMsgRegex.FindStringSubmatch(line)
			if nil != matches {
				userName := matches[1]
				//				msgType := matches[2]
				//				channel := matches[3]
				msg := matches[4]
				rgb.GPrintf("[%s] Direct message %s: %s\n", TimeStamp(), userName, msg)

			}

			// handle a PRIVMSG message
			matches = MsgRegex.FindStringSubmatch(line)
			if nil != matches {
				userName := matches[1]
				msgType := matches[2]
				channel := matches[3]

				switch msgType {
				case "PRIVMSG":
					msg := matches[4]
					rgb.GPrintf("[%s] %s: %s\n", TimeStamp(), userName, msg)
					rgb.GPrintf("[%s] raw line: %s\n", TimeStamp(), line)

					// parse commands from user message
					cmdMatches := CmdRegex.FindStringSubmatch(msg)
					if nil != cmdMatches {
						cmd := cmdMatches[1]
						cardCommand := ""
						commands := bb.GlobalData.ChannelData[channel].Commands
						for _, command := range commands {
							if command.CommandName == data.RandomPrompt {
								cardCommand = command.KeyWord
							}
						}
						rgb.YPrintf("[%s] Checking cmd %s against [%s]\n", TimeStamp(), cmd, bb.GlobalData.ChannelData[channel].Commands)
						switch cmd {
						case cardCommand:
							if cardCommand != "" {
								rgb.CPrintf("[%s] Card asked for by %s on %s' channel!\n", TimeStamp(), userName, channel)
								bb.Say("Your prompt is : "+bb.Prompts[rand.Intn(len(bb.Prompts))], channel)
							}
						case "join":
							if bb.GlobalData.ChannelData[channel].ControlChannel {
								rgb.CPrintf("[%s] Join asked for by %s on %s' channel!\n", TimeStamp(), userName, channel)
								bb.GlobalData.UpdateJoined(userName, false)
								bb.JoinChannel(userName)
							}
						}

						// channel-owner specific commands
						if userName == channel {
							switch cmd {
							case "tbdown":
								rgb.CPrintf(
									"[%s] Shutdown command received. Shutting down now...\n",
									TimeStamp(),
								)

								bb.Disconnect()
								return nil
							case "kcardadmin":
								magicCode := bb.GlobalData.ReadOrCreateChannelKey(channel)
								rgb.CPrintf(
									"[%s] Magic code is %s - https://karaokards.ing.martyn.berlin/admin/%s/%s\n",
									TimeStamp(),
									magicCode, userName, magicCode,
								)
								err := bb.Msg("Welcome to Karaokards, your admin panel is https://karaokards.ing.martyn.berlin/admin/"+userName+"/"+magicCode, userName)
								if err != nil {
									rgb.RPrintf("[%s] ERROR %s\n", err)
								}
								bb.Say("Ack.")
							default:
								// do nothing
							}
						}
					}
				default:
					// do nothing
					rgb.YPrintf("[%s] unknown IRC message : %s\n", TimeStamp(), line)
				}
			}
		}
		time.Sleep(bb.MsgRate)
	}
}

// Login to the IRC server
func (bb *KardBot) Login() {
	rgb.YPrintf("[%s] Logging into #%s...\n", TimeStamp(), bb.Channel)
	bb.conn.Write([]byte("PASS " + bb.IrcCredentials.Password + "\r\n"))
	bb.conn.Write([]byte("NICK " + bb.Name + "\r\n"))
}

func (bb *KardBot) LeaveChannel(channels ...string) {
	for _, channel := range channels {
		rgb.YPrintf("[%s] Leaving #%s...\n", TimeStamp(), channel)
		bb.conn.Write([]byte("PART #" + channel + "\r\n"))
		rgb.YPrintf("[%s] Left #%s as @%s!\n", TimeStamp(), channel, bb.Name)
	}
}

// Makes the bot join its pre-specified channel.
func (bb *KardBot) JoinChannel(channels ...string) {
	if len(channels) == 0 {
		channels = append(channels, bb.Channel)
	}

	for _, channel := range channels {
		rgb.YPrintf("[%s] Joining #%s...\n", TimeStamp(), channel)
		bb.conn.Write([]byte("JOIN #" + channel + "\r\n"))
		rgb.YPrintf("[%s] Joined #%s as @%s!\n", TimeStamp(), channel, bb.Name)
	}
}

// Reads from the private credentials file and stores the data in the bot's appropriate Credentials field.
func (bb *KardBot) ReadCredentials(credType string) error {

	var err error
	var credFile []byte
	// reads from the file
	if credType == "IRC" {
		credFile, err = ioutil.ReadFile(bb.IrcPrivatePath)
	} else {
		credFile, err = ioutil.ReadFile(bb.AppPrivatePath)
	}
	if nil != err {
		return err
	}

	// parses the file contents
	if credType == "IRC" {
		var creds IRCOAuthCred
		dec := json.NewDecoder(strings.NewReader(string(credFile)))
		if err = dec.Decode(&creds); nil != err && io.EOF != err {
			return err
		}

		bb.IrcCredentials = &creds
	} else {
		var creds AppOAuthCred
		dec := json.NewDecoder(strings.NewReader(string(credFile)))
		if err = dec.Decode(&creds); nil != err && io.EOF != err {
			return err
		}

		bb.AppCredentials = &creds
	}

	return nil
}

func (bb *KardBot) Msg(msg string, users ...string) error {
	if "" == msg {
		return errors.New("BasicBot.Say: msg was empty.")
	}

	// check if message is too large for IRC
	if len(msg) > 512 {
		return errors.New("BasicBot.Say: msg exceeded 512 bytes")
	}

	if len(users) == 0 {
		return errors.New("BasicBot.Say: users was empty.")
	}

	rgb.YPrintf("[%s] sending %s to users %v as @%s!\n", TimeStamp(), msg, users, bb.Name)
	for _, channel := range users {
		_, err := bb.conn.Write([]byte(fmt.Sprintf("PRIVMSG %s :%s\r\n", channel, msg)))
		rgb.YPrintf("[%s] PRIVMSG %s :%s\r\n", TimeStamp(), channel, msg)
		if nil != err {
			return err
		}
	}
	return nil
}

// Makes the bot send a message to the chat channel.
func (bb *KardBot) Say(msg string, channels ...string) error {
	if "" == msg {
		return errors.New("BasicBot.Say: msg was empty.")
	}

	// check if message is too large for IRC
	if len(msg) > 512 {
		return errors.New("BasicBot.Say: msg exceeded 512 bytes")
	}

	if len(channels) == 0 {
		channels = append(channels, bb.Channel)
	}

	rgb.YPrintf("[%s] sending %s to channels %v as @%s!\n", TimeStamp(), msg, channels, bb.Name)
	for _, channel := range channels {
		_, err := bb.conn.Write([]byte(fmt.Sprintf("PRIVMSG #%s :%s\r\n", channel, msg)))
		rgb.YPrintf("[%s] PRIVMSG #%s :%s\r\n", TimeStamp(), channel, msg)
		if nil != err {
			return err
		}
	}
	return nil
}

// Starts a loop where the bot will attempt to connect to the Twitch IRC server, then connect to the
// pre-specified channel, and then handle the chat. It will attempt to reconnect until it is told to
// shut down, or is forcefully shutdown.
func (bb *KardBot) Start() {
	err := bb.ReadCredentials("IRC")
	if nil != err {
		fmt.Println(err)
		fmt.Println("Aborting!")
		return
	}

	err = bb.ReadCredentials("App")
	if nil != err {
		fmt.Println(err)
		fmt.Println("Aborting!")
		return
	}

	for {
		bb.Connect()
		bb.Login()
		if len(bb.GlobalData.ChannelData) > 0 {
			for channelName, channelData := range bb.GlobalData.ChannelData {
				if !channelData.HasLeft {
					bb.JoinChannel(channelName)
				}
			}
		} else {
			bb.JoinChannel()
		}
		err = bb.HandleChat()
		if nil != err {

			// attempts to reconnect upon unexpected chat error
			time.Sleep(1000 * time.Millisecond)
			fmt.Println(err)
			fmt.Println("Starting bot again...")
		} else {
			return
		}
	}
}

func TimeStamp() string {
	return TimeStampFmt(UTCFormat)
}

func TimeStampFmt(format string) string {
	return time.Now().Format(format)
}