package main

import (
	"context"
	"crypto/tls"
	"fmt"
	"log"
	"net"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strings"
	"time"

	"github.com/viki-org/dnscache"
)

func main() {
	c, err := NewConfig()
	if err != nil {
		panic(err)
	}

	s := &http.Server{
		Addr:    fmt.Sprintf(":%d", c.Port),
		Handler: c,
	}
	foo := s.ListenAndServe
	if c.Cert.CRT != "" {
		foo = func() error {
			return s.ListenAndServeTLS(c.Cert.CRT, c.Cert.Key)
		}
	}
	log.Printf("listening on %v...", s.Addr)
	if err := foo(); err != nil {
		panic(err)
	}
}

var resolver = dnscache.New(time.Minute * 500)

func (c Config) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	endpoint := c.endpoint(r)
	if endpoint.To == "" {
		http.NotFound(w, r)
		return
	}

	if endpoint.Redir {
		u, _ := url.Parse(endpoint.To)
		r.URL = u
		r.Host = u.Host
		http.Redirect(w, r, r.URL.String(), http.StatusSeeOther)
		return
	}

	cors(w)
	if r.Method == http.MethodOptions {
		w.Header().Set("Content-Length", "0")
		w.Header().Set("Content-Type", "text/plain")
		w.WriteHeader(http.StatusOK)
		return
	}

	if r.URL.Scheme == "https" {
		w.Header().Set("X-Forwarded-Proto", "https")
	}

	if c.handleAdmin(w, r) {
		return
	}

	if !c.basicAuth(w, r) {
		return
	}

	u, err := url.Parse(endpoint.To)
	if err != nil {
		log.Printf("[%s] %v", c.key(r), err)
	}

	var transport http.Transport
	transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
	if ip, _ := resolver.FetchOneString(strings.Split(u.Host, ":")[0]); ip != "" {
		transport.DialContext = func(ctx context.Context, network string, address string) (net.Conn, error) {
			if idx := strings.LastIndex(u.Host, ":"); idx > 0 {
				ip = ip + u.Host[idx:]
			}
			return (&net.Dialer{}).DialContext(ctx, network, ip)
		}
	}

	proxy := httputil.NewSingleHostReverseProxy(u)
	proxy.Transport = redirPurge{
		proxyHost:     r.Host,
		targetHost:    u.Host,
		baseTransport: &transport,
	}
	proxy.ServeHTTP(w, r)
}

func cors(w http.ResponseWriter) {
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set("Access-Control-Allow-Headers", "X-Auth-Token, content-type, Content-Type")
	w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS, TRACE, PATCH, HEAD, DELETE")
}

func (c Config) key(r *http.Request) string {
	return strings.Split(r.Host, ".")[0]
}

func (c Config) handleAdmin(w http.ResponseWriter, r *http.Request) bool {
	switch c.key(r) {
	case "_":
		panic("not impl: list")
	case "home":
		panic("not impl: home")
	}
	return false
}

func (c Config) basicAuth(w http.ResponseWriter, r *http.Request) bool {
	basicAuth := c.endpoint(r).BasicAuth
	if noAuth := basicAuth == ""; noAuth {
		return true
	}

	u, p, _ := r.BasicAuth()
	if fmt.Sprintf("%s:%s", u, p) != basicAuth {
		w.Header().Set("WWW-Authenticate", "Basic")
		http.Error(w, "unexpected basic auth", http.StatusUnauthorized)
		return false
	}

	return true
}

func (c Config) endpoint(r *http.Request) Endpoint {
	key := c.key(r)
	domain := strings.TrimPrefix(r.Host, key)
	m, ok := c.Domains[domain]
	if !ok {
		return Endpoint{}
	}
	return m[c.key(r)]
}

type redirPurge struct {
	proxyHost     string
	targetHost    string
	baseTransport http.RoundTripper
}

func (rp redirPurge) RoundTrip(r *http.Request) (*http.Response, error) {
	resp, err := rp.baseTransport.RoundTrip(r)
	if err != nil {
		return resp, err
	}
	if loc := resp.Header.Get("Location"); loc != "" {
		resp.Header.Set("Location", strings.Replace(loc, rp.targetHost, rp.proxyHost, 1))
	}
	// google floc https://paramdeo.com/blog/opting-your-website-out-of-googles-floc-network
	resp.Header.Set("Permissions-Policy", "interest-cohort=()")
	return resp, err
}
