package server_hosting

import (
	"cwtch.im/cwtch/app"
	"cwtch.im/cwtch/event"
	"git.openprivacy.ca/cwtch.im/autobindings/constants"
	"git.openprivacy.ca/cwtch.im/server"
	"git.openprivacy.ca/openprivacy/connectivity"
	"git.openprivacy.ca/openprivacy/log"
	"os"
	"path/filepath"
	"strconv"
	"sync"
	"time"
)

const serversExperiment = "servers-experiment"

const (
	ZeroServersLoaded  = event.Type("ZeroServersLoaded")
	NewServer          = event.Type("NewServer")
	ServerIntentUpdate = event.Type("ServerIntentUpdate")
	ServerDeleted      = event.Type("ServerDeleted")
	ServerStatsUpdate  = event.Type("ServerStatsUpdate")
)

const (
	Intent        = event.Field("Intent")
	TotalMessages = event.Field("TotalMessages")
	Connections   = event.Field("Connections")
)

const (
	IntentRunning = "running"
	IntentStopped = "stopped"
)

type ServerInfo struct {
	Onion        string
	ServerBundle string
	Autostart    bool
	Running      bool
	Description  string
	StorageType  string
}

var lock sync.Mutex
var appServers server.Servers
var killStatsUpdate chan bool = make(chan bool, 1)
var enabled bool = false

func Init(acn connectivity.ACN, appdir string) *ServersFunctionality {
	lock.Lock()
	defer lock.Unlock()
	if appServers == nil {
		serversDir := filepath.Join(appdir, "servers")
		err := os.MkdirAll(serversDir, 0700)
		if err != nil {
			log.Errorf("Could not init servers directory: %s", err)
		}
		appServers = server.NewServers(acn, serversDir)
	}

	serversFunctionality := new(ServersFunctionality)

	return serversFunctionality
}

// track acnStatus across events
var acnStatus = -1

func (sf *ServersFunctionality) OnACNStatusEvent(appl app.Application, e *event.Event) {
	newAcnStatus, err := strconv.Atoi(e.Data[event.Progress])
	if !appl.IsFeatureEnabled(serversExperiment) || err != nil {
		return
	}
	if newAcnStatus == 100 {
		if acnStatus != 100 {
			// just came online
			for _, onion := range sf.ListServers() {
				autostart := false
				if s := sf.GetServer(onion); s != nil {
					autostart = s.GetAttribute(server.AttrAutostart) == "true"
				}
				if autostart {
					sf.LaunchServer(appl, onion)
				}
			}
		}
	} else {
		if acnStatus == 100 {
			for _, onion := range sf.ListServers() {
				sf.StopServer(appl, onion)
			}
		}
	}
	acnStatus = newAcnStatus
}

func (sh *ServersFunctionality) Disable() {
	lock.Lock()
	defer lock.Unlock()
	if appServers != nil {
		appServers.Stop()
	}
	if enabled {
		enabled = false
		killStatsUpdate <- true
	}
}

func (sh *ServersFunctionality) Enabled() bool {
	lock.Lock()
	defer lock.Unlock()
	return enabled
}

// ServersFunctionality provides experiment gated server functionality
type ServersFunctionality struct {
}

func (sh *ServersFunctionality) UpdateSettings(appl app.Application, acn connectivity.ACN) {
	if appl.IsFeatureEnabled(serversExperiment) {
		sh.LoadServers(appl, acn, app.DefactoPasswordForUnencryptedProfiles)
		if !sh.Enabled() {
			sh.Enable(appl, acn)

			// republish servers
			serversList := sh.ListServers()
			for _, server := range serversList {
				serverInfo := sh.GetServerInfo(server)
				ev := event.NewEvent(NewServer, make(map[event.Field]string))
				serverInfo.EnrichEvent(&ev)
				appl.GetPrimaryBus().Publish(ev)
			}
			sh.LaunchServers(appl, acn)
		}
	} else {
		sh.Disable()
	}
}

func (sh *ServersFunctionality) SetServerAttribute(appl app.Application, handle string, key string, val string) {
	if appl.IsFeatureEnabled(serversExperiment) {
		server := sh.GetServer(handle)
		if server != nil {
			server.SetAttribute(key, val)
		}
	}
}

func (sh *ServersFunctionality) StopServers(appl app.Application) {
	// we are always permitted to stop servers
	for _, onion := range sh.ListServers() {
		sh.StopServer(appl, onion)
	}
}

func (sh *ServersFunctionality) LaunchServers(appl app.Application, acn connectivity.ACN) {
	if appl.IsFeatureEnabled(serversExperiment) {
		acnStatus, _ := acn.GetBootstrapStatus()
		if acnStatus == 100 {
			for _, onion := range sh.ListServers() {
				autostart := false
				if s := sh.GetServer(onion); s != nil {
					autostart = s.GetAttribute(server.AttrAutostart) == "true"
				}
				if autostart {
					sh.LaunchServer(appl, onion)
				}
			}
		}
	}
}

func (sf *ServersFunctionality) Enable(application app.Application, acn connectivity.ACN) {
	lock.Lock()
	defer lock.Unlock()
	if appServers != nil && !enabled {
		enabled = true

		// load unencrypted profiles
		sf.LoadServers(application, acn, app.DefactoPasswordForUnencryptedProfiles)

		go cacheForwardServerMetricUpdates(application)
	}
}

func (sf *ServersFunctionality) LoadServers(appl app.Application, acn connectivity.ACN, password string) {
	serversList, err := appServers.LoadServers(password)
	if err != nil {
		log.Errorf("Error attempting to load servers :%s\n", err)
		appl.GetPrimaryBus().Publish(event.NewEventList(ZeroServersLoaded))
	} else if len(serversList) == 0 {
		log.Debugln("Loaded 0 servers")
		appl.GetPrimaryBus().Publish(event.NewEventList(ZeroServersLoaded))
	} else {
		acnStatus, _ := acn.GetBootstrapStatus()
		for _, serverOnion := range serversList {
			serverInfo := sf.GetServerInfo(serverOnion)
			log.Debugf("Load Server NewServer event: %s", serverInfo)
			ev := event.NewEvent(NewServer, make(map[event.Field]string))
			serverInfo.EnrichEvent(&ev)
			appl.GetPrimaryBus().Publish(ev)
			if serverInfo.Autostart && acnStatus == 100 {
				sf.LaunchServer(appl, serverOnion)
			}
		}
	}

	// server:1.3/libcwtch-go:1.4 accidentally enabled monitor logging by default. make sure it's turned off
	for _, onion := range serversList {
		server := appServers.GetServer(onion)
		server.SetMonitorLogging(false)
	}
}

func (sf *ServersFunctionality) CreateServer(appl app.Application, password string, description string, autostart bool) {
	s, err := appServers.CreateServer(password)
	if err != nil {
		log.Errorf("Could not create new server: %s\n", err)
	} else {
		s.SetAttribute(server.AttrDescription, description)
		if autostart {
			s.SetAttribute(server.AttrAutostart, "true")
		} else {
			s.SetAttribute(server.AttrAutostart, "false")
		}
		if password == app.DefactoPasswordForUnencryptedProfiles {
			s.SetAttribute(server.AttrStorageType, server.StorageTypeDefaultPassword)
		} else {
			s.SetAttribute(server.AttrStorageType, server.StorageTypePassword)
		}
		serverInfo := sf.GetServerInfo(s.Onion())
		log.Debugf("Creating Server NewServer event: %s", serverInfo)
		ev := event.NewEvent(NewServer, make(map[event.Field]string))
		serverInfo.EnrichEvent(&ev)
		appl.GetPrimaryBus().Publish(ev)
		if autostart {
			sf.LaunchServer(appl, s.Onion())
		}
	}
}

func (sf *ServersFunctionality) GetServer(onion string) server.Server {
	return appServers.GetServer(onion)
}

func (sf *ServersFunctionality) GetServerStatistics(onion string) server.Statistics {
	s := appServers.GetServer(onion)
	if s != nil {
		return s.GetStatistics()
	}
	return server.Statistics{}
}

func (sf *ServersFunctionality) ListServers() []string {
	return appServers.ListServers()
}

func (sf *ServersFunctionality) DeleteServer(appl app.Application, onion string, currentPassword string) error {
	sf.StopServer(appl, onion)
	appl.GetPrimaryBus().Publish(event.NewEventList(ServerIntentUpdate, event.Identity, onion, Intent, IntentStopped))
	err := appServers.DeleteServer(onion, currentPassword)
	if err == nil {
		appl.GetPrimaryBus().Publish(event.NewEventList(ServerDeleted, event.Status, constants.StatusSuccess, event.Identity, onion))
	} else {
		appl.GetPrimaryBus().Publish(event.NewEventList(ServerDeleted, event.Status, constants.StatusError, event.Error, err.Error(), event.Identity, onion))
	}
	return err
}

func (sf *ServersFunctionality) LaunchServer(appl app.Application, onion string) {
	if appl.IsFeatureEnabled(serversExperiment) {
		appServers.LaunchServer(onion)
		server := appServers.GetServer(onion)
		if server != nil {
			newStats := server.GetStatistics()
			appl.GetPrimaryBus().Publish(event.NewEventList(ServerStatsUpdate, event.Identity, onion, TotalMessages, strconv.Itoa(newStats.TotalMessages), Connections, strconv.Itoa(newStats.TotalConnections)))
			appl.GetPrimaryBus().Publish(event.NewEventList(ServerIntentUpdate, event.Identity, onion, Intent, IntentRunning))
		}
	}
}

func (sf *ServersFunctionality) StopServer(appl app.Application, onion string) {
	// we are always permitted to stop servers
	appServers.StopServer(onion)
	appl.GetPrimaryBus().Publish(event.NewEventList(ServerIntentUpdate, event.Identity, onion, Intent, IntentStopped))
}

func (sf *ServersFunctionality) DestroyServers() {
	// we are always permitted to destroy servers
	appServers.Destroy()
}

func (sf *ServersFunctionality) GetServerInfo(onion string) *ServerInfo {
	s := sf.GetServer(onion)
	var serverInfo ServerInfo
	serverInfo.Onion = s.Onion()
	serverInfo.ServerBundle = s.ServerBundle()
	serverInfo.Autostart = s.GetAttribute(server.AttrAutostart) == "true"
	running, _ := s.CheckStatus()
	serverInfo.Running = running
	serverInfo.Description = s.GetAttribute(server.AttrDescription)
	serverInfo.StorageType = s.GetAttribute(server.AttrStorageType)
	return &serverInfo
}

func (si *ServerInfo) EnrichEvent(e *event.Event) {
	e.Data["Onion"] = si.Onion
	e.Data["ServerBundle"] = si.ServerBundle
	e.Data["Description"] = si.Description
	e.Data["StorageType"] = si.StorageType
	if si.Autostart {
		e.Data["Autostart"] = "true"
	} else {
		e.Data["Autostart"] = "false"
	}
	if si.Running {
		e.Data["Running"] = "true"
	} else {
		e.Data["Running"] = "false"
	}
}

// cacheForwardServerMetricUpdates every minute gets metrics for all servers, and if they have changed, sends events to the UI
func cacheForwardServerMetricUpdates(appl app.Application) {
	var cache map[string]server.Statistics = make(map[string]server.Statistics)
	duration := time.Second // allow first load
	for {
		select {
		case <-time.After(duration):
			duration = time.Minute
			serverList := appServers.ListServers()
			for _, serverOnion := range serverList {
				server := appServers.GetServer(serverOnion)
				if running, err := server.CheckStatus(); running && err == nil {
					newStats := server.GetStatistics()
					if stats, ok := cache[serverOnion]; !ok || stats.TotalConnections != newStats.TotalConnections || stats.TotalMessages != newStats.TotalMessages {
						cache[serverOnion] = newStats
						appl.GetPrimaryBus().Publish(event.NewEventList(ServerStatsUpdate, event.Identity, serverOnion, TotalMessages, strconv.Itoa(newStats.TotalMessages), Connections, strconv.Itoa(newStats.TotalConnections)))
					}
				}
			}
		case <-killStatsUpdate:
			return
		}
	}
}

func (sh *ServersFunctionality) Shutdown() {
	sh.Disable()
	appServers.Destroy()
}
