package settings

import (
	"fmt"
	"path/filepath"

	"github.com/qdm12/gluetun/internal/publicip/api"
	"github.com/qdm12/gosettings"
	"github.com/qdm12/gosettings/reader"
	"github.com/qdm12/gotree"
)

// PublicIP contains settings for port forwarding.
type PublicIP struct {
	// Enabled is set to true to fetch the public ip address
	// information on VPN connection. It defaults to true.
	Enabled *bool
	// IPFilepath is the public IP address status file path
	// to use. It can be the empty string to indicate not
	// to write to a file. It cannot be nil for the
	// internal state
	IPFilepath *string
	// APIs is the list of public ip APIs to use to fetch public IP information.
	// If there is more than one API, the first one is used
	// by default and the others are used as fallbacks in case of
	// the service rate limiting us. It defaults to use all services,
	// with the first one being ipinfo.io for historical reasons.
	APIs []PublicIPAPI
}

type PublicIPAPI struct {
	// Name is the name of the public ip API service.
	// It can be "cloudflare", "ifconfigco", "ip2location" or "ipinfo".
	Name string
	// Token is the token to use for the public ip API service.
	Token string
}

// UpdateWith deep copies the receiving settings, overrides the copy with
// fields set in the partialUpdate argument, validates the new settings
// and returns them if they are valid, or returns an error otherwise.
// In all cases, the receiving settings are unmodified.
func (p PublicIP) UpdateWith(partialUpdate PublicIP) (updatedSettings PublicIP, err error) {
	updatedSettings = p.copy()
	updatedSettings.overrideWith(partialUpdate)
	err = updatedSettings.validate()
	if err != nil {
		return updatedSettings, fmt.Errorf("validating updated settings: %w", err)
	}
	return updatedSettings, nil
}

func (p PublicIP) validate() (err error) {
	if *p.IPFilepath != "" { // optional
		_, err := filepath.Abs(*p.IPFilepath)
		if err != nil {
			return fmt.Errorf("filepath is not valid: %w", err)
		}
	}

	for _, publicIPAPI := range p.APIs {
		_, err = api.ParseProvider(publicIPAPI.Name)
		if err != nil {
			return fmt.Errorf("API name: %w", err)
		}
	}

	return nil
}

func (p *PublicIP) copy() (copied PublicIP) {
	return PublicIP{
		Enabled:    gosettings.CopyPointer(p.Enabled),
		IPFilepath: gosettings.CopyPointer(p.IPFilepath),
		APIs:       gosettings.CopySlice(p.APIs),
	}
}

func (p *PublicIP) overrideWith(other PublicIP) {
	p.Enabled = gosettings.OverrideWithPointer(p.Enabled, other.Enabled)
	p.IPFilepath = gosettings.OverrideWithPointer(p.IPFilepath, other.IPFilepath)
	p.APIs = gosettings.OverrideWithSlice(p.APIs, other.APIs)
}

func (p *PublicIP) setDefaults() {
	p.Enabled = gosettings.DefaultPointer(p.Enabled, true)
	p.IPFilepath = gosettings.DefaultPointer(p.IPFilepath, "/tmp/gluetun/ip")
	p.APIs = gosettings.DefaultSlice(p.APIs, []PublicIPAPI{
		{Name: string(api.IPInfo)},
		{Name: string(api.Cloudflare)},
		{Name: string(api.IfConfigCo)},
		{Name: string(api.IP2Location)},
	})
}

func (p PublicIP) String() string {
	return p.toLinesNode().String()
}

func (p PublicIP) toLinesNode() (node *gotree.Node) {
	if !*p.Enabled {
		return gotree.New("Public IP settings: disabled")
	}

	node = gotree.New("Public IP settings:")

	if *p.IPFilepath != "" {
		node.Appendf("IP file path: %s", *p.IPFilepath)
	}

	baseAPIString := "Public IP data base API: " + p.APIs[0].Name
	if p.APIs[0].Token != "" {
		baseAPIString += " (token " + gosettings.ObfuscateKey(p.APIs[0].Token) + ")"
	}
	node.Append(baseAPIString)
	if len(p.APIs) > 1 {
		backupAPIsNode := node.Append("Public IP data backup APIs:")
		for i := 1; i < len(p.APIs); i++ {
			message := p.APIs[i].Name
			if p.APIs[i].Token != "" {
				message += " (token " + gosettings.ObfuscateKey(p.APIs[i].Token) + ")"
			}
			backupAPIsNode.Append(message)
		}
	}

	return node
}

func (p *PublicIP) read(r *reader.Reader, warner Warner) (err error) {
	p.Enabled, err = readPublicIPEnabled(r, warner)
	if err != nil {
		return err
	}

	p.IPFilepath = r.Get("PUBLICIP_FILE",
		reader.ForceLowercase(false), reader.RetroKeys("IP_STATUS_FILE"))

	apiNames := r.CSV("PUBLICIP_API")
	if len(apiNames) > 0 {
		apiTokens := r.CSV("PUBLICIP_API_TOKEN")
		p.APIs = make([]PublicIPAPI, len(apiNames))
		for i := range apiNames {
			p.APIs[i].Name = apiNames[i]
			var token string
			if i < len(apiTokens) { // only set token if it exists
				token = apiTokens[i]
			}
			p.APIs[i].Token = token
		}
	}

	return nil
}

func readPublicIPEnabled(r *reader.Reader, warner Warner) (
	enabled *bool, err error,
) {
	periodPtr, err := r.DurationPtr("PUBLICIP_PERIOD") // Retro-compatibility
	if err != nil {
		return nil, err
	} else if periodPtr == nil {
		return r.BoolPtr("PUBLICIP_ENABLED")
	}

	if *periodPtr == 0 {
		warner.Warn("please replace PUBLICIP_PERIOD=0 with PUBLICIP_ENABLED=no")
		return ptrTo(false), nil
	}

	warner.Warn("PUBLICIP_PERIOD is no longer used. " +
		"It is assumed from its non-zero value you want PUBLICIP_ENABLED=yes. " +
		"Please migrate to use PUBLICIP_ENABLED only in the future.")
	return ptrTo(true), nil
}
