// Package client retrieves data over Gemini and implements a TOFU system.
package client

import (
	"errors"
	"io/ioutil"
	"net"
	"net/url"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/makeworld-the-better-one/go-gemini"
	gemsocks5 "github.com/makeworld-the-better-one/go-gemini-socks5"
	"github.com/mitchellh/go-homedir"
	"github.com/spf13/viper"
)

// Simple key for certCache map and others, instead of a full URL
// Only uses the part of the URL relevant to matching certs to a URL
type certMapKey struct {
	host string
	path string
}

var (
	// [auth] section of config put into maps
	confCerts = make(map[certMapKey]string)
	confKeys  = make(map[certMapKey]string)

	// Cache the cert and key assigned to different URLs
	certCache   = make(map[certMapKey][][]byte)
	certCacheMu = &sync.RWMutex{}

	fetchClient *gemini.Client
)

func Init() error {
	fetchClient = &gemini.Client{
		ConnectTimeout: 10 * time.Second, // Default is 15
		ReadTimeout:    time.Duration(viper.GetInt("a-general.page_max_time")) * time.Second,
	}

	if socksHost := os.Getenv("AMFORA_SOCKS5"); socksHost != "" {
		fetchClient.Proxy = gemsocks5.ProxyFunc(socksHost, nil)
	}

	// Populate config maps

	certsViper := viper.Sub("auth.certs")
	for _, certURL := range certsViper.AllKeys() {
		// Normalize URL so that it can be matched no matter how it was written
		// in the config
		pu, _ := normalizeURL(FixUserURL(certURL))
		if pu == nil {
			//nolint:goerr113
			return errors.New("[auth.certs]: couldn't normalize URL: " + certURL)
		}
		confCerts[certMapKey{pu.Host, pu.Path}] = certsViper.GetString(certURL)
	}

	keysViper := viper.Sub("auth.keys")
	for _, keyURL := range keysViper.AllKeys() {
		pu, _ := normalizeURL(FixUserURL(keyURL))
		if pu == nil {
			//nolint:goerr113
			return errors.New("[auth.keys]: couldn't normalize URL: " + keyURL)
		}
		confKeys[certMapKey{pu.Host, pu.Path}] = keysViper.GetString(keyURL)
	}

	return nil
}

// getCertPath returns the path of the cert from the config.
// It returns "" if no config value exists.
func getCertPath(host string, path string) string {
	for k, v := range confCerts {
		if k.host == host && (k.path == path || strings.HasPrefix(path, k.path)) {
			// Either exact match to what's in config, or a subpath
			return v
		}
	}
	// No matches
	return ""
}

// getKeyPath returns the path of the key from the config.
// It returns "" if no config value exists.
func getKeyPath(host string, path string) string {
	for k, v := range confKeys {
		if k.host == host && (k.path == path || strings.HasPrefix(path, k.path)) {
			// Either exact match to what's in config, or a subpath
			return v
		}
	}
	// No matches
	return ""
}

func clientCert(host string, path string) ([]byte, []byte) {
	mkey := certMapKey{host, path}

	certCacheMu.RLock()
	pair, ok := certCache[mkey]
	certCacheMu.RUnlock()
	if ok {
		return pair[0], pair[1]
	}

	ogCertPath := getCertPath(host, path)
	// Expand paths starting with ~/
	certPath, err := homedir.Expand(ogCertPath)
	if err != nil {
		certPath = ogCertPath
	}
	ogKeyPath := getKeyPath(host, path)
	keyPath, err := homedir.Expand(ogKeyPath)
	if err != nil {
		keyPath = ogKeyPath
	}

	if certPath == "" && keyPath == "" {
		certCacheMu.Lock()
		certCache[mkey] = [][]byte{nil, nil}
		certCacheMu.Unlock()
		return nil, nil
	}

	cert, err := ioutil.ReadFile(certPath)
	if err != nil {
		certCacheMu.Lock()
		certCache[mkey] = [][]byte{nil, nil}
		certCacheMu.Unlock()
		return nil, nil
	}
	key, err := ioutil.ReadFile(keyPath)
	if err != nil {
		certCacheMu.Lock()
		certCache[mkey] = [][]byte{nil, nil}
		certCacheMu.Unlock()
		return nil, nil
	}

	certCacheMu.Lock()
	certCache[mkey] = [][]byte{cert, key}
	certCacheMu.Unlock()
	return cert, key
}

// HasClientCert returns whether or not a client certificate exists for a host and path.
func HasClientCert(host string, path string) bool {
	cert, _ := clientCert(host, path)
	return cert != nil
}

func fetch(u string, c *gemini.Client) (*gemini.Response, error) {
	parsed, _ := url.Parse(u)
	cert, key := clientCert(parsed.Host, parsed.Path)

	var res *gemini.Response
	var err error
	if cert != nil {
		res, err = c.FetchWithCert(u, cert, key)
	} else {
		res, err = c.Fetch(u)
	}
	if err != nil {
		return nil, err
	}

	ok := handleTofu(parsed.Hostname(), parsed.Port(), res.Cert)
	if !ok {
		return res, ErrTofu
	}

	return res, err
}

// Fetch returns response data and an error.
// The error text is human friendly and should be displayed.
func Fetch(u string) (*gemini.Response, error) {
	return fetch(u, fetchClient)
}

func fetchWithProxy(proxyHostname, proxyPort, u string, c *gemini.Client) (*gemini.Response, error) {
	parsed, _ := url.Parse(u)
	cert, key := clientCert(parsed.Host, parsed.Path)

	var res *gemini.Response
	var err error
	if cert != nil {
		res, err = c.FetchWithHostAndCert(net.JoinHostPort(proxyHostname, proxyPort), u, cert, key)
	} else {
		res, err = c.FetchWithHost(net.JoinHostPort(proxyHostname, proxyPort), u)
	}
	if err != nil {
		return nil, err
	}

	// Only associate the returned cert with the proxy
	ok := handleTofu(proxyHostname, proxyPort, res.Cert)
	if !ok {
		return res, ErrTofu
	}

	return res, nil
}

// FetchWithProxy is the same as Fetch, but uses a proxy.
func FetchWithProxy(proxyHostname, proxyPort, u string) (*gemini.Response, error) {
	return fetchWithProxy(proxyHostname, proxyPort, u, fetchClient)
}
