package bmumble

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"io/ioutil"
	"net"
	"strconv"
	"time"

	"layeh.com/gumble/gumble"
	"layeh.com/gumble/gumbleutil"

	"github.com/42wim/matterbridge/bridge"
	"github.com/42wim/matterbridge/bridge/config"
	"github.com/42wim/matterbridge/bridge/helper"
	stripmd "github.com/writeas/go-strip-markdown"

	// We need to import the 'data' package as an implicit dependency.
	// See: https://godoc.org/github.com/paulrosania/go-charset/charset
	_ "github.com/paulrosania/go-charset/data"
)

type Bmumble struct {
	client             *gumble.Client
	Nick               string
	Host               string
	Channel            *uint32
	local              chan config.Message
	running            chan error
	connected          chan gumble.DisconnectEvent
	serverConfigUpdate chan gumble.ServerConfigEvent
	serverConfig       gumble.ServerConfigEvent
	tlsConfig          tls.Config

	*bridge.Config
}

func New(cfg *bridge.Config) bridge.Bridger {
	b := &Bmumble{}
	b.Config = cfg
	b.Nick = b.GetString("Nick")
	b.local = make(chan config.Message)
	b.running = make(chan error)
	b.connected = make(chan gumble.DisconnectEvent)
	b.serverConfigUpdate = make(chan gumble.ServerConfigEvent)
	return b
}

func (b *Bmumble) Connect() error {
	b.Log.Infof("Connecting %s", b.GetString("Server"))
	host, portstr, err := net.SplitHostPort(b.GetString("Server"))
	if err != nil {
		return err
	}
	b.Host = host
	_, err = strconv.Atoi(portstr)
	if err != nil {
		return err
	}

	if err = b.buildTLSConfig(); err != nil {
		return err
	}

	go b.doSend()
	go b.connectLoop()
	err = <-b.running
	return err
}

func (b *Bmumble) Disconnect() error {
	return b.client.Disconnect()
}

func (b *Bmumble) JoinChannel(channel config.ChannelInfo) error {
	cid, err := strconv.ParseUint(channel.Name, 10, 32)
	if err != nil {
		return err
	}
	channelID := uint32(cid)
	if b.Channel != nil && *b.Channel != channelID {
		b.Log.Fatalf("Cannot join channel ID '%d', already joined to channel ID %d", channelID, *b.Channel)
		return errors.New("the Mumble bridge can only join a single channel")
	}
	b.Channel = &channelID
	return b.doJoin(b.client, channelID)
}

func (b *Bmumble) Send(msg config.Message) (string, error) {
	// Only process text messages
	b.Log.Debugf("=> Received local message %#v", msg)
	if msg.Event != "" && msg.Event != config.EventUserAction {
		return "", nil
	}

	attachments := b.extractFiles(&msg)
	b.local <- msg
	for _, a := range attachments {
		b.local <- a
	}
	return "", nil
}

func (b *Bmumble) buildTLSConfig() error {
	b.tlsConfig = tls.Config{}
	// Load TLS client certificate keypair required for registered user authentication
	if cpath := b.GetString("TLSClientCertificate"); cpath != "" {
		if ckey := b.GetString("TLSClientKey"); ckey != "" {
			cert, err := tls.LoadX509KeyPair(cpath, ckey)
			if err != nil {
				return err
			}
			b.tlsConfig.Certificates = []tls.Certificate{cert}
		}
	}
	// Load TLS CA used for server verification.  If not provided, the Go system trust anchor is used
	if capath := b.GetString("TLSCACertificate"); capath != "" {
		ca, err := ioutil.ReadFile(capath)
		if err != nil {
			return err
		}
		b.tlsConfig.RootCAs = x509.NewCertPool()
		b.tlsConfig.RootCAs.AppendCertsFromPEM(ca)
	}
	b.tlsConfig.InsecureSkipVerify = b.GetBool("SkipTLSVerify")
	return nil
}

func (b *Bmumble) connectLoop() {
	firstConnect := true
	for {
		err := b.doConnect()
		if firstConnect {
			b.running <- err
		}
		if err != nil {
			b.Log.Errorf("Connection to server failed: %#v", err)
			if firstConnect {
				break
			} else {
				b.Log.Info("Retrying in 10s")
				time.Sleep(10 * time.Second)
				continue
			}
		}
		firstConnect = false
		d := <-b.connected
		switch d.Type {
		case gumble.DisconnectError:
			b.Log.Errorf("Lost connection to the server (%s), attempting reconnect", d.String)
			continue
		case gumble.DisconnectKicked:
			b.Log.Errorf("Kicked from the server (%s), attempting reconnect", d.String)
			continue
		case gumble.DisconnectBanned:
			b.Log.Errorf("Banned from the server (%s), not attempting reconnect", d.String)
			close(b.connected)
			close(b.running)
			return
		case gumble.DisconnectUser:
			b.Log.Infof("Disconnect successful")
			close(b.connected)
			close(b.running)
			return
		}
	}
}

func (b *Bmumble) doConnect() error {
	// Create new gumble config and attach event handlers
	gumbleConfig := gumble.NewConfig()
	gumbleConfig.Attach(gumbleutil.Listener{
		ServerConfig: b.handleServerConfig,
		TextMessage:  b.handleTextMessage,
		Connect:      b.handleConnect,
		Disconnect:   b.handleDisconnect,
		UserChange:   b.handleUserChange,
	})
	gumbleConfig.Username = b.GetString("Nick")
	if password := b.GetString("Password"); password != "" {
		gumbleConfig.Password = password
	}

	client, err := gumble.DialWithDialer(new(net.Dialer), b.GetString("Server"), gumbleConfig, &b.tlsConfig)
	if err != nil {
		return err
	}
	b.client = client
	return nil
}

func (b *Bmumble) doJoin(client *gumble.Client, channelID uint32) error {
	channel, ok := client.Channels[channelID]
	if !ok {
		return fmt.Errorf("no channel with ID %d", channelID)
	}
	client.Self.Move(channel)
	return nil
}

func (b *Bmumble) doSend() {
	// Message sending loop that makes sure server-side
	// restrictions and client-side message traits don't conflict
	// with each other.
	for {
		select {
		case serverConfig := <-b.serverConfigUpdate:
			b.Log.Debugf("Received server config update: AllowHTML=%#v, MaximumMessageLength=%#v", serverConfig.AllowHTML, serverConfig.MaximumMessageLength)
			b.serverConfig = serverConfig
		case msg := <-b.local:
			b.processMessage(&msg)
		}
	}
}

func (b *Bmumble) processMessage(msg *config.Message) {
	b.Log.Debugf("Processing message %s", msg.Text)

	allowHTML := true
	if b.serverConfig.AllowHTML != nil {
		allowHTML = *b.serverConfig.AllowHTML
	}

	// If this is a specially generated image message, send it unmodified
	if msg.Event == "mumble_image" {
		if allowHTML {
			b.client.Self.Channel.Send(msg.Username+msg.Text, false)
		} else {
			b.Log.Info("Can't send image, server does not allow HTML messages")
		}
		return
	}

	// Don't process empty messages
	if len(msg.Text) == 0 {
		return
	}
	// If HTML is allowed, convert markdown into HTML, otherwise strip markdown
	if allowHTML {
		msg.Text = helper.ParseMarkdown(msg.Text)
	} else {
		msg.Text = stripmd.Strip(msg.Text)
	}

	// If there is a maximum message length, split and truncate the lines
	var msgLines []string
	if maxLength := b.serverConfig.MaximumMessageLength; maxLength != nil {
		msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username))
	} else {
		msgLines = helper.GetSubLines(msg.Text, 0)
	}
	// Send the individual lindes
	for i := range msgLines {
		b.client.Self.Channel.Send(msg.Username+msgLines[i], false)
	}
}