package c2

import (
	"bytes"
	"crypto/tls"
	"fmt"
	"strings"
	"text/template"

	"github.com/acarl005/stripansi"
	"github.com/bettercap/bettercap/v2/modules/events_stream"
	"github.com/bettercap/bettercap/v2/session"
	"github.com/evilsocket/islazy/log"
	"github.com/evilsocket/islazy/str"
	irc "github.com/thoj/go-ircevent"
)

type settings struct {
	server         string
	tls            bool
	tlsVerify      bool
	nick           string
	user           string
	password       string
	saslUser       string
	saslPassword   string
	operator       string
	controlChannel string
	eventsChannel  string
	outputChannel  string
}

type C2 struct {
	session.SessionModule

	settings  settings
	stream    *events_stream.EventsStream
	templates map[string]*template.Template
	channels  map[string]string
	client    *irc.Connection
	eventBus  session.EventBus
	quit      chan bool
}

type eventContext struct {
	Session *session.Session
	Event   session.Event
}

func NewC2(s *session.Session) *C2 {
	mod := &C2{
		SessionModule: session.NewSessionModule("c2", s),
		stream:        events_stream.NewEventsStream(s),
		templates:     make(map[string]*template.Template),
		channels:      make(map[string]string),
		quit:          make(chan bool),
		settings: settings{
			server:         "localhost:6697",
			tls:            true,
			tlsVerify:      false,
			nick:           "bettercap",
			user:           "bettercap",
			password:       "password",
			operator:       "admin",
			eventsChannel:  "#events",
			outputChannel:  "#events",
			controlChannel: "#events",
		},
	}

	mod.AddParam(session.NewStringParameter("c2.server",
		mod.settings.server,
		"",
		"IRC server address and port."))

	mod.AddParam(session.NewBoolParameter("c2.server.tls",
		"true",
		"Enable TLS."))

	mod.AddParam(session.NewBoolParameter("c2.server.tls.verify",
		"false",
		"Enable TLS certificate validation."))

	mod.AddParam(session.NewStringParameter("c2.operator",
		mod.settings.operator,
		"",
		"IRC nickname of the user allowed to run commands."))

	mod.AddParam(session.NewStringParameter("c2.nick",
		mod.settings.nick,
		"",
		"IRC nickname."))

	mod.AddParam(session.NewStringParameter("c2.username",
		mod.settings.user,
		"",
		"IRC username."))

	mod.AddParam(session.NewStringParameter("c2.password",
		mod.settings.password,
		"",
		"IRC server password."))

	mod.AddParam(session.NewStringParameter("c2.sasl.username",
		mod.settings.saslUser,
		"",
		"IRC SASL username."))

	mod.AddParam(session.NewStringParameter("c2.sasl.password",
		mod.settings.saslPassword,
		"",
		"IRC server SASL password."))

	mod.AddParam(session.NewStringParameter("c2.channel.output",
		mod.settings.outputChannel,
		"",
		"IRC channel to send commands output to."))

	mod.AddParam(session.NewStringParameter("c2.channel.events",
		mod.settings.eventsChannel,
		"",
		"IRC channel to send events to."))

	mod.AddParam(session.NewStringParameter("c2.channel.control",
		mod.settings.controlChannel,
		"",
		"IRC channel to receive commands from."))

	mod.AddHandler(session.NewModuleHandler("c2 on", "",
		"Start the C2 module.",
		func(args []string) error {
			return mod.Start()
		}))

	mod.AddHandler(session.NewModuleHandler("c2 off", "",
		"Stop the C2 module.",
		func(args []string) error {
			return mod.Stop()
		}))

	mod.AddHandler(session.NewModuleHandler("c2.channel.set EVENT_TYPE CHANNEL",
		"c2.channel.set ([^\\s]+) (.+)",
		"Set a specific channel to report events of this type.",
		func(args []string) error {
			eventType := args[0]
			channel := args[1]

			mod.Debug("setting channel for event %s: %v", eventType, channel)
			mod.channels[eventType] = channel
			return nil
		}))

	mod.AddHandler(session.NewModuleHandler("c2.channel.clear EVENT_TYPE",
		"c2.channel.clear ([^\\s]+)",
		"Clear the channel to use for a specific event type.",
		func(args []string) error {
			eventType := args[0]
			if _, found := mod.channels[args[0]]; found {
				delete(mod.channels, eventType)
				mod.Debug("cleared channel for %s", eventType)
			} else {
				return fmt.Errorf("channel for event %s not set", args[0])
			}
			return nil
		}))

	mod.AddHandler(session.NewModuleHandler("c2.template.set EVENT_TYPE TEMPLATE",
		"c2.template.set ([^\\s]+) (.+)",
		"Set the reporting template to use for a specific event type.",
		func(args []string) error {
			eventType := args[0]
			eventTemplate := args[1]

			parsed, err := template.New(eventType).Parse(eventTemplate)
			if err != nil {
				return err
			}

			mod.Debug("setting template for event %s: %v", eventType, parsed)
			mod.templates[eventType] = parsed
			return nil
		}))

	mod.AddHandler(session.NewModuleHandler("c2.template.clear EVENT_TYPE",
		"c2.template.clear ([^\\s]+)",
		"Clear the reporting template to use for a specific event type.",
		func(args []string) error {
			eventType := args[0]
			if _, found := mod.templates[args[0]]; found {
				delete(mod.templates, eventType)
				mod.Debug("cleared template for %s", eventType)
			} else {
				return fmt.Errorf("template for event %s not set", args[0])
			}
			return nil
		}))

	mod.Session.Events.OnPrint(mod.onPrint)

	return mod
}

func (mod *C2) Name() string {
	return "c2"
}

func (mod *C2) Description() string {
	return "A CnC module that connects to an IRC server for reporting and commands."
}

func (mod *C2) Author() string {
	return "Simone Margaritelli <evilsocket@gmail.com>"
}

func (mod *C2) Configure() (err error) {
	if mod.Running() {
		return session.ErrAlreadyStarted(mod.Name())
	}

	if err, mod.settings.server = mod.StringParam("c2.server"); err != nil {
		return err
	} else if err, mod.settings.tls = mod.BoolParam("c2.server.tls"); err != nil {
		return err
	} else if err, mod.settings.tlsVerify = mod.BoolParam("c2.server.tls.verify"); err != nil {
		return err
	} else if err, mod.settings.nick = mod.StringParam("c2.nick"); err != nil {
		return err
	} else if err, mod.settings.user = mod.StringParam("c2.username"); err != nil {
		return err
	} else if err, mod.settings.password = mod.StringParam("c2.password"); err != nil {
		return err
	} else if err, mod.settings.saslUser = mod.StringParam("c2.sasl.username"); err != nil {
		return err
	} else if err, mod.settings.saslPassword = mod.StringParam("c2.sasl.password"); err != nil {
		return err
	} else if err, mod.settings.operator = mod.StringParam("c2.operator"); err != nil {
		return err
	} else if err, mod.settings.eventsChannel = mod.StringParam("c2.channel.events"); err != nil {
		return err
	} else if err, mod.settings.controlChannel = mod.StringParam("c2.channel.control"); err != nil {
		return err
	} else if err, mod.settings.outputChannel = mod.StringParam("c2.channel.output"); err != nil {
		return err
	}

	mod.eventBus = mod.Session.Events.Listen()

	mod.client = irc.IRC(mod.settings.nick, mod.settings.user)

	if log.Level == log.DEBUG {
		mod.client.VerboseCallbackHandler = true
		mod.client.Debug = true
	}

	mod.client.Password = mod.settings.password
	mod.client.UseTLS = mod.settings.tls
	mod.client.TLSConfig = &tls.Config{
		InsecureSkipVerify: !mod.settings.tlsVerify,
	}

	if mod.settings.saslUser != "" || mod.settings.saslPassword != "" {
		mod.client.SASLLogin = mod.settings.saslUser
		mod.client.SASLPassword = mod.settings.saslPassword
		mod.client.UseSASL = true
	}

	mod.client.AddCallback("PRIVMSG", func(event *irc.Event) {
		channel := event.Arguments[0]
		message := event.Message()
		from := event.Nick

		if from != mod.settings.operator {
			mod.client.Privmsg(event.Nick, "nope")
			return
		}

		if channel != mod.settings.controlChannel && channel != mod.settings.nick {
			mod.Debug("from:%s on:%s - '%s'", from, channel, message)
			return
		}

		mod.Debug("from:%s on:%s - '%s'", from, channel, message)

		parts := strings.SplitN(message, " ", 2)
		cmd := parts[0]
		args := ""
		if len(parts) > 1 {
			args = parts[1]
		}

		if cmd == "join" {
			mod.client.Join(args)
		} else if cmd == "part" {
			mod.client.Part(args)
		} else if cmd == "nick" {
			mod.client.Nick(args)
		} else if err = mod.Session.Run(message); err == nil {

		} else {
			mod.client.Privmsgf(event.Nick, "error: %v", stripansi.Strip(err.Error()))
		}
	})

	mod.client.AddCallback("001", func(e *irc.Event) {
		mod.Debug("got 101")
		mod.client.Join(mod.settings.controlChannel)
		mod.client.Join(mod.settings.outputChannel)
		mod.client.Join(mod.settings.eventsChannel)
	})

	return mod.client.Connect(mod.settings.server)
}

func (mod *C2) onPrint(format string, args ...interface{}) {
	if !mod.Running() {
		return
	}

	msg := stripansi.Strip(str.Trim(fmt.Sprintf(format, args...)))

	for _, line := range strings.Split(msg, "\n") {
		mod.client.Privmsg(mod.settings.outputChannel, line)
	}
}

func (mod *C2) onEvent(e session.Event) {
	if mod.Session.EventsIgnoreList.Ignored(e) {
		return
	}

	// default channel or event specific channel?
	channel := mod.settings.eventsChannel
	if custom, found := mod.channels[e.Tag]; found {
		channel = custom
	}

	var out bytes.Buffer
	if tpl, found := mod.templates[e.Tag]; found {
		// use a custom template to render this event
		if err := tpl.Execute(&out, eventContext{
			Session: mod.Session,
			Event:   e,
		}); err != nil {
			fmt.Fprintf(&out, "%v", err)
		}
	} else {
		// use the default view to render this event
		mod.stream.Render(&out, e)
	}

	// make sure colors and in general bash escape sequences are removed
	msg := stripansi.Strip(str.Trim(string(out.Bytes())))

	mod.client.Privmsg(channel, msg)
}

func (mod *C2) Start() error {
	if err := mod.Configure(); err != nil {
		return err
	}

	return mod.SetRunning(true, func() {
		mod.Info("started")

		for mod.Running() {
			var e session.Event
			select {
			case e = <-mod.eventBus:
				mod.onEvent(e)

			case <-mod.quit:
				mod.Debug("got quit")
				return
			}
		}
	})
}

func (mod *C2) Stop() error {
	return mod.SetRunning(false, func() {
		mod.quit <- true
		mod.Session.Events.Unlisten(mod.eventBus)
		mod.client.Quit()
		mod.client.Disconnect()
	})
}
