package cachesstats

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 (
	"database/sql"
	"errors"
	"net/http"
	"strconv"

	"github.com/apache/trafficcontrol/v8/lib/go-log"
	"github.com/apache/trafficcontrol/v8/lib/go-tc"
	"github.com/apache/trafficcontrol/v8/lib/go-util"
	"github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/api"
	"github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/util/monitorhlp"
)

const ATSCurrentConnectionsStat = "ats.proxy.process.http.current_client_connections"

func Get(w http.ResponseWriter, r *http.Request) {
	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
	if userErr != nil || sysErr != nil {
		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
		return
	}
	defer inf.Close()

	api.RespWriter(w, r, inf.Tx.Tx)(getCachesStats(inf.Tx.Tx))
}

func getCachesStats(tx *sql.Tx) ([]CacheData, error) {
	monitors, err := monitorhlp.GetURLs(tx)
	if err != nil {
		return nil, errors.New("getting monitors: " + err.Error())
	}

	client, err := monitorhlp.GetClient(tx)
	if err != nil {
		return nil, errors.New("getting monitor client: " + err.Error())
	}

	cacheData, err := getCacheData(tx)
	if err != nil {
		return nil, errors.New("getting cache data: " + err.Error())
	}

	for cdn, monitorFQDNs := range monitors {
		if len(monitorFQDNs) == 0 {
			log.Warnln("getCachesStats: cdn '" + string(cdn) + "' has no online monitors, skipping!")
			continue
		}

		success := false
		errs := []error{}
		for _, monitorFQDN := range monitorFQDNs {
			crStates, err := monitorhlp.GetCRStates(monitorFQDN, client)
			if err != nil {
				errs = append(errs, errors.New("getting CRStates for CDN '"+string(cdn)+"' monitor '"+monitorFQDN+"': "+err.Error()))
				continue
			}

			var cacheStats tc.Stats
			var url string
			stats := []string{ATSCurrentConnectionsStat, tc.StatNameBandwidth}
			cacheStats, url, err = monitorhlp.GetCacheStats(monitorFQDN, client, stats)
			if err != nil {
				legacyCacheStats, legacyUrl, err := monitorhlp.GetLegacyCacheStats(monitorFQDN, client, stats)
				if err != nil {
					errs = append(errs, errors.New("getting CacheStats for CDN '"+string(cdn)+"' monitor '"+monitorFQDN+"': "+err.Error()))
					continue
				}
				url = legacyUrl
				cacheStats = monitorhlp.UpgradeLegacyStats(legacyCacheStats)
			}

			cacheData = addHealth(cacheData, crStates)
			cacheData = addStats(cacheData, cacheStats, url)
			success = true
			break
		}

		if !success {
			return nil, errors.New("getting cache stats from all monitors failed for cdn '" + string(cdn) + "': " + util.JoinErrs(errs).Error())
		}

		// if we succeeded, log the monitor failures but don't return them
		for _, err := range errs {
			log.Errorln(err.Error())
		}
	}
	cacheData = addTotals(cacheData)
	return cacheData, nil
}

// addTotals sums each cachegroup, and adds the sum to an object with the cache-specific fields set to "ALL", and the cachegroup. It then sums all cachegroups, and adds the total to an object with all fields set to "ALL".
// TODO in the next API version, add totals in their own JSON objects, not amidst the cachegroup keys.
func addTotals(data []CacheData) []CacheData {
	all := "ALL"
	cachegroups := map[tc.CacheGroupName]CacheData{}
	total := CacheData{Profile: all, Status: all, Healthy: true, HostName: tc.CacheName(all), CacheGroup: tc.CacheGroupName(all)}
	for _, d := range data {
		cg := cachegroups[tc.CacheGroupName(d.CacheGroup)]
		cg.CacheGroup = d.CacheGroup
		cg.Connections += d.Connections
		cg.KBPS += d.KBPS
		cachegroups[tc.CacheGroupName(d.CacheGroup)] = cg
		total.Connections += d.Connections
		total.KBPS += d.KBPS
	}
	for _, cg := range cachegroups {
		cg.Profile = all
		cg.Status = all
		cg.Healthy = true
		cg.HostName = tc.CacheName(all)
		data = append(data, cg)
	}
	data = append(data, total)
	return data
}

func addStats(cacheData []CacheData, stats tc.Stats, url string) []CacheData {
	var err error
	if stats.Caches == nil {
		return cacheData // TODO warn?
	}
	for i, cache := range cacheData {
		stat, ok := stats.Caches[string(cache.HostName)]
		if !ok {
			continue
		}
		bandwidth, ok := stat.Stats[tc.StatNameBandwidth]
		if ok && len(bandwidth) > 0 {
			if kbps, ok := bandwidth[0].Val.(string); !ok {
				log.Warnf("bandwidth %v of cache %s from url %s couldn't be converted into string", bandwidth[0].Val, string(cache.HostName), url)
			} else {
				cache.KBPS, err = strconv.ParseUint(kbps, 10, 64)
				if err != nil {
					log.Warnf("'bandwidth' stat %v of cache %s from url %s couldn't be converted into uint64", kbps, string(cache.HostName), url)
				}
			}
		}
		connections, ok := stat.Stats[ATSCurrentConnectionsStat]
		if ok && len(connections) > 0 {
			if conn, ok := connections[0].Val.(string); !ok {
				log.Warnf("'connections' stat %v of cache %s from url %s couldn't be converted into string", connections[0].Val, string(cache.HostName), url)
			} else {
				cache.Connections, err = strconv.ParseUint(conn, 10, 64)
				if err != nil {
					log.Warnf("'connections' stat %v of cache %s from url %s couldn't be converted into uint64", conn, string(cache.HostName), url)
				}
			}
		}
		cacheData[i] = cache
	}
	return cacheData
}

func addHealth(cacheData []CacheData, crStates tc.CRStates) []CacheData {
	if crStates.Caches == nil {
		return cacheData // TODO warn?
	}
	for i, cache := range cacheData {
		crsCache, ok := crStates.Caches[cache.HostName]
		if !ok {
			continue
		}
		cache.Healthy = crsCache.IsAvailable
		cacheData[i] = cache
	}
	return cacheData
}

type CacheData struct {
	HostName    tc.CacheName      `json:"hostname"`
	CacheGroup  tc.CacheGroupName `json:"cachegroup"`
	Status      string            `json:"status"`
	Profile     string            `json:"profile"`
	IP          *string           `json:"ip"`
	Healthy     bool              `json:"healthy"`
	KBPS        uint64            `json:"kbps"`
	Connections uint64            `json:"connections"`
}

// getCacheData gets the cache data from the servers table. Note this only gets from the database, and thus does not set the Healthy member.
func getCacheData(tx *sql.Tx) ([]CacheData, error) {
	qry := `
SELECT
  s.host_name,
  cg.name as cachegroup,
  st.name as status,
  p.name as profile,
  (select address from ip_address where s.id = ip_address.server and service_address = true AND family(address) = 4) as ip
FROM
  server s
  JOIN cachegroup cg ON s.cachegroup = cg.id
  JOIN status st ON s.status = st.id
  JOIN profile p ON s.profile = p.id
WHERE
  p.name LIKE '` + tc.CacheTypeEdge.String() + `%' OR p.name LIKE '` + tc.CacheTypeMid.String() + `%'
`
	rows, err := tx.Query(qry)
	if err != nil {
		return nil, errors.New("querying cache data: " + err.Error())
	}
	defer rows.Close()
	data := []CacheData{}
	for rows.Next() {
		d := CacheData{}
		if err := rows.Scan(&d.HostName, &d.CacheGroup, &d.Status, &d.Profile, &d.IP); err != nil {
			return nil, errors.New("scanning cache data: " + err.Error())
		}
		data = append(data, d)
	}
	return data, nil
}
