package ircclient

import (
	"bufio"
	"crypto/tls"
	"crypto/x509"
	"encoding/base64"
	"fmt"
	"io"
	"log"
	"math/rand"
	"net"
	"os"
	"os/signal"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"
)

const VERSION string = "red-green.com/irc-client 0.1.0"

func StrInArray(strings []string, str string) bool {
	for _, s := range strings {
		if s == str {
			return true
		}
	}
	return false
}

type IRCMsg struct {
	MsgParts []string
	From     string
	To       string
	Cmd      string
	Msg      string
}

func NameLower(name string) string {
	// uppercase:  []\-
	// lowercase:  {}|^

	var result string = strings.ToLower(name)
	result = strings.ReplaceAll(result, "[", "{")
	result = strings.ReplaceAll(result, "]", "}")
	result = strings.ReplaceAll(result, "\\", "|")
	result = strings.ReplaceAll(result, "-", "^")
	return result
}

// Check to see if nicks or channels match according to IRC rules.
func Match(name1 string, name2 string) bool {
	return NameLower(name1) == NameLower(name2)
}

// Strip out the NICK part of the From message.
// :NickServ!services@services.red-green.com => NickServ
func IRCNick(from string) string {
	if from[0] == ':' {
		from = from[1:]
	}
	var pos int = strings.Index(from, "!")
	if pos != -1 {
		from = from[:pos]
	}
	return from
}

/*
IRCParse - split line into IRCMsg

Everything after " :" is the Msg.
Everything before " :" is split into MsgParts[].

If >= 3 MsgParts {
	To = MsgParts[2]
}
if >= 2 MsgParts {
	From = IrcNic(MsgParts[0])
	Cmd = MsgParts[1]
} else {
	Cmd = MsgParts[0]
}

Example Messages:

:irc.red-green.com 001 test :Welcome to IRC
^From              ^Cmd     ^Msg
                       ^To
^0                 ^1  ^2   MsgParts[]

PING :1234567890
^Cmd ^Msg
^0 MsgParts[]



*/
func IRCParse(line string) IRCMsg {
	var pos int = strings.Index(line, " :")
	var results IRCMsg

	if pos != -1 {
		// Message is everything after " :"
		results.Msg = line[pos+2:]
		line = line[:pos]
	}

	results.MsgParts = strings.Split(line, " ")

	if len(results.MsgParts) >= 2 {
		results.From = IRCNick(results.MsgParts[0])
		results.Cmd = results.MsgParts[1]
	} else {
		results.Cmd = results.MsgParts[0]
	}
	if len(results.MsgParts) >= 3 {
		results.To = results.MsgParts[2]
	}
	return results
}

/*
func oldIRCParse(line string) []string {
	var pos int = strings.Index(line, " :")
	var message string

	if pos != -1 {
		message = line[pos+2:]
		line = line[:pos]
	}
	var results []string
	results = strings.Split(line, " ")
	if message != "" {
		results = append(results, message)
	}
	return results
}
*/

type IRCWrite struct {
	To     string
	Output string
}

type IRCConfig struct {
	Port           int      `json:"Port"`
	Hostname       string   `json:"Hostname"`
	UseTLS         bool     `json:"UseTLS"`   // Use TLS Secure connection
	UseSASL        bool     `json:"UseSASL"`  // Authenticate via SASL
	Insecure       bool     `json:"Insecure"` // Allow self-signed certificates
	Nick           string   `json:"Nick"`
	Username       string   `json:"Username"`
	Realname       string   `json:"Realname"`
	Password       string   `json:"Password"`       // Password for nickserv
	ServerPassword string   `json:"ServerPassword"` // Password for server
	AutoJoin       []string `json:"AutoJoin"`       // Channels to auto-join
	RejoinDelay    int      `json:"RejoinDelay"`    // ms to rejoin
	Version        string   `json:"Version"`        // Version displayed
	Flood_Num      int      `json:"FloodNum"`       // Number of lines sent before considered a flood
	Flood_Time     int      `json:"FloodTime"`      // Number of Seconds to track previous messages
	Flood_Delay    int      `json:"FloodDelay"`     // Delay between sending when flood protection on (Milliseconds)
	Debug_Output   bool     `json:"Debug"`
	MyNick         string
	OnExit         func()
	Socket         net.Conn
	Reader         *bufio.Reader
	ReadChannel    chan IRCMsg
	ReadEvents     []string
	WriteChannel   chan IRCWrite
	DelChannel     chan string
	Registered     bool
	ISupport       map[string]string // 005
	wg             sync.WaitGroup
	Mutex          sync.Mutex
}

func (Config *IRCConfig) GetNick() string {
	Config.Mutex.Lock()
	defer Config.Mutex.Unlock()
	return Config.MyNick
}

func (Config *IRCConfig) SetNick(nick string) {
	Config.Mutex.Lock()
	defer Config.Mutex.Unlock()
	Config.MyNick = nick
}

// Writer       *bufio.Writer

func (Config *IRCConfig) IsAuto(ch string) bool {
	return StrInArray(Config.AutoJoin, ch)
}

func (Config *IRCConfig) Connect() bool {
	var err error

	if Config.Flood_Num == 0 {
		Config.Flood_Num = 5
	}
	if Config.Flood_Time == 0 {
		Config.Flood_Time = 10
	}

	if Config.Flood_Delay == 0 {
		Config.Flood_Delay = 1000
	}

	if Config.UseSASL {
		if !Config.UseTLS {
			log.Println("Can't UseSASL if not using UseTLS")
			Config.UseSASL = false
		}
	}

	Config.Registered = false

	if Config.ReadChannel == nil {
		log.Println("Warning: ReadChannel is nil.")
	}

	if Config.UseTLS {
		var tlsConfig tls.Config

		if !Config.Insecure {
			certPool := x509.NewCertPool()
			tlsConfig.ClientCAs = certPool
			tlsConfig.InsecureSkipVerify = false
		} else {
			tlsConfig.InsecureSkipVerify = true
		}

		Config.Socket, err = tls.Dial("tcp", Config.Hostname+":"+strconv.Itoa(Config.Port), &tlsConfig)
		if err != nil {
			log.Fatal("tls.Dial:", err)
		}
		// Config.Writer = bufio.NewWriter(Config.TLSSocket)
		Config.Reader = bufio.NewReader(Config.Socket)
	} else {
		Config.Socket, err = net.Dial("tcp", Config.Hostname+":"+strconv.Itoa(Config.Port))
		if err != nil {
			log.Fatal("net.Dial:", err)
		}
		// Config.Writer = bufio.NewWriter(Config.Socket)
		Config.Reader = bufio.NewReader(Config.Socket)
	}

	// WriteChannel may contain a message when we're trying to PriorityWrite from sigChannel.
	Config.WriteChannel = make(chan IRCWrite, 3)
	Config.DelChannel = make(chan string)
	Config.ISupport = make(map[string]string)

	// We are connected.
	go Config.WriterRoutine()
	Config.wg.Add(1)

	// Registration
	if Config.UseTLS && Config.UseSASL {
		Config.PriorityWrite("CAP REQ :sasl")
	}

	if Config.ServerPassword != "" {
		Config.PriorityWrite(fmt.Sprintf("PASS %s", Config.ServerPassword))
	}

	// Register nick
	Config.MyNick = Config.Nick
	Config.PriorityWrite(fmt.Sprintf("NICK %s", Config.Nick))
	Config.PriorityWrite(fmt.Sprintf("USER %s 0 * :%s", Config.Username, Config.Realname))
	// Config.Writer.Flush()

	go Config.ReaderRoutine()
	Config.wg.Add(1)
	return true
}

// Low level write to [TLS]Socket routine.
func (Config *IRCConfig) write(output string) error {
	var err error
	if Config.Debug_Output {
		log.Println(">>", output)
	}
	output += "\r\n"
	/*
		if Config.UseTLS {
			_, err = Config.TLSSocket.Write([]byte(output))
		} else {
	*/
	_, err = Config.Socket.Write([]byte(output))

	return err
}

func (Config *IRCConfig) WriterRoutine() {
	var err error
	var throttle ThrottleBuffer
	var Flood FloodTrack

	defer Config.wg.Done()
	throttle.init()

	Flood.Init(Config.Flood_Num, Config.Flood_Time)

	// signal handler
	var sigChannel chan os.Signal = make(chan os.Signal, 1)
	signal.Notify(sigChannel, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)

	// Change this into a select with timeout.
	// Timeout, if there's something to be buffered.

	var gotSignal bool

	for {
		if throttle.Life_sucks {
			select {
			case output := <-Config.WriteChannel:
				if output.To == "" {
					err = Config.write(output.Output)
					if err != nil {
						log.Println("Writer:", err)
						return
					}
					continue
				}

				throttle.push(output.To, output.Output)

			case <-sigChannel:
				if !gotSignal {
					gotSignal = true
					log.Println("SIGNAL")
					/*
						// This should be handled now by Close().
						if Config.OnExit != nil {
							Config.OnExit()
						}
					*/
					Config.PriorityWrite("QUIT :Received SIGINT")
				}
				// return
				continue
				// Config.Close()
				// return
				//os.Exit(2)

			case remove := <-Config.DelChannel:
				if Config.Debug_Output {
					log.Printf("Remove: [%s]\n", remove)
				}
				throttle.delete(remove)

			case <-time.After(time.Millisecond * time.Duration(Config.Flood_Delay)):
				// Send from the buffer
				// debugging the flood buffer
				// log.Printf("targets: %#v\n", targets)
				// log.Printf("last: %d\n", last)
				// log.Printf("buffer: %#v\n", buffer)

				var msg string = throttle.pop()
				err = Config.write(msg)
				if err != nil {
					log.Println("Writer:", err)
				}
			}
		} else {
			// Life is good.
			select {
			case <-sigChannel:
				if !gotSignal {
					gotSignal = true
					log.Println("SIGNAL")
					/*
						// This should now be handled by Close().
						if Config.OnExit != nil {
							Config.OnExit()
						}
					*/
					Config.PriorityWrite("QUIT :Received SIGINT")
				}
				// return
				continue
				// Config.Close()
				// return
				// os.Exit(2)

			case remove := <-Config.DelChannel:
				if Config.Debug_Output {
					log.Printf("Remove: [%s]\n", remove)
				}
				throttle.delete(remove)

			case output := <-Config.WriteChannel:
				if output.To == "" {
					err = Config.write(output.Output)
					if err != nil {
						log.Println("Writer:", err)
						return
					}
					continue
				}
				if Flood.Full() {
					throttle.push(output.To, output.Output)
				} else {
					// Flood limits not reached
					Flood.Save()
					err = Config.write(output.Output)
					if err != nil {
						log.Println("Writer:", err)
					}
				}

			}
		}
	}

	log.Println("~WriterRoutine")
}

func (Config *IRCConfig) Close() {
	Config.Socket.Close()
	Config.PriorityWrite("")
	if Config.OnExit != nil {
		Config.OnExit()
	}
	Config.wg.Wait()
}

func RandomNick(nick string) string {
	var result string = nick + "-"
	result += strconv.Itoa(rand.Intn(1000))
	return result
}

func (Config *IRCConfig) PriorityWrite(output string) {
	Config.WriteChannel <- IRCWrite{To: "", Output: output}
}

func (Config *IRCConfig) WriteTo(to string, output string) {
	Config.WriteChannel <- IRCWrite{To: to, Output: output}
}

func (Config *IRCConfig) Msg(to string, message string) {
	Config.WriteTo(to, fmt.Sprintf("PRIVMSG %s :%s", to, message))
}

func (Config *IRCConfig) Notice(to string, message string) {
	Config.WriteTo(to, fmt.Sprintf("NOTICE %s :%s", to, message))
}

func (Config *IRCConfig) Action(to string, message string) {
	Config.WriteTo(to, fmt.Sprintf("PRIVMSG %s :\x01ACTION %s\x01", to, message))
}

func (Config *IRCConfig) ReaderRoutine() {
	defer Config.wg.Done()
	var registering bool
	// var identified bool

	for {
		var line string
		var err error
		var results IRCMsg

		line, err = Config.Reader.ReadString('\n')
		if err == nil {
			line = strings.Trim(line, "\r\n")
			if Config.Debug_Output {
				log.Println("<<", line)
			}

			results = IRCParse(line)

			switch results.Cmd {
			case "PING":
				// Ping from Server
				Config.PriorityWrite("PONG " + results.Msg)
			case "005":
				var support string
				for _, support = range results.MsgParts[3:] {
					if strings.Contains(support, "=") {
						var suppart []string = strings.Split(support, "=")
						Config.ISupport[suppart[0]] = suppart[1]
					} else {
						Config.ISupport[support] = ""
					}
				}
			case "433":
				if !registering {
					// Nick already in use!
					var newNick string = RandomNick(Config.Nick)
					Config.MyNick = newNick
					Config.PriorityWrite("NICK " + newNick)
				}
			case "PRIVMSG":
				if (results.Msg[0] == '\x01') && (results.Msg[len(results.Msg)-1] == '\x01') {
					// ACTION
					results.Cmd = "CTCP"
					results.Msg = results.Msg[1 : len(results.Msg)-1]
					if Config.Debug_Output {
						log.Println("CTCP:", results.Msg)
					}

					// Process CTCP commands
					if strings.HasPrefix(results.Msg, "ACTION ") {
						results.Cmd = "ACTION"
						results.Msg = results.Msg[7:]
					}

					if strings.HasPrefix(results.Msg, "PING ") {
						Config.WriteTo(IRCNick(results.From),
							fmt.Sprintf("NOTICE %s :\x01PING %s\x01",
								IRCNick(results.From),
								results.Msg[5:]))
					}

					if results.Msg == "VERSION" {
						// Send version reply
						var version string
						if Config.Version != "" {
							version = Config.Version + " " + VERSION
						} else {
							version = VERSION
						}
						Config.WriteTo(IRCNick(results.From),
							fmt.Sprintf("NOTICE %s :\x01VERSION %s\x01",
								IRCNick(results.From), version))
					}

					if results.Msg == "TIME" {
						// Send time reply
						var now time.Time = time.Now()
						Config.WriteTo(IRCNick(results.From),
							fmt.Sprintf("NOTICE %s :\x01TIME %s\x01",
								IRCNick(results.From),
								now.Format(time.ANSIC)))
					}

				}

			}
		} else {
			// This is likely, 2022/04/05 10:11:41 ReadString: EOF
			if err != io.EOF {
				log.Println("ReadString:", err)
			}
			close(Config.ReadChannel)
			return
		}

		var msg IRCMsg = results
		/*
			IRCMsg{MsgParts: results}
			if len(results) >= 3 {
				msg.From = IRCNick(results[0])
				msg.Cmd = results[1]
				msg.To = results[2]
				if len(results) >= 4 {
					msg.Msg = results[3]
				}
			} else {
				msg.Cmd = results[0]
			}
		*/

		if !Config.Debug_Output {
			if msg.Cmd == "ERROR" || msg.Cmd[0] == '4' || msg.Cmd[0] == '5' {
				// Always log errors.
				log.Println("<<", line)
			}
		}

		if msg.Cmd == "401" || msg.Cmd == "404" {
			// No such nick/channel
			log.Printf("Remove %s from buffer.", msg.MsgParts[3])
			Config.DelChannel <- msg.MsgParts[3]
		}

		if !Config.Registered {
			// We're not registered yet

			// Answer the queries for SASL authentication
			if msg.Cmd == "CAP" && msg.MsgParts[3] == "ACK" {
				Config.PriorityWrite("AUTHENTICATE PLAIN")
			}

			if msg.Cmd == "CAP" && msg.MsgParts[3] == "NAK" {
				// SASL Authentication failed/not available.
				Config.PriorityWrite("CAP END")
				Config.UseSASL = false
			}

			// msg.Cmd == "AUTH..."
			if msg.MsgParts[0] == "AUTHENTICATE" && msg.MsgParts[1] == "+" {
				var userpass string = fmt.Sprintf("\x00%s\x00%s", Config.Nick, Config.Password)
				var b64 string = base64.StdEncoding.EncodeToString([]byte(userpass))
				Config.PriorityWrite("AUTHENTICATE " + b64)
			}

			if msg.Cmd == "903" {
				// Success SASL
				Config.PriorityWrite("CAP END")
				Config.Registered = true
			}

			if msg.Cmd == "904" {
				// Failed SASL
				Config.PriorityWrite("CAP END")
				// Should we exit here?
				Config.UseSASL = false
			}

			/*
				2022/04/06 19:12:11 << :NickServ!services@services.red-green.com NOTICE meow :This nickname is registered and protected.  If it is your
				2022/04/06 19:12:11 << :NickServ!services@services.red-green.com NOTICE meow :nick, type /msg NickServ IDENTIFY password.  Otherwise,
				2022/04/06 19:12:11 << :NickServ!services@services.red-green.com NOTICE meow :please choose a different nick.
			*/
			if (msg.From == "NickServ") && (msg.Cmd == "NOTICE") {
				if strings.Contains(msg.Msg, "IDENTIFY") && Config.Password != "" {
					Config.PriorityWrite(fmt.Sprintf("NS IDENTIFY %s", Config.Password))
				}
				// :NickServ!services@services.red-green.com NOTICE meow :Password accepted - you are now recognized.
			}

			if !Config.UseSASL && (msg.Cmd == "900") {
				Config.Registered = true
			}

		}

		// This is a better way of knowing when we've identified for services
		if (msg.Cmd == "MODE") && (msg.To == Config.MyNick) {
			// This should probably be look for + and contains "r"
			if (msg.Msg[0] == '+') && (strings.Contains(msg.Msg, "r")) {
				Config.ReadChannel <- IRCMsg{Cmd: "Identified"}
				// identified = true
			}
			if len(Config.AutoJoin) > 0 {
				Config.PriorityWrite("JOIN " + strings.Join(Config.AutoJoin, ","))
			}
		}

		if msg.Cmd == "KICK" {
			// Were we kicked, is channel in AutoJoin?
			// 2022/04/13 20:02:52 << :bugz!bugz@furball.red-green.com KICK #bugz meow-bot :bugz
			// Msg: ircclient.IRCMsg{MsgParts:[]string{":bugz!bugz@furball.red-green.com", "KICK", "#bugz", "meow-bot", "bugz"}, From:"bugz", To:"#bugz", Cmd:"KICK", Msg:"meow-bot"}
			if msg.MsgParts[3] == Config.MyNick {
				if Config.IsAuto(msg.To) {
					// Yes, we were kicked from AutoJoin channel
					time.AfterFunc(time.Duration(Config.RejoinDelay)*time.Millisecond, func() { Config.WriteTo(msg.To, "JOIN "+msg.To) })
				}
			}
		}

		/*
			// Needs rate limit, it doesn't always work.  (if they're not on the HOP+ list)
				if msg.Cmd == "474" && identified {

						:irc.red-green.com 474 meow-bot #chat :Cannot join channel (+b)
						Msg: ircclient.IRCMsg{MsgParts:[]string{":irc.red-green.com", "474", "meow-bot", "#chat", "Cannot join channel (+b)"}, From:"irc.red-green.com", To:"meow-bot", Cmd:"474", Msg:"#chat"}
					  :ChanServ!services@services.red-green.com NOTICE meow-bot :Access denied.
					  Msg: ircclient.IRCMsg{MsgParts:[]string{":ChanServ!services@services.red-green.com", "NOTICE", "meow-bot", "Access denied."}, From:"ChanServ", To:"meow-bot", Cmd:"NOTICE", Msg:"Access denied."}
					if Config.IsAuto(msg.MsgParts[3]) {
						Config.PriorityWrite(fmt.Sprintf("CS UNBAN %s", msg.MsgParts[3]))
						time.AfterFunc(time.Duration(Config.RejoinDelay)*time.Millisecond, func() { Config.WriteTo(msg.To, "JOIN "+msg.MsgParts[3]) })
					}
				}
		*/

		if Config.ReadChannel != nil {
			Config.ReadChannel <- msg
		}

		if (msg.Cmd == "376") || (msg.Cmd == "422") {
			// End MOTD, or MOTD Missing
			var reg IRCMsg = IRCMsg{Cmd: "EndMOTD"}
			Config.ReadChannel <- reg
			registering = true
			if Config.Password == "" {
				if len(Config.AutoJoin) > 0 {
					Config.PriorityWrite("JOIN " + strings.Join(Config.AutoJoin, ","))
				}
			}
		}

		if msg.Cmd == "NICK" {
			// :meow NICK :meow-bot

			if msg.From == Config.MyNick {
				Config.SetNick(msg.Msg)
				if Config.Debug_Output {
					log.Println("Nick is now:", Config.MyNick)
				}
			}
		}
	}
}