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            `json:"-"` // Client's current nick
	OnExit         func()            `json:"-"` // Called on exit
	Socket         net.Conn          `json:"-"`
	Reader         *bufio.Reader     `json:"-"`
	ReadChannel    chan IRCMsg       `json:"-"`
	ReadEvents     []string          `json:"-"`
	WriteChannel   chan IRCWrite     `json:"-"`
	DelChannel     chan string       `json:"-"` // For deleting channel or nicks that are missing.
	Registered     bool              `json:"-"`
	ISupport       map[string]string `json:"-"` // 005
	wg             sync.WaitGroup    `json:"-"`
	Mutex          sync.Mutex        `json:"-"`
}

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
}

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

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

	// Set sensible defaults (if not provided),
	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 = 2000
	}

	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.  You can't received IRCMsg messages.")
	}

	if Config.UseTLS {
		var tlsConfig tls.Config

		if !Config.Insecure {
			// Use system default CA Certificates
			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.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.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))

	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"
	_, err = Config.Socket.Write([]byte(output))

	return err
}

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

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

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

	// 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")
					Config.PriorityWrite("QUIT :Received SIGINT")
				}
				continue

			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)):
				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")
					Config.PriorityWrite("QUIT :Received SIGINT")
				}
				continue

			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)
					}
				}

			}
		}
	}
}

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
}

// PriorityWrite: Send output command to server immediately.
func (Config *IRCConfig) PriorityWrite(output string) {
	Config.WriteChannel <- IRCWrite{To: "", Output: output}
}

// WriteTo: Send throttled output command using "to" to throttle.
func (Config *IRCConfig) WriteTo(to string, output string) {
	Config.WriteChannel <- IRCWrite{To: to, Output: output}
}

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

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

// Action: Send PRIVMSG CTCP ACTION, uses WriteTo to throttle.
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

	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":
				// Convert Cmd "PRIVMSG" to "CTCP" or "ACTION".
				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

		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 :nick, type /msg NickServ IDENTIFY password.  Otherwise,
			*/
			if (msg.From == "NickServ") && (msg.Cmd == "NOTICE") {
				if strings.Contains(msg.Msg, "IDENTIFY") && Config.Password != "" {
					Config.PriorityWrite(fmt.Sprintf("NS IDENTIFY %s", Config.Password))
				}
			}

			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"}

				if len(Config.AutoJoin) > 0 {
					Config.PriorityWrite("JOIN " + strings.Join(Config.AutoJoin, ","))
				}
			}
		}

		if msg.Cmd == "KICK" {
			// We were kicked, is channel in AutoJoin?
			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) })
				}
			}
		}

		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 == "" {
				// We can't register with services, so join the AutJoin channels.
				if len(Config.AutoJoin) > 0 {
					Config.PriorityWrite("JOIN " + strings.Join(Config.AutoJoin, ","))
				}
			}
		}

		if msg.Cmd == "NICK" {
			// Handle nick changes from the server

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