// Package server implements functions that have to deal with server interaction
package server

import (
	"context"
	"errors"
	"os"
	"time"

	v2 "codeberg.org/eduVPN/eduvpn-common/internal/config/v2"
	"codeberg.org/eduVPN/eduvpn-common/internal/eduvpnapi"
	"codeberg.org/eduVPN/eduvpn-common/internal/eduvpnapi/profiles"
	"codeberg.org/eduVPN/eduvpn-common/types/protocol"
	srvtypes "codeberg.org/eduVPN/eduvpn-common/types/server"
)

// Server is the struct for a single server
type Server struct {
	identifier string
	t          srvtypes.Type
	apiw       *eduvpnapi.API
	storage    *v2.V2
}

// ErrInvalidProfile is an error that is returned when an invalid profile has been chosen
var ErrInvalidProfile = errors.New("invalid profile")

// NewServer creates a new server
func (s *Servers) NewServer(identifier string, t srvtypes.Type, api *eduvpnapi.API) Server {
	return Server{
		identifier: identifier,
		t:          t,
		apiw:       api,
		storage:    s.config,
	}
}

// Profiles gets the cached profiles from the configuration/state file
func (s *Server) Profiles() (*srvtypes.Profiles, error) {
	cfgs, err := s.cfgServer()
	if err != nil {
		return nil, err
	}
	return &cfgs.Profiles, nil
}

// FreshProfiles gets the profiles for the server
// It only does a /info network request if the profiles have not been cached
// force indicates whether or not the profiles should be fetched fresh
func (s *Server) FreshProfiles(ctx context.Context) (*profiles.Info, error) {
	a, err := s.api()
	if err != nil {
		return nil, err
	}
	// Otherwise get fresh profiles and set the cache
	prfs, err := a.Info(ctx)
	if err != nil {
		return nil, err
	}
	// Update the profile list in the config
	err = s.SetProfileList(prfs.Public())
	if err != nil {
		return nil, err
	}
	return prfs, nil
}

func (s *Server) api() (*eduvpnapi.API, error) {
	if s.apiw == nil {
		return nil, errors.New("no API object found")
	}
	return s.apiw, nil
}

func (s *Server) findProfile(ctx context.Context) (*profiles.Profile, error) {
	// Get the profiles by ignoring the cache
	prfs, err := s.FreshProfiles(ctx)
	if err != nil {
		return nil, err
	}

	var chosenP profiles.Profile

	n := prfs.Len()
	switch n {
	// no profiles available
	case 0:
		return nil, errors.New("the server has no available profiles for your account")
	case 1:
		// Only one profile, make sure it is set
		chosenP = prfs.MustIndex(0)
	default:
		// Profile doesn't exist
		prID, err := s.ProfileID()
		if err != nil {
			return nil, err
		}
		v := prfs.Get(prID)
		if v == nil {
			return nil, ErrInvalidProfile
		}
		chosenP = *v
	}
	return &chosenP, nil
}

func (s *Server) connect(ctx context.Context, pTCP bool) (*srvtypes.Configuration, error) {
	a, err := s.api()
	if err != nil {
		return nil, err
	}

	// find a suitable profile to connect
	chosenP, err := s.findProfile(ctx)
	if err != nil {
		return nil, err
	}
	err = s.SetProfileID(chosenP.ID)
	if err != nil {
		return nil, err
	}

	// protos supported by the client
	protos := []protocol.Protocol{protocol.OpenVPN, protocol.WireGuard}
	// If profile supports both protocols we remove openvpn from client support if EDUVPN_PREFER_WG is set to "1"
	// This also only happens if prefer TCP is set to false
	if os.Getenv("EDUVPN_PREFER_WG") == "1" {
		if chosenP.HasWireGuard() && chosenP.HasOpenVPN() {
			protos = []protocol.Protocol{protocol.WireGuard}
		}
	}
	// SAFETY: chosenP is guaranteed to be non-nil
	apicfg, err := a.Connect(ctx, *chosenP, protos, pTCP)
	if err != nil {
		return nil, err
	}
	err = s.SetExpireTime(apicfg.Expires)
	if err != nil {
		return nil, err
	}
	return &srvtypes.Configuration{
		VPNConfig:        apicfg.Configuration,
		Protocol:         apicfg.Protocol,
		DefaultGateway:   chosenP.DefaultGateway,
		DNSSearchDomains: chosenP.DNSSearchDomains,
		ShouldFailover:   chosenP.ShouldFailover() && !pTCP,
	}, nil
}

// Disconnect sends an API /disconnect to the server
func (s *Server) Disconnect(ctx context.Context) error {
	a, err := s.api()
	if err != nil {
		return err
	}
	return a.Disconnect(ctx)
}

func (s *Server) cfgServer() (*v2.Server, error) {
	if s.storage == nil {
		return nil, errors.New("cannot get server, no configuration passed")
	}
	return s.storage.GetServer(s.identifier, s.t)
}

// SetProfileID sets the profile id `id` for the server
func (s *Server) SetProfileID(id string) error {
	cs, err := s.cfgServer()
	if err != nil {
		return err
	}
	oldP := cs.Profiles.Current
	cs.Profiles.Current = id

	if s.t == srvtypes.TypeSecureInternet {
		if cs.LocationProfiles == nil {
			cs.LocationProfiles = make(map[string]string)
		}
		cs.LocationProfiles[cs.CountryCode] = oldP
	}
	return nil
}

// SetProfileList sets the profile list `prfs` for the server
func (s *Server) SetProfileList(prfs srvtypes.Profiles) error {
	cs, err := s.cfgServer()
	if err != nil {
		return err
	}
	cs.Profiles.Map = prfs.Map
	return nil
}

// SetExpireTime sets the time `et` when the VPN expires
func (s *Server) SetExpireTime(et time.Time) error {
	cs, err := s.cfgServer()
	if err != nil {
		return err
	}
	cs.ExpireTime = et
	return nil
}

// ProfileID gets the profile ID for the server
func (s *Server) ProfileID() (string, error) {
	cs, err := s.cfgServer()
	if err != nil {
		return "", err
	}
	return cs.Profiles.Current, nil
}

// SetCurrent sets the current server in the state file to this one
func (s *Server) SetCurrent() error {
	if s.storage == nil {
		return errors.New("no storage available")
	}
	s.storage.LastChosen = &v2.ServerKey{
		ID: s.identifier,
		T:  s.t,
	}
	return nil
}
