// Copyright 2017 fatedier, fatedier@gmail.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package server

import (
	"sync"
	"time"

	"github.com/fatedier/frp/g"
	"github.com/fatedier/frp/utils/log"
	"github.com/fatedier/frp/utils/metric"
)

const (
	ReserveDays = 7
)

var globalStats *ServerStatistics

type ServerStatistics struct {
	TotalTrafficIn  metric.DateCounter
	TotalTrafficOut metric.DateCounter
	CurConns        metric.Counter

	// counter for clients
	ClientCounts metric.Counter

	// counter for proxy types
	ProxyTypeCounts map[string]metric.Counter

	// statistics for different proxies
	// key is proxy name
	ProxyStatistics map[string]*ProxyStatistics

	mu sync.Mutex
}

type ProxyStatistics struct {
	Name          string
	ProxyType     string
	TrafficIn     metric.DateCounter
	TrafficOut    metric.DateCounter
	CurConns      metric.Counter
	LastStartTime time.Time
	LastCloseTime time.Time
}

func init() {
	globalStats = &ServerStatistics{
		TotalTrafficIn:  metric.NewDateCounter(ReserveDays),
		TotalTrafficOut: metric.NewDateCounter(ReserveDays),
		CurConns:        metric.NewCounter(),

		ClientCounts:    metric.NewCounter(),
		ProxyTypeCounts: make(map[string]metric.Counter),

		ProxyStatistics: make(map[string]*ProxyStatistics),
	}

	go func() {
		for {
			time.Sleep(12 * time.Hour)
			log.Debug("start to clear useless proxy statistics data...")
			StatsClearUselessInfo()
			log.Debug("finish to clear useless proxy statistics data")
		}
	}()
}

func StatsClearUselessInfo() {
	// To check if there are proxies that closed than 7 days and drop them.
	globalStats.mu.Lock()
	defer globalStats.mu.Unlock()
	for name, data := range globalStats.ProxyStatistics {
		if !data.LastCloseTime.IsZero() && time.Since(data.LastCloseTime) > time.Duration(7*24)*time.Hour {
			delete(globalStats.ProxyStatistics, name)
			log.Trace("clear proxy [%s]'s statistics data, lastCloseTime: [%s]", name, data.LastCloseTime.String())
		}
	}
}

func StatsNewClient() {
	if g.GlbServerCfg.DashboardPort != 0 {
		globalStats.ClientCounts.Inc(1)
	}
}

func StatsCloseClient() {
	if g.GlbServerCfg.DashboardPort != 0 {
		globalStats.ClientCounts.Dec(1)
	}
}

func StatsNewProxy(name string, proxyType string) {
	if g.GlbServerCfg.DashboardPort != 0 {
		globalStats.mu.Lock()
		defer globalStats.mu.Unlock()
		counter, ok := globalStats.ProxyTypeCounts[proxyType]
		if !ok {
			counter = metric.NewCounter()
		}
		counter.Inc(1)
		globalStats.ProxyTypeCounts[proxyType] = counter

		proxyStats, ok := globalStats.ProxyStatistics[name]
		if !(ok && proxyStats.ProxyType == proxyType) {
			proxyStats = &ProxyStatistics{
				Name:       name,
				ProxyType:  proxyType,
				CurConns:   metric.NewCounter(),
				TrafficIn:  metric.NewDateCounter(ReserveDays),
				TrafficOut: metric.NewDateCounter(ReserveDays),
			}
			globalStats.ProxyStatistics[name] = proxyStats
		}
		proxyStats.LastStartTime = time.Now()
	}
}

func StatsCloseProxy(proxyName string, proxyType string) {
	if g.GlbServerCfg.DashboardPort != 0 {
		globalStats.mu.Lock()
		defer globalStats.mu.Unlock()
		if counter, ok := globalStats.ProxyTypeCounts[proxyType]; ok {
			counter.Dec(1)
		}
		if proxyStats, ok := globalStats.ProxyStatistics[proxyName]; ok {
			proxyStats.LastCloseTime = time.Now()
		}
	}
}

func StatsOpenConnection(name string) {
	if g.GlbServerCfg.DashboardPort != 0 {
		globalStats.CurConns.Inc(1)

		globalStats.mu.Lock()
		defer globalStats.mu.Unlock()
		proxyStats, ok := globalStats.ProxyStatistics[name]
		if ok {
			proxyStats.CurConns.Inc(1)
			globalStats.ProxyStatistics[name] = proxyStats
		}
	}
}

func StatsCloseConnection(name string) {
	if g.GlbServerCfg.DashboardPort != 0 {
		globalStats.CurConns.Dec(1)

		globalStats.mu.Lock()
		defer globalStats.mu.Unlock()
		proxyStats, ok := globalStats.ProxyStatistics[name]
		if ok {
			proxyStats.CurConns.Dec(1)
			globalStats.ProxyStatistics[name] = proxyStats
		}
	}
}

func StatsAddTrafficIn(name string, trafficIn int64) {
	if g.GlbServerCfg.DashboardPort != 0 {
		globalStats.TotalTrafficIn.Inc(trafficIn)

		globalStats.mu.Lock()
		defer globalStats.mu.Unlock()

		proxyStats, ok := globalStats.ProxyStatistics[name]
		if ok {
			proxyStats.TrafficIn.Inc(trafficIn)
			globalStats.ProxyStatistics[name] = proxyStats
		}
	}
}

func StatsAddTrafficOut(name string, trafficOut int64) {
	if g.GlbServerCfg.DashboardPort != 0 {
		globalStats.TotalTrafficOut.Inc(trafficOut)

		globalStats.mu.Lock()
		defer globalStats.mu.Unlock()

		proxyStats, ok := globalStats.ProxyStatistics[name]
		if ok {
			proxyStats.TrafficOut.Inc(trafficOut)
			globalStats.ProxyStatistics[name] = proxyStats
		}
	}
}

// Functions for getting server stats.
type ServerStats struct {
	TotalTrafficIn  int64
	TotalTrafficOut int64
	CurConns        int64
	ClientCounts    int64
	ProxyTypeCounts map[string]int64
}

func StatsGetServer() *ServerStats {
	globalStats.mu.Lock()
	defer globalStats.mu.Unlock()
	s := &ServerStats{
		TotalTrafficIn:  globalStats.TotalTrafficIn.TodayCount(),
		TotalTrafficOut: globalStats.TotalTrafficOut.TodayCount(),
		CurConns:        globalStats.CurConns.Count(),
		ClientCounts:    globalStats.ClientCounts.Count(),
		ProxyTypeCounts: make(map[string]int64),
	}
	for k, v := range globalStats.ProxyTypeCounts {
		s.ProxyTypeCounts[k] = v.Count()
	}
	return s
}

type ProxyStats struct {
	Name            string
	Type            string
	TodayTrafficIn  int64
	TodayTrafficOut int64
	LastStartTime   string
	LastCloseTime   string
	CurConns        int64
}

func StatsGetProxiesByType(proxyType string) []*ProxyStats {
	res := make([]*ProxyStats, 0)
	globalStats.mu.Lock()
	defer globalStats.mu.Unlock()

	for name, proxyStats := range globalStats.ProxyStatistics {
		if proxyStats.ProxyType != proxyType {
			continue
		}

		ps := &ProxyStats{
			Name:            name,
			Type:            proxyStats.ProxyType,
			TodayTrafficIn:  proxyStats.TrafficIn.TodayCount(),
			TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
			CurConns:        proxyStats.CurConns.Count(),
		}
		if !proxyStats.LastStartTime.IsZero() {
			ps.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05")
		}
		if !proxyStats.LastCloseTime.IsZero() {
			ps.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05")
		}
		res = append(res, ps)
	}
	return res
}

func StatsGetProxiesByTypeAndName(proxyType string, proxyName string) (res *ProxyStats) {
	globalStats.mu.Lock()
	defer globalStats.mu.Unlock()

	for name, proxyStats := range globalStats.ProxyStatistics {
		if proxyStats.ProxyType != proxyType {
			continue
		}

		if name != proxyName {
			continue
		}

		res = &ProxyStats{
			Name:            name,
			Type:            proxyStats.ProxyType,
			TodayTrafficIn:  proxyStats.TrafficIn.TodayCount(),
			TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
			CurConns:        proxyStats.CurConns.Count(),
		}
		if !proxyStats.LastStartTime.IsZero() {
			res.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05")
		}
		if !proxyStats.LastCloseTime.IsZero() {
			res.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05")
		}
		break
	}
	return
}

type ProxyTrafficInfo struct {
	Name       string
	TrafficIn  []int64
	TrafficOut []int64
}

func StatsGetProxyTraffic(name string) (res *ProxyTrafficInfo) {
	globalStats.mu.Lock()
	defer globalStats.mu.Unlock()

	proxyStats, ok := globalStats.ProxyStatistics[name]
	if ok {
		res = &ProxyTrafficInfo{
			Name: name,
		}
		res.TrafficIn = proxyStats.TrafficIn.GetLastDaysCount(ReserveDays)
		res.TrafficOut = proxyStats.TrafficOut.GetLastDaysCount(ReserveDays)
	}
	return
}
