package chain

import (
	"fmt"
	"os"
	"strconv"
	"sync"
	"time"
)

type ErrDebug struct {
	cond  StopCond
	value int
}

type StopCond int

// stop before swap chain
const (
	DEBUG_CHAIN_STOP StopCond = 0 + iota
	DEBUG_CHAIN_RANDOM_STOP
	DEBUG_CHAIN_BP_SLEEP
	DEBUG_CHAIN_OTHER_SLEEP
	DEBUG_SYNCER_CRASH
	DEBUG_RAFT_SNAP_FREQ // change snap frequency after first snapshot
)

const (
	DEBUG_CHAIN_STOP_INF = DEBUG_RAFT_SNAP_FREQ
)

var (
	EnvNameStaticCrash     = "DEBUG_CHAIN_CRASH"       // 1 ~ 4
	EnvNameRandomCrashTime = "DEBUG_RANDOM_CRASH_TIME" // 1 ~ 600000(=10min) ms
	EnvNameChainBPSleep    = "DEBUG_CHAIN_BP_SLEEP"    // bp node sleeps before connecting block for each block (ms). used
	EnvNameChainOtherSleep = "DEBUG_CHAIN_OTHER_SLEEP" // non bp node sleeps before connecting block for each block (ms).
	EnvNameSyncCrash       = "DEBUG_SYNCER_CRASH"      // case 1
	EnvNameRaftSnapFreq    = "DEBUG_RAFT_SNAP_FREQ"    // case 1
)

var stopConds = [...]string{
	EnvNameStaticCrash,
	EnvNameRandomCrashTime,
	EnvNameChainBPSleep,
	EnvNameChainOtherSleep,
	EnvNameSyncCrash,
	EnvNameRaftSnapFreq,
}

type DebugHandler func(value int) error

func (c StopCond) String() string { return stopConds[c] }

func (ec *ErrDebug) Error() string {
	return fmt.Sprintf("stopped by debugger cond[%s]=%d", ec.cond.String(), ec.value)
}

type Debugger struct {
	sync.RWMutex
	condMap map[StopCond]int
	isEnv   map[StopCond]bool
}

func newDebugger() *Debugger {
	dbg := &Debugger{condMap: make(map[StopCond]int), isEnv: make(map[StopCond]bool)}

	checkEnv := func(condName StopCond) {
		envName := stopConds[condName]

		envStr := os.Getenv(envName)
		if len(envStr) > 0 {
			val, err := strconv.Atoi(envStr)
			if err != nil {
				logger.Error().Err(err).Msgf("environment variable %s must be integer", envName)
				return
			}
			logger.Debug().Int("value", val).Msgf("env variable[%s] is set", envName)

			dbg.Set(condName, val, true)
		}
	}

	checkEnv(DEBUG_CHAIN_STOP)
	checkEnv(DEBUG_CHAIN_RANDOM_STOP)
	checkEnv(DEBUG_CHAIN_BP_SLEEP)
	checkEnv(DEBUG_CHAIN_OTHER_SLEEP)
	checkEnv(DEBUG_SYNCER_CRASH)
	checkEnv(DEBUG_RAFT_SNAP_FREQ)

	return dbg
}

func (debug *Debugger) Set(cond StopCond, value int, env bool) {
	if debug == nil {
		return
	}

	debug.Lock()
	defer debug.Unlock()

	logger.Debug().Int("cond", int(cond)).Str("name", stopConds[cond]).Int("val", value).Msg("set debug condition")

	debug.condMap[cond] = value
	debug.isEnv[cond] = env
}

func (debug *Debugger) Unset(cond StopCond) {
	if debug == nil {
		return
	}

	debug.Lock()
	defer debug.Unlock()

	logger.Debug().Str("cond", cond.String()).Msg("debugger condition is unset")
	delete(debug.condMap, cond)
}

func (debug *Debugger) clear() {
	if debug == nil {
		return
	}

	debug.Lock()
	defer debug.Unlock()

	debug.condMap = make(map[StopCond]int)
	debug.isEnv = make(map[StopCond]bool)
}

func (debug *Debugger) Check(cond StopCond, value int, handler DebugHandler) error {
	if debug == nil {
		return nil
	}

	debug.Lock()
	defer debug.Unlock()

	if setVal, ok := debug.condMap[cond]; ok {
		logger.Debug().Str("cond", stopConds[cond]).Int("val", setVal).Msg("check debug condition")

		switch cond {
		case DEBUG_CHAIN_STOP:
			if setVal == value {
				if debug.isEnv[cond] {
					logger.Fatal().Str("cond", stopConds[cond]).Msg("shutdown by DEBUG_CHAIN_CRASH")
				} else {
					return &ErrDebug{cond: cond, value: value}
				}
			}

		case DEBUG_CHAIN_RANDOM_STOP:
			go crashRandom(setVal)
			handleCrashRandom(setVal)

		case DEBUG_CHAIN_OTHER_SLEEP, DEBUG_CHAIN_BP_SLEEP:
			handleChainSleep(setVal)

		case DEBUG_SYNCER_CRASH:
			if setVal == value {
				return handleSyncerCrash(setVal, cond)
			}
		case DEBUG_RAFT_SNAP_FREQ:
			handler(setVal)
		}
	}

	return nil
}

func handleChainSleep(sleepMils int) {
	logger.Debug().Int("sleep(ms)", sleepMils).Msg("before chain sleep")

	time.Sleep(time.Millisecond * time.Duration(sleepMils))

	logger.Debug().Msg("after chain sleep")
}

func handleCrashRandom(waitMils int) {
	logger.Debug().Int("after(ms)", waitMils).Msg("before random crash")

	go crashRandom(waitMils)
}

func handleSyncerCrash(val int, cond StopCond) error {
	if val >= 1 {
		logger.Fatal().Int("val", val).Msg("sync crash by DEBUG_SYNC_CRASH")
		return nil
	} else {
		return &ErrDebug{cond: cond, value: val}
	}
}

func crashRandom(waitMils int) {
	if waitMils <= 0 {
		return
	}

	time.Sleep(time.Millisecond * time.Duration(waitMils))

	logger.Debug().Msg("shutdown by DEBUG_RANDOM_CRASH_TIME")

	os.Exit(100)
}
