package bird

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"reflect"
	"strconv"
	"strings"
	"sync"
	"time"

	"os/exec"
)

type Cache interface {
	Set(key string, val Parsed, ttl int) error
	Get(key string) (Parsed, error)
	Expire() int
}

var ClientConf BirdConfig
var StatusConf StatusConfig
var IPVersion = "4"
var BirdVersion = 0
var cache Cache // stores parsed birdc output
var CacheConf CacheConfig
var RateLimitConf struct {
	sync.RWMutex
	Conf RateLimitConfig
}
var RunQueue sync.Map // queue birdc commands before execution

var NilParse Parsed = (Parsed)(nil) // special Parsed values
var BirdError Parsed = Parsed{"error": "bird unreachable"}

func IsSpecial(ret Parsed) bool { // test for special Parsed values
	return reflect.DeepEqual(ret, NilParse) || reflect.DeepEqual(ret, BirdError)
}

// intitialize the Cache once during setup with either a MemoryCache or
// RedisCache implementation.
// TODO implement singleton pattern
func InitializeCache() {
	var err error
	if CacheConf.UseRedis {
		cache, err = NewRedisCache(CacheConf)
		if err != nil {
			log.Println("Could not initialize redis cache, falling back to memory cache:", err)
		}
	} else { // initialize the MemoryCache
		maxKeys := CacheConf.MaxKeys
		maxKeysDefault := 60
		if maxKeys == 0 {
			log.Println("MaxKeys not set, using default value:", maxKeysDefault)
			maxKeys = maxKeysDefault
		}

		cache = NewMemoryCache(maxKeys)
		log.Println("Initialized MemoryCache with maxKeys:", maxKeys)
	}
}

// ExpireCache is a convenience method to expire the cache.
func ExpireCache() int {
	return cache.Expire()
}

/* Convenience method to make new entries in the cache.
 * Abstracts over the specific caching implementation and the ability to set
 * individual TTL values for entries. Always use the default TTL value from the
 * config.
 */
func toCache(key string, val Parsed) bool {
	var ttl int
	if ClientConf.CacheTtl >= 0 {
		ttl = ClientConf.CacheTtl
	} else {
		ttl = 5 // five minutes
	}

	if err := cache.Set(key, val, ttl); err != nil {
		log.Println(err)
		return false
	}

	return true
}

/* Convenience method to retrieve entries from the cache.
 * Abstracts over the specific caching implementations.
 * If err returned by cache.Get(key) is set, the value from the cache is not
 * used. There is either a fault e.g. missing entry or the ttl is expired.
 * Handling of specific error conditions e.g. ttl expired but entry present is
 * possible but currently not implemented.
 */
func fromCache(key string) (Parsed, bool) {
	val, err := cache.Get(key)
	if err == nil {
		return val, true
	} else {
		return val, false
	}
	//DEBUG log.Println(err)

}

// Determines the key in the cache, where the result of specific functions are stored.
// Eliminates the need to know what command was executed by that function.
func GetCacheKey(fname string, fargs ...interface{}) string {
	key := strings.ToLower(fname)

	for _, arg := range fargs {
		switch arg.(type) {
		case string:
			key += "_" + strings.ToLower(arg.(string))
		}
	}

	return key
}

func Run(args string) (io.Reader, error) {
	args = "-r " + "show " + args // enforce birdc in restricted mode with "-r" argument
	argsList := strings.Split(args, " ")

	// Allow for arguments in the config
	cmdArgs := strings.Split(ClientConf.BirdCmd, " ")
	birdc := cmdArgs[0]
	cmdArgs = cmdArgs[1:]

	cmd := []string{}
	cmd = append(cmd, cmdArgs...)
	cmd = append(cmd, argsList...)

	out, err := exec.Command(birdc, cmd...).Output()
	if err != nil {
		return nil, err
	}

	return bytes.NewReader(out), nil
}

func InstallRateLimitReset() {
	go func() {
		c := time.Tick(time.Second)

		for _ = range c {
			RateLimitConf.Lock()
			RateLimitConf.Conf.Reqs = RateLimitConf.Conf.Max
			RateLimitConf.Unlock()
		}
	}()
}

func checkRateLimit() bool {
	RateLimitConf.RLock()
	check := !RateLimitConf.Conf.Enabled
	RateLimitConf.RUnlock()
	if check {
		return true
	}

	RateLimitConf.RLock()
	check = RateLimitConf.Conf.Reqs < 1
	RateLimitConf.RUnlock()
	if check {
		return false
	}

	RateLimitConf.Lock()
	RateLimitConf.Conf.Reqs -= 1
	RateLimitConf.Unlock()

	return true
}

func RunAndParse(useCache bool, key string, cmd string, parser func(io.Reader) Parsed, updateCache func(*Parsed)) (Parsed, bool) {
	var wg sync.WaitGroup

	if useCache {
		if val, ok := fromCache(cmd); ok {
			return val, true
		}
	}

	wg.Add(1)
	if queueGroup, queueLoaded := RunQueue.LoadOrStore(cmd, &wg); queueLoaded {
		(*queueGroup.(*sync.WaitGroup)).Wait()

		if val, ok := fromCache(cmd); ok {
			return val, true
		} else {
			// TODO BirdError should also be signaled somehow
			return NilParse, false
		}
	}

	if !checkRateLimit() {
		wg.Done()
		RunQueue.Delete(cmd)
		return NilParse, false
	}

	out, err := Run(cmd)
	if err != nil {
		// ignore errors for now
		wg.Done()
		RunQueue.Delete(cmd)
		return BirdError, false
	}

	parsed := parser(out)

	if updateCache != nil {
		updateCache(&parsed)
	}

	toCache(cmd, parsed)

	wg.Done()
	RunQueue.Delete(cmd)

	return parsed, false
}

func Status(useCache bool) (Parsed, bool) {
	updateParsedCache := func(p *Parsed) {
		status := (*p)["status"].(Parsed)

		// Last Reconfig Timestamp source:
		var lastReconfig string
		switch StatusConf.ReconfigTimestampSource {
		case "bird":
			lastReconfig = status["last_reconfig"].(string)
			break
		case "config_modified":
			lastReconfig = lastReconfigTimestampFromFileStat(
				ClientConf.ConfigFilename,
			)
		case "config_regex":
			lastReconfig = lastReconfigTimestampFromFileContent(
				ClientConf.ConfigFilename,
				StatusConf.ReconfigTimestampMatch,
			)
		}

		status["last_reconfig"] = lastReconfig

		// Filter fields
		for _, field := range StatusConf.FilterFields {
			status[field] = nil
		}
	}

	birdStatus, from_cache := RunAndParse(useCache, GetCacheKey("Status"), "status", parseStatus, updateParsedCache)
	return birdStatus, from_cache
}

func ProtocolsShort(useCache bool) (Parsed, bool) {
	res, from_cache := RunAndParse(useCache, GetCacheKey("ProtocolsShort"), "protocols", parseProtocolsShort, nil)
	return res, from_cache
}

func Protocols(useCache bool) (Parsed, bool) {
	createMetaCache := func(p *Parsed) {
		metaProtocol := Parsed{"protocols": Parsed{"bird_protocol": Parsed{}}}

		for key, _ := range (*p)["protocols"].(Parsed) {
			parsed := (*p)["protocols"].(Parsed)[key].(Parsed)
			protocol := parsed["protocol"].(string)

			birdProtocol := parsed["bird_protocol"].(string)
			// Check if the structure for the current birdProtocol already exists inside the metaProtocol cache, if not create it (BGP|Pipe|etc)
			if _, ok := metaProtocol["protocols"].(Parsed)["bird_protocol"].(Parsed)[birdProtocol]; !ok {
				metaProtocol["protocols"].(Parsed)["bird_protocol"].(Parsed)[birdProtocol] = Parsed{}
			}
			metaProtocol["protocols"].(Parsed)["bird_protocol"].(Parsed)[birdProtocol].(Parsed)[protocol] = &parsed
		}

		toCache(GetCacheKey("metaProtocol"), metaProtocol)
	}

	res, from_cache := RunAndParse(useCache, GetCacheKey("Protocols"), "protocols all", parseProtocols, createMetaCache)
	return res, from_cache
}

func ProtocolsBgp(useCache bool) (Parsed, bool) {
	protocols, from_cache := Protocols(useCache)
	if IsSpecial(protocols) {
		return protocols, from_cache
	}

	protocolsMeta, _ := fromCache(GetCacheKey("metaProtocol"))
	metaProtocol := protocolsMeta["protocols"].(Parsed)

	bgpProtocols := Parsed{}

	for key, protocol := range metaProtocol["bird_protocol"].(Parsed)["BGP"].(Parsed) {
		bgpProtocols[key] = *(protocol.(*Parsed))
	}

	return Parsed{"protocols": bgpProtocols,
		"ttl":       protocols["ttl"],
		"cached_at": protocols["cached_at"]}, from_cache
}

func Symbols(useCache bool) (Parsed, bool) {
	return RunAndParse(useCache, GetCacheKey("Symbols"), "symbols", parseSymbols, nil)
}

func routesQuery(filter string) string {
	cmd := "route " + filter

	if getBirdVersion() < 2 || ClientConf.Dualstack {
		return cmd
	}

	return cmd + " where net.type = NET_IP" + IPVersion
}

func remapTable(table string) string {
	if v := getBirdVersion(); v < 2 {
		return table // Nothing to do for bird1
	}

	if table != "master" {
		return table // Nothing to do here
	}

	// Rewrite master table
	if IPVersion == "4" {
		return "master4"
	}

	return "master6"
}

func RoutesPrefixed(useCache bool, prefix string) (Parsed, bool) {
	cmd := routesQuery(prefix + " all")
	return RunAndParse(
		useCache,
		GetCacheKey("RoutesPrefixed", prefix),
		cmd,
		parseRoutes,
		nil)
}

func RoutesProto(useCache bool, protocol string) (Parsed, bool) {
	cmd := routesQuery("all protocol '" + protocol + "'")
	return RunAndParse(
		useCache,
		GetCacheKey("RoutesProto", protocol),
		cmd,
		parseRoutes,
		nil)
}

func RoutesPeer(useCache bool, peer string) (Parsed, bool) {
	cmd := "route all where from=" + peer
	return RunAndParse(
		useCache,
		GetCacheKey("RoutesPeer", peer),
		cmd,
		parseRoutes,
		nil)
}

func RoutesTableAndPeer(useCache bool, table string, peer string) (Parsed, bool) {
	table = remapTable(table)
	cmd := "route table '" + table + "' all where from=" + peer
	return RunAndParse(
		useCache,
		GetCacheKey("RoutesTableAndPeer", table, peer),
		cmd,
		parseRoutes,
		nil)
}

func RoutesProtoCount(useCache bool, protocol string) (Parsed, bool) {
	cmd := routesQuery("protocol '" + protocol + "' count")
	return RunAndParse(
		useCache,
		GetCacheKey("RoutesProtoCount", protocol),
		cmd,
		parseRoutesCount,
		nil)
}

func RoutesProtoPrimaryCount(useCache bool, protocol string) (Parsed, bool) {
	cmd := routesQuery("primary protocol '" + protocol + "' count")
	return RunAndParse(
		useCache,
		GetCacheKey("RoutesProtoPrimaryCount", protocol),
		cmd,
		parseRoutesCount,
		nil)
}

func PipeRoutesFilteredCount(useCache bool, pipe string, table string, neighborAddress string) (Parsed, bool) {
	table = remapTable(table)
	cmd := "route table '" + table +
		"' noexport '" + pipe +
		"' where from='" + neighborAddress + "' count"
	return RunAndParse(
		useCache,
		GetCacheKey("PipeRoutesFilteredCount", table, pipe, neighborAddress),
		cmd,
		parseRoutesCount,
		nil)
}

func PipeRoutesFiltered(useCache bool, pipe string, table string) (Parsed, bool) {
	table = remapTable(table)
	cmd := "route table '" + table + "' noexport '" + pipe + "' all"
	return RunAndParse(
		useCache,
		GetCacheKey("PipeRoutesFiltered", table, pipe),
		cmd,
		parseRoutes,
		nil)
}

// PipeRoutesFilteredFrom returns the result of the
// show route command:
//
// show route all table <t> noexport <p> protocol <r>
//
// <r> is the id of the peer belonging to the table <t>
func PipeRoutesFilteredProtocol(
	useCache bool,
	pipe string,
	table string,
	protocol string,
) (Parsed, bool) {
	table = remapTable(table)
	cmd := "route table '" + table + "' noexport '" + pipe + "' protocol '" + protocol + "' all"
	fmt.Println("CMD:", cmd)
	return RunAndParse(
		useCache,
		GetCacheKey("PipeRoutesFiltered", table, pipe),
		cmd,
		parseRoutes,
		nil)
}

func RoutesFiltered(useCache bool, protocol string) (Parsed, bool) {
	cmd := routesQuery("all filtered protocol '" + protocol + "'")
	return RunAndParse(
		useCache,
		GetCacheKey("RoutesFiltered", protocol),
		cmd,
		parseRoutes,
		nil)
}

func RoutesExport(useCache bool, protocol string) (Parsed, bool) {
	cmd := routesQuery("all export '" + protocol + "'")
	return RunAndParse(
		useCache,
		GetCacheKey("RoutesExport", protocol),
		cmd,
		parseRoutes,
		nil)
}

func RoutesNoExport(useCache bool, protocol string) (Parsed, bool) {
	cmd := routesQuery("all noexport '" + protocol + "'")
	return RunAndParse(
		useCache,
		GetCacheKey("RoutesNoExport", protocol),
		cmd,
		parseRoutes,
		nil)
}

func RoutesExportCount(useCache bool, protocol string) (Parsed, bool) {
	cmd := routesQuery("export '" + protocol + "' count")
	return RunAndParse(
		useCache,
		GetCacheKey("RoutesExportCount", protocol),
		cmd,
		parseRoutesCount,
		nil)
}

func RoutesTable(useCache bool, table string) (Parsed, bool) {
	table = remapTable(table)
	cmd := routesQuery("table '" + table + "' all")
	return RunAndParse(
		useCache,
		GetCacheKey("RoutesTable", table),
		cmd,
		parseRoutes,
		nil)
}

func RoutesTableFiltered(useCache bool, table string) (Parsed, bool) {
	table = remapTable(table)
	cmd := routesQuery("table '" + table + "' all filtered")
	return RunAndParse(
		useCache,
		GetCacheKey("RoutesTableFiltered", table),
		cmd,
		parseRoutes,
		nil)
}

func RoutesTableCount(useCache bool, table string) (Parsed, bool) {
	table = remapTable(table)
	cmd := routesQuery("table '" + table + "' count")
	return RunAndParse(
		useCache,
		GetCacheKey("RoutesTableCount", table),
		cmd,
		parseRoutesCount,
		nil,
	)
}

func RoutesLookupTable(useCache bool, net string, table string) (Parsed, bool) {
	table = remapTable(table)
	cmd := routesQuery("for " + net + " table '" + table + "' all")
	return RunAndParse(
		useCache,
		GetCacheKey("RoutesLookupTable", net, table),
		cmd,
		parseRoutes,
		nil)
}

func RoutesLookupProtocol(useCache bool, net string, protocol string) (Parsed, bool) {
	cmd := routesQuery("for " + net + " protocol '" + protocol + "' all")
	return RunAndParse(
		useCache,
		GetCacheKey("RoutesLookupProtocol", net, protocol),
		cmd,
		parseRoutes,
		nil)
}

func getBirdVersion() int {
	// We assume the bird major version does not change during
	// the time the birdwatcher is running.
	//
	// However, this requires now a restart when going
	// from bird1 to bird2.
	if BirdVersion != 0 {
		return BirdVersion
	}

	// This method is a bit hacky.
	status, _ := Status(false) // Get status without cache
	if IsSpecial(status) {
		return 0
	}

	birdStatus, ok := status["status"].(Parsed)
	if !ok {
		return 0
	}

	version, ok := birdStatus["version"].(string)
	if !ok {
		return 0
	}

	v, err := strconv.Atoi(string(version[0]))
	if err != nil {
		return 0
	}

	BirdVersion = v
	return v
}
