// Package v2 implements version 2 of the state file
package v2

import (
	"errors"
	"fmt"
	"log/slog"
	"net/url"
	"time"

	"codeberg.org/eduVPN/eduvpn-common/internal/discovery"
	"codeberg.org/eduVPN/eduvpn-common/types/server"
)

// Server is the struct for each server
type Server struct {
	// Profiles are the list of profiles
	Profiles server.Profiles `json:"profiles"`
	// LastAuthorizeTime is the time we last authorized
	// This is used for determining when to show e.g. the renew button
	LastAuthorizeTime time.Time `json:"last_authorize_time"`
	// ExpireTime is the time at which the VPN expires
	ExpireTime time.Time `json:"expire_time"`

	// CountryCode is the country code for the server in case of secure internet
	// Otherwise it is an empty string
	CountryCode string `json:"country_code,omitempty"`

	// LocationProfiles are current profiles for each secure internet location
	LocationProfiles map[string]string `json:"location_profiles,omitempty"`
}

// ServerKey is the key type of the server map
type ServerKey struct {
	// T is the type of server, e.g. secure internet
	T server.Type
	// ID is the identifier for the server
	ID string
}

const keyFormat = "%d,%s"

func newServerType(key string) (*ServerKey, error) {
	var t server.Type
	var id string
	if _, err := fmt.Sscanf(key, keyFormat, &t, &id); err != nil {
		return nil, err
	}

	return &ServerKey{
		T:  t,
		ID: id,
	}, nil
}

// MarshalText convers the server key into one that can be used in a map
func (st ServerKey) MarshalText() ([]byte, error) {
	k := fmt.Sprintf(keyFormat, st.T, st.ID)
	return []byte(k), nil
}

// UnmarshalText converts the marshaled key into a ServerType struct
func (st *ServerKey) UnmarshalText(text []byte) error {
	k := string(text)
	g, err := newServerType(k)
	if err != nil {
		return err
	}
	*st = *g
	return nil
}

// V2 is the top-level struct for the state file
type V2 struct {
	// List is the list of servers
	List map[ServerKey]*Server `json:"server_list,omitempty"`
	// LastChosen represents the key of the last chosen server
	// A server is chosen if we got a config for it
	LastChosen *ServerKey `json:"last_chosen_id,omitempty"`
	// Discovery is the cached list of discovery JSON
	Discovery discovery.Discovery `json:"discovery"`
}

// RemoveServer removes a server with id `id` and type `t` from the V2 struct
// It returns an error if no such server exists
func (cfg *V2) RemoveServer(id string, t server.Type) error {
	k := ServerKey{
		ID: id,
		T:  t,
	}

	if _, ok := cfg.List[k]; ok {
		delete(cfg.List, k)

		// reset the last chosen
		if cfg.LastChosen != nil && *cfg.LastChosen == k {
			cfg.LastChosen = nil
		}
		return nil
	}
	return errors.New("server does not exist")
}

func (cfg *V2) getServerWithKey(k ServerKey) (*Server, error) {
	if v, ok := cfg.List[k]; ok {
		return v, nil
	}
	return nil, errors.New("server does not exist")
}

// GetServer gets a server with id `id` and type `t`
// If the server doesn't exist it returns nil and an error
func (cfg *V2) GetServer(id string, t server.Type) (*Server, error) {
	k := ServerKey{
		ID: id,
		T:  t,
	}
	return cfg.getServerWithKey(k)
}

// CurrentServer gets the last chosen server
// It returns the server, the server type and an error if it doesn't exist
func (cfg *V2) CurrentServer() (*Server, *ServerKey, error) {
	if cfg.LastChosen == nil {
		return nil, nil, errors.New("no server chosen before")
	}
	srv, err := cfg.getServerWithKey(*cfg.LastChosen)
	if err != nil {
		return nil, nil, err
	}
	return srv, cfg.LastChosen, nil
}

// HasSecureInternet returns true whether or not the state file
// has a secure internet server in it
func (cfg *V2) HasSecureInternet() bool {
	for k := range cfg.List {
		if k.T == server.TypeSecureInternet {
			return true
		}
	}
	return false
}

// AddServer adds a server with id `id`, type `t` and server `srv`
func (cfg *V2) AddServer(id string, t server.Type, srv Server) error {
	if cfg.HasSecureInternet() && t == server.TypeSecureInternet {
		return errors.New("a secure internet server already exists, remove the other secure internet server first")
	}
	gsrv, gerr := cfg.GetServer(id, t)
	if gsrv != nil && gerr == nil {
		return errors.New("this server is already added, remove it first before adding it again")
	}
	k := ServerKey{
		ID: id,
		T:  t,
	}
	if cfg.List == nil {
		cfg.List = make(map[ServerKey]*Server)
	}
	cfg.List[k] = &srv
	return nil
}

// PublicCurrent gets the current server as a type that should be returned to the client
// It returns this server or nil and an error if it doesn't exist
func (cfg *V2) PublicCurrent(disco *discovery.Discovery) (*server.Current, error) {
	curr, _, err := cfg.CurrentServer()
	if err != nil {
		return nil, err
	}
	rcurr := &server.Current{}
	// SAFETY: LastChosen is guaranteed to be non-nil here
	switch cfg.LastChosen.T {
	case server.TypeInstituteAccess:
		g := convertInstitute(cfg.LastChosen.ID, disco)
		g.Profiles = curr.Profiles
		rcurr.Institute = &g
	case server.TypeSecureInternet:
		g := convertSecure(cfg.LastChosen.ID, curr.CountryCode, disco)
		g.Profiles = curr.Profiles
		rcurr.SecureInternet = &g
	case server.TypeCustom:
		g := convertCustom(cfg.LastChosen.ID)
		g.Profiles = curr.Profiles
		rcurr.Custom = &g
	default:
		return nil, fmt.Errorf("unknown connected type: %d", cfg.LastChosen.T)
	}
	rcurr.Type = cfg.LastChosen.T
	return rcurr, nil
}

func convertInstitute(url string, disco *discovery.Discovery) server.Institute {
	dsrv, err := disco.ServerByURL(url, "institute_access")
	// server is delisted
	if err != nil {
		return server.Institute{
			Server: server.Server{
				DisplayName: map[string]string{
					"en": url,
				},
				Identifier: url,
			},
			Delisted: true,
		}
	}

	return server.Institute{
		Server: server.Server{
			DisplayName: dsrv.DisplayName,
			Identifier:  url,
		},
		SupportContacts: dsrv.SupportContact,
	}
}

func convertCustom(u string) server.Server {
	dn := u
	pu, err := url.Parse(u)
	if err != nil {
		slog.Error("failed to parse server hostname", "error", err)
	} else {
		dn = pu.Hostname()
	}
	return server.Server{
		DisplayName: map[string]string{
			"en": dn,
		},
		Identifier: u,
	}
}

func convertSecure(orgID string, countryCode string, disco *discovery.Discovery) server.SecureInternet {
	dorg, _, err := disco.SecureHomeArgs(orgID)
	locs := disco.SecureLocationList()
	// server is delisted
	if err != nil {
		return server.SecureInternet{
			Server: server.Server{
				DisplayName: map[string]string{
					"en": orgID,
				},
				Identifier: orgID,
			},
			CountryCode: countryCode,
			Locations:   locs,
			Delisted:    true,
		}
	}
	return server.SecureInternet{
		Server: server.Server{
			DisplayName: dorg.DisplayName,
			Identifier:  dorg.OrgID,
		},
		CountryCode: countryCode,
		Locations:   locs,
	}
}

// PublicList gets all the servers in a format that is returned to the client
func (cfg *V2) PublicList(disco *discovery.Discovery) *server.List {
	ret := &server.List{}
	for k, v := range cfg.List {
		switch k.T {
		case server.TypeInstituteAccess:
			g := convertInstitute(k.ID, disco)
			g.Profiles = v.Profiles
			ret.Institutes = append(ret.Institutes, g)
		case server.TypeSecureInternet:
			g := convertSecure(k.ID, v.CountryCode, disco)
			g.Profiles = v.Profiles
			ret.SecureInternet = &g
		case server.TypeCustom:
			g := convertCustom(k.ID)
			g.Profiles = v.Profiles
			ret.Custom = append(ret.Custom, g)
		default:
			slog.Error("no such server type in list", "type", k.T)
			continue
		}
	}
	return ret
}
