package client

import (
	"fmt"
	"log/slog"

	"codeberg.org/eduVPN/eduvpn-common/i18n/err"
	"codeberg.org/eduVPN/eduvpn-common/internal/fsm"
)

type (
	// FSMStateID is an alias to the fsm state ID type
	FSMStateID = fsm.StateID
	// FSMStates is an alias to the fsm states type
	FSMStates = fsm.States
	// FSMState is an alias to the fsm state type
	FSMState = fsm.State
	// FSMTransition is an alias to the fsm transition type
	FSMTransition = fsm.Transition
)

const (
	// StateDeregistered is the state where we are deregistered
	StateDeregistered FSMStateID = iota

	// StateMain is the main state
	StateMain

	// StateAddingServer is the state where a server is being added
	StateAddingServer

	// StateOAuthStarted means the state where the OAuth procedure is triggered
	StateOAuthStarted

	// StateGettingConfig is the state a VPN config is being obtained
	StateGettingConfig

	// StateAskLocation is the state where a secure internet location is being asked
	StateAskLocation

	// StateAskProfile is the state where a profile is being asked for
	StateAskProfile

	// StateGotConfig is the state where a config is obtained
	StateGotConfig

	// StateConnecting is the state where the VPN is connecting
	StateConnecting

	// StateConnected is the state where the VPN is connected
	StateConnected

	// StateDisconnecting is the state where the VPN is disconnecting
	StateDisconnecting

	// StateDisconnected is the state where the VPN is disconnected
	StateDisconnected
)

// GetStateName gets the State name for state `s`
func GetStateName(s FSMStateID) string {
	switch s {
	case StateDeregistered:
		return "Deregistered"
	case StateMain:
		return "Main"
	case StateAddingServer:
		return "AddingServer"
	case StateOAuthStarted:
		return "OAuthStarted"
	case StateGettingConfig:
		return "GettingConfig"
	case StateAskLocation:
		return "AskLocation"
	case StateAskProfile:
		return "AskProfile"
	case StateGotConfig:
		return "GotConfig"
	case StateConnecting:
		return "Connecting"
	case StateConnected:
		return "Connected"
	case StateDisconnecting:
		return "Disconnecting"
	case StateDisconnected:
		return "Disconnected"
	default:
		panic(fmt.Sprintf("unknown conversion of state: %d to string", s))
	}
}

func newFSM(
	callback func(FSMStateID, FSMStateID, any) bool,
) fsm.FSM {
	states := FSMStates{
		StateDeregistered: FSMState{
			Transitions: []FSMTransition{
				{To: StateMain, Description: "Register"},
			},
		},
		StateMain: FSMState{
			Transitions: []FSMTransition{
				{To: StateDeregistered, Description: "Deregister"},
				{To: StateAddingServer, Description: "Add a server"},
				{To: StateGettingConfig, Description: "Get a VPN config"},
				{To: StateConnected, Description: "Already connected"},
			},
		},
		StateAddingServer: FSMState{
			Transitions: []FSMTransition{
				{To: StateOAuthStarted, Description: "Authorize"},
			},
		},
		StateOAuthStarted: FSMState{
			Transitions: []FSMTransition{
				{To: StateMain, Description: "Authorized"},
				{To: StateDisconnected, Description: "Cancel, was disconnected"},
				{To: StateGotConfig, Description: "Cancel, was got config"},
			},
		},
		StateGettingConfig: FSMState{
			Transitions: []FSMTransition{
				{To: StateAskLocation, Description: "Invalid location"},
				{To: StateAskProfile, Description: "Invalid or no profile"},
				{To: StateDisconnected, Description: "Go back to disconnected"},
				{To: StateGotConfig, Description: "Successfully got a configuration"},
				{To: StateOAuthStarted, Description: "Authorize"},
			},
		},
		StateAskLocation: FSMState{
			Transitions: []FSMTransition{
				{To: StateGettingConfig, Description: "Location chosen"},
			},
		},
		StateAskProfile: FSMState{
			Transitions: []FSMTransition{
				{To: StateGettingConfig, Description: "Profile chosen"},
			},
		},
		StateGotConfig: FSMState{
			Transitions: []FSMTransition{
				{To: StateGettingConfig, Description: "Get a VPN config again"},
				{To: StateConnecting, Description: "VPN is connecting"},
				{To: StateOAuthStarted, Description: "Renew"},
			},
		},
		StateConnecting: FSMState{
			Transitions: []FSMTransition{
				{To: StateConnected, Description: "VPN is connected"},
				{To: StateDisconnecting, Description: "Cancel connecting"},
			},
		},
		StateConnected: FSMState{
			Transitions: []FSMTransition{
				{To: StateDisconnecting, Description: "VPN is disconnecting"},
			},
		},
		StateDisconnecting: FSMState{
			Transitions: []FSMTransition{
				{To: StateDisconnected, Description: "VPN is disconnected"},
				{To: StateConnected, Description: "Cancel disconnecting"},
			},
		},
		StateDisconnected: FSMState{
			Transitions: []FSMTransition{
				{To: StateConnecting, Description: "Connect with existing config"},
				{To: StateGettingConfig, Description: "Connect with a new config"},
				{To: StateOAuthStarted, Description: "Renew"},
			},
		},
	}

	cbLogged := func(oldState FSMStateID, newState FSMStateID, data any) bool {
		if oldState != newState {
			slog.Debug("FSM transition", "old", GetStateName(oldState), "new", GetStateName(newState))
		}
		return callback(oldState, newState, data)
	}

	return fsm.NewFSM(StateMain, states, cbLogged, GetStateName)
}

// SetState sets the state for the client FSM to `state`
func (c *Client) SetState(state FSMStateID) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	curr := c.FSM.Current
	_, err := c.FSM.GoTransition(state)
	if err != nil {
		// self-transitions are only debug errors
		if c.FSM.InState(state) {
			slog.Debug("attempt at an invalid self-transition", "transition", c.FSM.GetStateName(state))
			return nil
		}
		return i18nerr.WrapInternalf(err, "Failed internal state transition requested by the client from: '%s' to '%s'", GetStateName(curr), GetStateName(state))
	}
	return nil
}

// InState returns whether or not the client is in state `state`
func (c *Client) InState(state FSMStateID) bool {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.FSM.InState(state)
}
