package stat

/*
   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.
*/

import (
	"net/http"
	"strings"
	"sync/atomic"
	"time"

	"github.com/apache/trafficcontrol/v8/grove/cacheobj"
	"github.com/apache/trafficcontrol/v8/grove/icache"
	"github.com/apache/trafficcontrol/v8/grove/remapdata"
	"github.com/apache/trafficcontrol/v8/grove/web"

	"github.com/apache/trafficcontrol/v8/lib/go-log"
)

type StatsSystem interface {
	AddConfigReloadRequests()
	SetLastReloadRequest(time.Time)
	AddConfigReload()
	SetLastReload(time.Time)
	SetAstatsLoad(time.Time)

	ConfigReloadRequests() uint64
	LastReloadRequest() time.Time
	ConfigReloads() uint64
	LastReload() time.Time
	AstatsLoad() time.Time
	Version() string
}

type Stats interface {
	System() StatsSystem
	Remap() StatsRemaps

	Connections() uint64

	CacheHits() uint64
	AddCacheHit()
	CacheMisses() uint64
	AddCacheMiss()

	CacheSize() uint64
	CacheCapacity() uint64

	// Write writes to the remapRuleStats of s, and returns the bytes written to the connection
	Write(w http.ResponseWriter, conn *web.InterceptConn, reqFQDN string, remoteAddr string, code int, bytesWritten uint64, cacheHit bool) uint64

	CacheKeys(string) []string
	CacheSizeByName(string) (uint64, bool)
	CacheCapacityByName(string) (uint64, bool)
	CacheNames() []string
	CachePeek(string, string) (*cacheobj.CacheObj, bool)
}

func New(remapRules []remapdata.RemapRule, caches map[string]icache.Cache, cacheCapacityBytes uint64, httpConns *web.ConnMap, httpsConns *web.ConnMap, version string) Stats {
	cacheHits := uint64(0)
	cacheMisses := uint64(0)
	return &stats{
		system:             NewStatsSystem(version),
		remap:              NewStatsRemaps(remapRules),
		cacheHits:          &cacheHits,
		cacheMisses:        &cacheMisses,
		caches:             caches,
		cacheCapacityBytes: cacheCapacityBytes,
		httpConns:          httpConns,
		httpsConns:         httpsConns,
	}
}

// Write writes to the remapRuleStats of s, and returns the bytes written to the connection
func (stats *stats) Write(w http.ResponseWriter, conn *web.InterceptConn, reqFQDN string, remoteAddr string, code int, bytesWritten uint64, cacheHit bool) uint64 {
	remapRuleStats, ok := stats.Remap().Stats(reqFQDN)
	if !ok {
		log.Errorf("Remap rule %v not in Stats\n", reqFQDN)
		return bytesWritten
	}

	if wFlusher, ok := w.(http.Flusher); !ok {
		log.Errorf("ResponseWriter is not a Flusher, could not flush written bytes, stat out_bytes will be inaccurate!\n")
	} else {
		wFlusher.Flush()
	}

	bytesRead := 0 // TODO get somehow? Count body? Sum header?
	if conn != nil {
		bytesRead = conn.BytesRead()
		bytesWritten = uint64(conn.BytesWritten()) // get the more accurate interceptConn bytes written, if we can
		// Don't log - the Handler has already logged the failure to get the conn
	}

	// bytesRead, bytesWritten := getConnInfoAndDestroyWriter(w, stats, remapRuleName)
	remapRuleStats.AddInBytes(uint64(bytesRead))
	remapRuleStats.AddOutBytes(uint64(bytesWritten))

	if cacheHit {
		stats.AddCacheHit()
		remapRuleStats.AddCacheHit()
	} else {
		stats.AddCacheMiss()
		remapRuleStats.AddCacheMiss()
	}

	switch {
	case code < 200:
		log.Errorf("responded with invalid code %v\n", code)
	case code < 300:
		remapRuleStats.AddStatus2xx(1)
	case code < 400:
		remapRuleStats.AddStatus3xx(1)
	case code < 500:
		remapRuleStats.AddStatus4xx(1)
	case code < 600:
		remapRuleStats.AddStatus5xx(1)
	default:
		log.Errorf("responded with invalid code %v\n", code)
	}
	return bytesWritten
}

// stats fulfills the Stats interface
type stats struct {
	system             StatsSystem
	remap              StatsRemaps
	cacheHits          *uint64
	cacheMisses        *uint64
	caches             map[string]icache.Cache
	cacheCapacityBytes uint64
	httpConns          *web.ConnMap
	httpsConns         *web.ConnMap
}

func (s stats) Connections() uint64 {
	l := uint64(0)
	if s.httpConns != nil {
		l += uint64(s.httpConns.Len())
	}
	if s.httpsConns != nil {
		l += uint64(s.httpsConns.Len())
	}
	return l
}
func (s stats) CacheHits() uint64    { return atomic.LoadUint64(s.cacheHits) }
func (s stats) AddCacheHit()         { atomic.AddUint64(s.cacheHits, 1) }
func (s stats) CacheMisses() uint64  { return atomic.LoadUint64(s.cacheMisses) }
func (s stats) AddCacheMiss()        { atomic.AddUint64(s.cacheMisses, 1) }
func (s *stats) System() StatsSystem { return StatsSystem(s.system) }
func (s *stats) Remap() StatsRemaps  { return s.remap }

// CacheSizeByName returns the size of tha cache for a particular cache
func (s stats) CacheSizeByName(cName string) (uint64, bool) {
	if cache, ok := s.caches[cName]; ok {
		return cache.Size(), true
	}
	return 0, false
}

// CacheSize() returns the combined size of all caches.
func (s stats) CacheSize() uint64 {
	sum := uint64(0)
	for _, c := range s.caches {
		sum += c.Size()
	}
	return sum
}

// CacheNames returns an array of all the cache names
func (s stats) CacheNames() []string {
	cNames := make([]string, 0)
	for cacheName, _ := range s.caches {
		cNames = append(cNames, cacheName)
	}
	return cNames
}

// CacheKeys returns an array of all the cache keys for the cache cacheName
func (s stats) CacheKeys(cacheName string) []string {
	return s.caches[cacheName].Keys()
}

// CachePeek returns the cached object *without* changing the recent-used-ness.
func (s stats) CachePeek(key, cacheName string) (*cacheobj.CacheObj, bool) {
	return s.caches[cacheName].Peek(key)
}

func (s stats) CacheCapacityByName(cName string) (uint64, bool) {
	if cache, ok := s.caches[cName]; ok {
		return cache.Capacity(), true
	}
	return 0, false
}

func (s stats) CacheCapacity() uint64 { return s.cacheCapacityBytes }

type StatsRemaps interface {
	Stats(fqdn string) (StatsRemap, bool)
	Rules() []string
}

type StatsRemap interface {
	InBytes() uint64
	AddInBytes(uint64)
	OutBytes() uint64
	AddOutBytes(uint64)
	Status2xx() uint64
	AddStatus2xx(uint64)
	Status3xx() uint64
	AddStatus3xx(uint64)
	Status4xx() uint64
	AddStatus4xx(uint64)
	Status5xx() uint64
	AddStatus5xx(uint64)

	CacheHits() uint64
	AddCacheHit()
	CacheMisses() uint64
	AddCacheMiss()
}

func getFromFQDN(r remapdata.RemapRule) string {
	path := r.From
	schemeEnd := `://`
	if i := strings.Index(path, schemeEnd); i != -1 {
		path = path[i+len(schemeEnd):]
	}
	pathStart := `/`
	if i := strings.Index(path, pathStart); i != -1 {
		path = path[:i]
	}
	return path
}

func NewStatsRemaps(remapRules []remapdata.RemapRule) StatsRemaps {
	m := make(map[string]StatsRemap, len(remapRules))
	for _, rule := range remapRules {
		m[getFromFQDN(rule)] = NewStatsRemap() // must pre-allocate, for threadsafety, so users are never changing the map itself, only the value pointed to.
	}
	return statsRemaps(m)
}

// statsRemaps fulfills the StatsRemaps interface
type statsRemaps map[string]StatsRemap

func (s statsRemaps) Stats(rule string) (StatsRemap, bool) {
	r, ok := s[rule]
	return r, ok
}

func (s statsRemaps) Rules() []string {
	rules := make([]string, 0, len(s))
	for rule := range s {
		rules = append(rules, rule)
	}
	return rules
}

func NewStatsRemap() StatsRemap {
	return &statsRemap{}
}

type statsRemap struct {
	inBytes     uint64
	outBytes    uint64
	status2xx   uint64
	status3xx   uint64
	status4xx   uint64
	status5xx   uint64
	cacheHits   uint64
	cacheMisses uint64
}

func (r *statsRemap) InBytes() uint64       { return atomic.LoadUint64(&r.inBytes) }
func (r *statsRemap) AddInBytes(v uint64)   { atomic.AddUint64(&r.inBytes, v) }
func (r *statsRemap) OutBytes() uint64      { return atomic.LoadUint64(&r.outBytes) }
func (r *statsRemap) AddOutBytes(v uint64)  { atomic.AddUint64(&r.outBytes, v) }
func (r *statsRemap) Status2xx() uint64     { return atomic.LoadUint64(&r.status2xx) }
func (r *statsRemap) AddStatus2xx(v uint64) { atomic.AddUint64(&r.status2xx, v) }
func (r *statsRemap) Status3xx() uint64     { return atomic.LoadUint64(&r.status3xx) }
func (r *statsRemap) AddStatus3xx(v uint64) { atomic.AddUint64(&r.status3xx, v) }
func (r *statsRemap) Status4xx() uint64     { return atomic.LoadUint64(&r.status4xx) }
func (r *statsRemap) AddStatus4xx(v uint64) { atomic.AddUint64(&r.status4xx, v) }
func (r *statsRemap) Status5xx() uint64     { return atomic.LoadUint64(&r.status5xx) }
func (r *statsRemap) AddStatus5xx(v uint64) { atomic.AddUint64(&r.status5xx, v) }

func (r *statsRemap) CacheHits() uint64 { return atomic.LoadUint64(&r.cacheHits) }
func (r *statsRemap) AddCacheHit()      { atomic.AddUint64(&r.cacheHits, 1) }

func (r *statsRemap) CacheMisses() uint64 { return atomic.LoadUint64(&r.cacheMisses) }
func (r *statsRemap) AddCacheMiss()       { atomic.AddUint64(&r.cacheMisses, 1) }

func NewStatsSystem(version string) StatsSystem {
	return &statsSystem{version: version}
}

type statsSystem struct {
	configReloadRequests      uint64
	lastReloadRequestUnixNano int64
	configReloads             uint64
	lastReloadUnixNano        int64
	astatsLoadUnixNano        int64
	version                   string
}

func (s *statsSystem) ConfigReloadRequests() uint64 {
	return atomic.LoadUint64(&s.configReloadRequests)
}
func (s *statsSystem) AddConfigReloadRequests() {
	atomic.AddUint64(&s.configReloadRequests, 1)
}
func (s *statsSystem) LastReloadRequest() time.Time {
	return time.Unix(0, atomic.LoadInt64(&s.lastReloadRequestUnixNano))
}
func (s *statsSystem) SetLastReloadRequest(t time.Time) {
	atomic.StoreInt64(&s.lastReloadRequestUnixNano, t.UnixNano())
}
func (s *statsSystem) ConfigReloads() uint64 {
	return atomic.LoadUint64(&s.configReloads)
}
func (s *statsSystem) AddConfigReload() {
	atomic.AddUint64(&s.configReloads, 1)
}
func (s *statsSystem) LastReload() time.Time {
	return time.Unix(0, atomic.LoadInt64(&s.lastReloadUnixNano))
}
func (s *statsSystem) SetLastReload(t time.Time) {
	atomic.StoreInt64(&s.lastReloadUnixNano, t.UnixNano())
}
func (s *statsSystem) AstatsLoad() time.Time {
	return time.Unix(0, atomic.LoadInt64(&s.astatsLoadUnixNano))
}
func (s *statsSystem) SetAstatsLoad(t time.Time) {
	atomic.StoreInt64(&s.astatsLoadUnixNano, t.UnixNano())
}
func (s *statsSystem) Version() string {
	return s.version
}

const ATSVersion = "5.3.2" // of course, we're not really ATS. We're terrible liars.

// type StatsATSJSON struct {
// 	Server string            `json:"server"`
// 	Remap  map[string]uint64 `json:"remap"`
// }

type StatsSystemJSON struct {
	InterfaceName        string `json:"inf.name"`
	InterfaceSpeed       int64  `json:"inf.speed"`
	ProcNetDev           string `json:"proc.net.dev"`
	ProcLoadAvg          string `json:"proc.loadavg"`
	ConfigReloadRequests uint64 `json:"configReloadRequests"`
	LastReloadRequest    int64  `json:"lastReloadRequest"`
	ConfigReloads        uint64 `json:"configReloads"`
	LastReload           int64  `json:"lastReload"`
	AstatsLoad           int64  `json:"astatsLoad"`
	Something            string `json:"something"`
	Version              string `json:"application_version"`
}

type StatsJSON struct {
	ATS    map[string]interface{} `json:"ats"`
	System StatsSystemJSON        `json:"system"`
}
