package config

import (
	"fmt"
	"net/url"
	"os"
	"strings"
)

// Resolved is the answer to "who am I and where am I pointed", worked out once
// per invocation from flags, environment and the two files, in that order.
type Resolved struct {
	Host   string
	APIURL string
	Token  string
	// Source says where the token came from, because "it worked yesterday" is
	// almost always an environment variable nobody remembers exporting.
	Source string
	Entry  *Host
}

// ErrNoToken is returned when nothing is signed in. Callers turn it into the
// one message worth printing: how to sign in.
type ErrNoToken struct{ Host string }

func (e *ErrNoToken) Error() string {
	return fmt.Sprintf("not signed in to %s.\nRun `warmbly auth login` to sign in, or set WARMBLY_TOKEN to an API key (wmbly_...).", e.Host)
}

// Resolve works out the active host and token. hostFlag is --host, empty when
// not given.
func Resolve(cfg *Config, hosts Hosts, hostFlag string) (*Resolved, error) {
	host := strings.TrimSpace(hostFlag)
	if host == "" {
		host = strings.TrimSpace(os.Getenv(HostEnv))
	}
	if host == "" {
		host = strings.TrimSpace(cfg.ActiveHost)
	}
	if host == "" {
		// One signed-in host and no preference is not ambiguous.
		if names := hosts.Names(); len(names) == 1 {
			host = names[0]
		}
	}
	if host == "" {
		host = DefaultHost
	}
	host = NormalizeHost(host)

	r := &Resolved{Host: host, Entry: hosts[host]}
	if r.Entry != nil {
		r.APIURL = r.Entry.APIURL
		r.Token = r.Entry.Token
		r.Source = HostsPath()
	}

	// The environment wins, and says so, so a surprising result is traceable.
	if v := strings.TrimSpace(os.Getenv(TokenEnv)); v != "" {
		r.Token, r.Source = v, TokenEnv
	} else if v := strings.TrimSpace(os.Getenv(APIKeyEnv)); v != "" {
		r.Token, r.Source = v, APIKeyEnv
	}
	if v := strings.TrimSpace(os.Getenv(APIURLEnv)); v != "" {
		r.APIURL = strings.TrimRight(v, "/")
	}
	if r.APIURL == "" {
		r.APIURL = DefaultAPIURL(host)
	}
	if r.Token == "" {
		return r, &ErrNoToken{Host: host}
	}
	return r, nil
}

// NormalizeHost turns whatever someone typed into the key hosts.yml uses: a
// bare host, no scheme, no path, no trailing slash.
func NormalizeHost(raw string) string {
	h := strings.TrimSpace(strings.ToLower(raw))
	h = strings.TrimSuffix(h, "/")
	if h == "" {
		return DefaultHost
	}
	if strings.Contains(h, "://") {
		if u, err := url.Parse(h); err == nil && u.Host != "" {
			h = u.Host
		}
	}
	// A bare "app.warmbly.com" or "api.warmbly.com" means the same account as
	// "warmbly.com"; storing three entries for one instance helps nobody. The
	// dot count keeps a real two-label host like "api.dev" intact.
	for _, prefix := range []string{"app.", "api.", "www."} {
		if strings.HasPrefix(h, prefix) && strings.Count(h, ".") > 1 {
			return strings.TrimPrefix(h, prefix)
		}
	}
	return h
}

// isLoopback reports whether a host is this machine. That is the only case
// where plaintext HTTP is acceptable, because every request carries a bearer
// token: a remote host that merely names a port must still get https.
func isLoopback(host string) bool {
	name, _, found := strings.Cut(host, ":")
	if !found {
		name = host
	}
	return name == "localhost" || name == "127.0.0.1" || name == "[::1]" || name == "::1"
}

// DefaultAPIURL is the base URL to try first for a host. The hosted service is
// known; a self-hosted host follows the layout the installer writes.
func DefaultAPIURL(host string) string {
	host = NormalizeHost(host)
	if host == DefaultHost {
		return "https://api." + DefaultHost
	}
	if isLoopback(host) {
		return "http://" + host
	}
	// A host that names a port is already an exact address, so no subdomain is
	// invented for it; the scheme stays https.
	if strings.Contains(host, ":") {
		return "https://" + host
	}
	return "https://api." + host
}

// CandidateAPIURLs are the bases `auth login` probes for a self-hosted host,
// most likely first. The installer's three shapes produce the first three.
func CandidateAPIURLs(host string) []string {
	host = NormalizeHost(host)
	if strings.Contains(host, "://") {
		return []string{strings.TrimRight(host, "/")}
	}
	scheme := "https"
	if isLoopback(host) {
		scheme = "http"
	}
	// A host with a port is already an exact address; do not invent subdomains.
	if strings.Contains(host, ":") {
		return []string{scheme + "://" + host}
	}
	return []string{
		scheme + "://api." + host,
		scheme + "://" + host,
		scheme + "://" + host + "/api",
	}
}
