package config

import (
	"bytes"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/fsnotify/fsnotify"
	"github.com/sirupsen/logrus"
	"github.com/spf13/viper"
)

const (
	EventJoinLeave         = "join_leave"
	EventTopicChange       = "topic_change"
	EventFailure           = "failure"
	EventFileFailureSize   = "file_failure_size"
	EventAvatarDownload    = "avatar_download"
	EventRejoinChannels    = "rejoin_channels"
	EventUserAction        = "user_action"
	EventMsgDelete         = "msg_delete"
	EventFileDelete        = "file_delete"
	EventAPIConnected      = "api_connected"
	EventUserTyping        = "user_typing"
	EventGetChannelMembers = "get_channel_members"
	EventNoticeIRC         = "notice_irc"
)

const ParentIDNotFound = "msg-parent-not-found"

type Message struct {
	Text      string    `json:"text"`
	Channel   string    `json:"channel"`
	Username  string    `json:"username"`
	UserID    string    `json:"userid"` // userid on the bridge
	Avatar    string    `json:"avatar"`
	Account   string    `json:"account"`
	Event     string    `json:"event"`
	Protocol  string    `json:"protocol"`
	Gateway   string    `json:"gateway"`
	ParentID  string    `json:"parent_id"`
	Timestamp time.Time `json:"timestamp"`
	ID        string    `json:"id"`
	Extra     map[string][]interface{}
}

func (m Message) ParentNotFound() bool {
	return m.ParentID == ParentIDNotFound
}

func (m Message) ParentValid() bool {
	return m.ParentID != "" && !m.ParentNotFound()
}

type FileInfo struct {
	Name     string
	Data     *[]byte
	Comment  string
	URL      string
	Size     int64
	Avatar   bool
	SHA      string
	NativeID string
}

type ChannelInfo struct {
	Name        string
	Account     string
	Direction   string
	ID          string
	SameChannel map[string]bool
	Options     ChannelOptions
}

type ChannelMember struct {
	Username    string
	Nick        string
	UserID      string
	ChannelID   string
	ChannelName string
}

type ChannelMembers []ChannelMember

type Protocol struct {
	AllowMention           []string // discord
	AuthCode               string   // steam
	BindAddress            string   // mattermost, slack // DEPRECATED
	Buffer                 int      // api
	Charset                string   // irc
	ClientID               string   // msteams
	ColorNicks             bool     // only irc for now
	Debug                  bool     // general
	DebugLevel             int      // only for irc now
	DisableWebPagePreview  bool     // telegram
	EditSuffix             string   // mattermost, slack, discord, telegram, gitter
	EditDisable            bool     // mattermost, slack, discord, telegram, gitter
	HTMLDisable            bool     // matrix
	IconURL                string   // mattermost, slack
	IgnoreFailureOnStart   bool     // general
	IgnoreNicks            string   // all protocols
	IgnoreMessages         string   // all protocols
	Jid                    string   // xmpp
	JoinDelay              string   // all protocols
	Label                  string   // all protocols
	Login                  string   // mattermost, matrix
	LogFile                string   // general
	MediaDownloadBlackList []string
	MediaDownloadPath      string // Basically MediaServerUpload, but instead of uploading it, just write it to a file on the same server.
	MediaDownloadSize      int    // all protocols
	MediaServerDownload    string
	MediaServerUpload      string
	MediaConvertTgs        string     // telegram
	MediaConvertWebPToPNG  bool       // telegram
	MessageDelay           int        // IRC, time in millisecond to wait between messages
	MessageFormat          string     // telegram
	MessageLength          int        // IRC, max length of a message allowed
	MessageQueue           int        // IRC, size of message queue for flood control
	MessageSplit           bool       // IRC, split long messages with newlines on MessageLength instead of clipping
	MessageSplitMaxCount   int        // discord, split long messages into at most this many messages instead of clipping (MessageLength=1950 cannot be configured)
	Muc                    string     // xmpp
	MxID                   string     // matrix
	Name                   string     // all protocols
	Nick                   string     // all protocols
	NickFormatter          string     // mattermost, slack
	NickServNick           string     // IRC
	NickServUsername       string     // IRC
	NickServPassword       string     // IRC
	NicksPerRow            int        // mattermost, slack
	NoHomeServerSuffix     bool       // matrix
	NoSendJoinPart         bool       // all protocols
	NoTLS                  bool       // mattermost, xmpp
	Password               string     // IRC,mattermost,XMPP,matrix
	PrefixMessagesWithNick bool       // mattemost, slack
	PreserveThreading      bool       // slack
	Protocol               string     // all protocols
	QuoteDisable           bool       // telegram
	QuoteFormat            string     // telegram
	QuoteLengthLimit       int        // telegram
	RealName               string     // IRC
	RejoinDelay            int        // IRC
	ReplaceMessages        [][]string // all protocols
	ReplaceNicks           [][]string // all protocols
	RemoteNickFormat       string     // all protocols
	RunCommands            []string   // IRC
	Server                 string     // IRC,mattermost,XMPP,discord,matrix
	SessionFile            string     // msteams,whatsapp
	ShowJoinPart           bool       // all protocols
	ShowTopicChange        bool       // slack
	ShowUserTyping         bool       // slack
	ShowEmbeds             bool       // discord
	SkipTLSVerify          bool       // IRC, mattermost
	SkipVersionCheck       bool       // mattermost
	StripNick              bool       // all protocols
	StripMarkdown          bool       // irc
	SyncTopic              bool       // slack
	TengoModifyMessage     string     // general
	Team                   string     // mattermost, keybase
	TeamID                 string     // msteams
	TenantID               string     // msteams
	Token                  string     // gitter, slack, discord, api, matrix
	Topic                  string     // zulip
	URL                    string     // mattermost, slack // DEPRECATED
	UseAPI                 bool       // mattermost, slack
	UseLocalAvatar         []string   // discord
	UseSASL                bool       // IRC
	UseTLS                 bool       // IRC
	UseDiscriminator       bool       // discord
	UseFirstName           bool       // telegram
	UseUserName            bool       // discord, matrix, mattermost
	UseInsecureURL         bool       // telegram
	UserName               string     // IRC
	VerboseJoinPart        bool       // IRC
	WebhookBindAddress     string     // mattermost, slack
	WebhookURL             string     // mattermost, slack
}

type ChannelOptions struct {
	Key        string // irc, xmpp
	WebhookURL string // discord
	Topic      string // zulip
}

type Bridge struct {
	Account     string
	Channel     string
	Options     ChannelOptions
	SameChannel bool
}

type Gateway struct {
	Name   string
	Enable bool
	In     []Bridge
	Out    []Bridge
	InOut  []Bridge
}

type Tengo struct {
	InMessage        string
	Message          string
	RemoteNickFormat string
	OutMessage       string
}

type SameChannelGateway struct {
	Name     string
	Enable   bool
	Channels []string
	Accounts []string
}

type BridgeValues struct {
	API                map[string]Protocol
	IRC                map[string]Protocol
	Mattermost         map[string]Protocol
	Matrix             map[string]Protocol
	Slack              map[string]Protocol
	SlackLegacy        map[string]Protocol
	Steam              map[string]Protocol
	Gitter             map[string]Protocol
	XMPP               map[string]Protocol
	Discord            map[string]Protocol
	Telegram           map[string]Protocol
	Rocketchat         map[string]Protocol
	SSHChat            map[string]Protocol
	WhatsApp           map[string]Protocol // TODO is this struct used? Search for "SlackLegacy" for example didn't return any results
	Zulip              map[string]Protocol
	Keybase            map[string]Protocol
	Mumble             map[string]Protocol
	General            Protocol
	Tengo              Tengo
	Gateway            []Gateway
	SameChannelGateway []SameChannelGateway
}

type Config interface {
	Viper() *viper.Viper
	BridgeValues() *BridgeValues
	IsKeySet(key string) bool
	GetBool(key string) (bool, bool)
	GetInt(key string) (int, bool)
	GetString(key string) (string, bool)
	GetStringSlice(key string) ([]string, bool)
	GetStringSlice2D(key string) ([][]string, bool)
}

type config struct {
	sync.RWMutex

	logger *logrus.Entry
	v      *viper.Viper
	cv     *BridgeValues
}

// NewConfig instantiates a new configuration based on the specified configuration file path.
func NewConfig(rootLogger *logrus.Logger, cfgfile string) Config {
	logger := rootLogger.WithFields(logrus.Fields{"prefix": "config"})

	viper.SetConfigFile(cfgfile)
	input, err := ioutil.ReadFile(cfgfile)
	if err != nil {
		logger.Fatalf("Failed to read configuration file: %#v", err)
	}

	cfgtype := detectConfigType(cfgfile)
	mycfg := newConfigFromString(logger, input, cfgtype)
	if mycfg.cv.General.LogFile != "" {
		logfile, err := os.OpenFile(mycfg.cv.General.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
		if err == nil {
			logger.Info("Opening log file ", mycfg.cv.General.LogFile)
			rootLogger.Out = logfile
		} else {
			logger.Warn("Failed to open ", mycfg.cv.General.LogFile)
		}
	}
	if mycfg.cv.General.MediaDownloadSize == 0 {
		mycfg.cv.General.MediaDownloadSize = 1000000
	}
	viper.WatchConfig()
	viper.OnConfigChange(func(e fsnotify.Event) {
		logger.Println("Config file changed:", e.Name)
	})
	return mycfg
}

// detectConfigType detects JSON and YAML formats, defaults to TOML.
func detectConfigType(cfgfile string) string {
	fileExt := filepath.Ext(cfgfile)
	switch fileExt {
	case ".json":
		return "json"
	case ".yaml", ".yml":
		return "yaml"
	}
	return "toml"
}

// NewConfigFromString instantiates a new configuration based on the specified string.
func NewConfigFromString(rootLogger *logrus.Logger, input []byte) Config {
	logger := rootLogger.WithFields(logrus.Fields{"prefix": "config"})
	return newConfigFromString(logger, input, "toml")
}

func newConfigFromString(logger *logrus.Entry, input []byte, cfgtype string) *config {
	viper.SetConfigType(cfgtype)
	viper.SetEnvPrefix("matterbridge")
	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
	viper.AutomaticEnv()

	if err := viper.ReadConfig(bytes.NewBuffer(input)); err != nil {
		logger.Fatalf("Failed to parse the configuration: %s", err)
	}

	cfg := &BridgeValues{}
	if err := viper.Unmarshal(cfg); err != nil {
		logger.Fatalf("Failed to load the configuration: %s", err)
	}
	return &config{
		logger: logger,
		v:      viper.GetViper(),
		cv:     cfg,
	}
}

func (c *config) BridgeValues() *BridgeValues {
	return c.cv
}

func (c *config) Viper() *viper.Viper {
	return c.v
}

func (c *config) IsKeySet(key string) bool {
	c.RLock()
	defer c.RUnlock()
	return c.v.IsSet(key)
}

func (c *config) GetBool(key string) (bool, bool) {
	c.RLock()
	defer c.RUnlock()
	return c.v.GetBool(key), c.v.IsSet(key)
}

func (c *config) GetInt(key string) (int, bool) {
	c.RLock()
	defer c.RUnlock()
	return c.v.GetInt(key), c.v.IsSet(key)
}

func (c *config) GetString(key string) (string, bool) {
	c.RLock()
	defer c.RUnlock()
	return c.v.GetString(key), c.v.IsSet(key)
}

func (c *config) GetStringSlice(key string) ([]string, bool) {
	c.RLock()
	defer c.RUnlock()
	return c.v.GetStringSlice(key), c.v.IsSet(key)
}

func (c *config) GetStringSlice2D(key string) ([][]string, bool) {
	c.RLock()
	defer c.RUnlock()

	res, ok := c.v.Get(key).([]interface{})
	if !ok {
		return nil, false
	}
	var result [][]string
	for _, entry := range res {
		result2 := []string{}
		for _, entry2 := range entry.([]interface{}) {
			result2 = append(result2, entry2.(string))
		}
		result = append(result, result2)
	}
	return result, true
}

func GetIconURL(msg *Message, iconURL string) string {
	info := strings.Split(msg.Account, ".")
	protocol := info[0]
	name := info[1]
	iconURL = strings.Replace(iconURL, "{NICK}", msg.Username, -1)
	iconURL = strings.Replace(iconURL, "{BRIDGE}", name, -1)
	iconURL = strings.Replace(iconURL, "{PROTOCOL}", protocol, -1)
	return iconURL
}

type TestConfig struct {
	Config

	Overrides map[string]interface{}
}

func (c *TestConfig) IsKeySet(key string) bool {
	_, ok := c.Overrides[key]
	return ok || c.Config.IsKeySet(key)
}

func (c *TestConfig) GetBool(key string) (bool, bool) {
	val, ok := c.Overrides[key]
	if ok {
		return val.(bool), true
	}
	return c.Config.GetBool(key)
}

func (c *TestConfig) GetInt(key string) (int, bool) {
	if val, ok := c.Overrides[key]; ok {
		return val.(int), true
	}
	return c.Config.GetInt(key)
}

func (c *TestConfig) GetString(key string) (string, bool) {
	if val, ok := c.Overrides[key]; ok {
		return val.(string), true
	}
	return c.Config.GetString(key)
}

func (c *TestConfig) GetStringSlice(key string) ([]string, bool) {
	if val, ok := c.Overrides[key]; ok {
		return val.([]string), true
	}
	return c.Config.GetStringSlice(key)
}

func (c *TestConfig) GetStringSlice2D(key string) ([][]string, bool) {
	if val, ok := c.Overrides[key]; ok {
		return val.([][]string), true
	}
	return c.Config.GetStringSlice2D(key)
}
