package main

import (
	"crypto/tls"
	"errors"
	"net"
	"net/http"
	"net/http/httputil"
	"time"

	"git.xeserv.us/xena/route/internal/database"
	"git.xeserv.us/xena/route/internal/middleware"
	"git.xeserv.us/xena/route/internal/tun2"
	proto "git.xeserv.us/xena/route/proto"
	"github.com/Xe/ln"
	"github.com/lucas-clemente/quic-go/h2quic"
	"github.com/mtneug/pkg/ulid"
	kcp "github.com/xtaci/kcp-go"
	"golang.org/x/crypto/acme/autocert"
	"golang.org/x/net/context"
)

// RPC constants
const (
	RPCPort uint16 = 39453
)

// Server is the main server type.
type Server struct {
	cfg *Config
	db  database.Storage
	ts  *tun2.Server

	QuicServer *h2quic.Server

	*autocert.Manager
}

// Config configures Server
type Config struct {
	BoltDBPath string `env:"BOLTDB_PATH,required"`

	WebAddr        string `env:"WEB_ADDR,required"`
	SSLAddr        string `env:"SSL_ADDR,required"`
	QuicAddr       string `env:"QUIC_ADDR,required"`
	BackendTCPAddr string `env:"BACKEND_TCP_ADDR,required"`
	BackendKCPAddr string `env:"BACKEND_KCP_ADDR,required"`
	GRPCAddr       string `env:"GRPC_ADDR,required"`

	DomainSuffix string `env:"DOMAIN_SUFFIX,required"`
	ACMEEmail    string `env:"ACME_EMAIL,required"`
	CertKey      *[32]byte
}

// listenTCP configures a listener for TCP+TLS agent connections.
func (s *Server) listenTCP(ctx context.Context, addr string, tcfg *tls.Config) {
	l, err := tls.Listen("tcp", addr, tcfg)
	if err != nil {
		panic(err)
	}

	ln.Log(ctx, ln.Action("tcp+tls listening"), ln.F{"addr": l.Addr()})

	for {
		conn, err := l.Accept()
		if err != nil {
			ln.Error(ctx, err, ln.Action("accept backend client socket"))
		}

		ln.Log(ctx, ln.F{
			"action":  "new backend client",
			"addr":    conn.RemoteAddr(),
			"network": conn.RemoteAddr().Network(),
		})

		go s.ts.HandleConn(conn, false)
	}
}

// listenKCP configures a listener for KCP+TLS agent connections.
func (s *Server) listenKCP(ctx context.Context, addr string, tcfg *tls.Config) {
	l, err := kcp.Listen(addr)
	if err != nil {
		panic(err)
	}

	ln.Log(ctx, ln.Action("kcp+tls listening"), ln.F{"addr": l.Addr()})

	for {
		conn, err := l.Accept()
		if err != nil {
			ln.Error(ctx, err, ln.F{"kind": "kcp", "addr": l.Addr().String()})
		}

		ln.Log(ctx, ln.F{
			"action":  "new_client",
			"network": conn.RemoteAddr().Network(),
			"addr":    conn.RemoteAddr(),
		})

		tc := tls.Server(conn, tcfg)

		go s.ts.HandleConn(tc, true)
	}
}

// New creates a new Server
func New(cfg Config) (*Server, error) {
	if cfg.CertKey == nil {
		return nil, errors.New("no cert decryption key, can't do anything")
	}

	db, err := database.NewBoltStorage(cfg.BoltDBPath, cfg.CertKey)
	if err != nil {
		return nil, err
	}

	m := &autocert.Manager{
		Prompt:     autocert.AcceptTOS,
		Cache:      database.Cache(db),
		HostPolicy: nil,
		Email:      cfg.ACMEEmail,
	}

	tcfg := &tun2.ServerConfig{
		Storage: &storageWrapper{
			Storage: db,
		},
	}

	ts, err := tun2.NewServer(tcfg)
	if err != nil {
		return nil, err
	}

	s := &Server{
		cfg: &cfg,
		db:  db,
		ts:  ts,

		Manager: m,
	}

	tc := &tls.Config{
		GetCertificate: m.GetCertificate,
	}

	go s.listenKCP(context.Background(), cfg.BackendKCPAddr, tc)
	go s.listenTCP(context.Background(), cfg.BackendTCPAddr, tc)

	bhdr := proto.NewBackendsServer(&Backend{Server: s}, s.makeTwirpHooks())
	rhdr := proto.NewRoutesServer(&Route{Server: s}, s.makeTwirpHooks())
	thdr := proto.NewTokensServer(&Token{Server: s}, s.makeTwirpHooks())
	mux := http.NewServeMux()

	mux.Handle(proto.BackendsPathPrefix, bhdr)
	mux.Handle(proto.RoutesPathPrefix, rhdr)
	mux.Handle(proto.TokensPathPrefix, thdr)

	hs := &http.Server{
		TLSConfig: tc,
		Addr:      cfg.GRPCAddr,
		Handler:   middleware.Trace("twirp-https")(mux),
	}

	go hs.ListenAndServeTLS("", "")

	return s, nil
}

// Director removes headers that are typically stripped off at request ingress.
func (s *Server) Director(r *http.Request) {
	r.Header.Del("X-Forwarded-For")
	r.Header.Del("X-Client-Ip")
}

// ServeHTTP proxies traffic to a remote backend based on the request meta-information.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Header.Get("X-Tor2web") != "" {
		http.Error(w, "tor2web proxy use is not allowed", 400)
		return
	}

	host, _, err := net.SplitHostPort(r.RemoteAddr)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}
	r.Header.Set("X-Remote-Ip", host)
	r.Header.Set("X-Request-Ingress", time.Now().Format(time.RFC3339))

	rid := ulid.New().String()
	r.Header.Set("X-Request-Id", rid)
	w.Header().Set("X-Request-Id", rid)
	s.QuicServer.SetQuicHeaders(w.Header())

	// http://www.gnuterrypratchett.com/
	w.Header().Set("X-Clacks-Overhead", "GNU Ashlynn")

	rp := &httputil.ReverseProxy{
		Director:      s.Director,
		Transport:     s.ts,
		FlushInterval: 1 * time.Second,
	}

	rp.ServeHTTP(w, r)
}

// insecureRedirect redirects a client to https if they connect over plain HTTP.
func insecureRedirect(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodPatch, http.MethodPut, http.MethodPost:
		http.Error(w, "use https", http.StatusNotAcceptable)
		ln.Log(r.Context(), ln.Action("cannot redirect (wrong method)"), ln.F{"remote": r.RemoteAddr, "host": r.Host, "path": r.URL.Path})
		return
	}

	r.URL.Host = r.Host
	r.URL.Scheme = "https"

	ln.Log(r.Context(), ln.Action("redirecting insecure HTTP to HTTPS"), ln.F{"remote": r.RemoteAddr, "host": r.Host, "path": r.URL.Path})

	http.Redirect(w, r, r.URL.String(), http.StatusPermanentRedirect)
}
