package server

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

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

// RPC constants
const (
	RPCPort uint16 = 39453
)

// Server is the main server type
type Server struct {
	cfg *Config
	db  database.Storage
	ts  *tun2.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
}

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)
	}
}

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)

	// gRPC setup
	gs := grpc.NewServer(grpc.Creds(credentials.NewTLS(tc)))

	proto.RegisterBackendsServer(gs, &Backend{Server: s})
	proto.RegisterRoutesServer(gs, &Route{Server: s})
	proto.RegisterTokensServer(gs, &Token{Server: s})

	l, err := net.Listen("tcp", cfg.GRPCAddr)
	if err != nil {
		return nil, err
	}

	go gs.Serve(l)

	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")
}

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)
	w.Header().Add("Alt-Svc", fmt.Sprintf(`quic="%s"; ma=2592000; v="39"`, s.cfg.QuicAddr))

	// 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)
}