package gost

import (
	"bufio"
	"crypto/tls"
	"encoding/base64"
	"errors"
	"io"
	"net"
	"net/http"
	"net/http/httputil"
	"time"

	"github.com/ginuerzh/pht"
	"github.com/go-log/log"
	"github.com/golang/glog"
	"golang.org/x/net/http2"
)

type HttpServer struct {
	conn net.Conn
	Base *ProxyServer
}

func NewHttpServer(conn net.Conn, base *ProxyServer) *HttpServer {
	return &HttpServer{
		conn: conn,
		Base: base,
	}
}

// Default HTTP server handler
func (s *HttpServer) HandleRequest(req *http.Request) {

}

func (s *HttpServer) forwardRequest(req *http.Request) {
	last := s.Base.Chain.lastNode
	if last == nil {
		return
	}
	cc, err := s.Base.Chain.GetConn()
	if err != nil {
		glog.V(LWARNING).Infof("[http] %s -> %s : %s", s.conn.RemoteAddr(), last.Addr, err)

		b := []byte("HTTP/1.1 503 Service unavailable\r\n" +
			"Proxy-Agent: gost/" + Version + "\r\n\r\n")
		glog.V(LDEBUG).Infof("[http] %s <- %s\n%s", s.conn.RemoteAddr(), last.Addr, string(b))
		s.conn.Write(b)
		return
	}
	defer cc.Close()

	if len(last.Users) > 0 {
		user := last.Users[0]
		s := user.String()
		if _, set := user.Password(); !set {
			s += ":"
		}
		req.Header.Set("Proxy-Authorization",
			"Basic "+base64.StdEncoding.EncodeToString([]byte(s)))
	}

	cc.SetWriteDeadline(time.Now().Add(WriteTimeout))
	if err = req.WriteProxy(cc); err != nil {
		glog.V(LWARNING).Infof("[http] %s -> %s : %s", s.conn.RemoteAddr(), req.Host, err)
		return
	}
	cc.SetWriteDeadline(time.Time{})

	glog.V(LINFO).Infof("[http] %s <-> %s", s.conn.RemoteAddr(), req.Host)
	s.Base.transport(s.conn, cc)
	glog.V(LINFO).Infof("[http] %s >-< %s", s.conn.RemoteAddr(), req.Host)
	return
}

type httpHandler struct {
	server Server
}

func HTTPHandler(server Server) Handler {
	return &httpHandler{server: server}
}

func (h *httpHandler) Handle(conn net.Conn) {
	req, err := http.ReadRequest(bufio.NewReader(conn))
	if err != nil {
		log.Log("[http]", err)
		return
	}

	log.Logf("[http] %s %s - %s %s", req.Method, conn.RemoteAddr(), req.Host, req.Proto)

	if Debug {
		dump, _ := httputil.DumpRequest(req, false)
		log.Logf(string(dump))
	}

	if req.Method == "PRI" && req.ProtoMajor == 2 {
		log.Logf("[http] %s <- %s : Not an HTTP2 server", conn.RemoteAddr(), req.Host)
		resp := "HTTP/1.1 400 Bad Request\r\n" +
			"Proxy-Agent: gost/" + Version + "\r\n\r\n"
		conn.Write([]byte(resp))
		return
	}

	valid := false
	u, p, _ := basicProxyAuth(req.Header.Get("Proxy-Authorization"))
	users := h.server.Options().BaseOptions().Users
	for _, user := range users {
		username := user.Username()
		password, _ := user.Password()
		if (u == username && p == password) ||
			(u == username && password == "") ||
			(username == "" && p == password) {
			valid = true
			break
		}
	}

	if len(users) > 0 && !valid {
		log.Logf("[http] %s <- %s : proxy authentication required", conn.RemoteAddr(), req.Host)
		resp := "HTTP/1.1 407 Proxy Authentication Required\r\n" +
			"Proxy-Authenticate: Basic realm=\"gost\"\r\n" +
			"Proxy-Agent: gost/" + Version + "\r\n\r\n"
		conn.Write([]byte(resp))
		return
	}

	req.Header.Del("Proxy-Authorization")

	// forward http request
	//lastNode := s.Base.Chain.lastNode
	//if lastNode != nil && lastNode.Transport == "" && (lastNode.Protocol == "http" || lastNode.Protocol == "") {
	//	s.forwardRequest(req)
	//	return
	//}

	// if !s.Base.Node.Can("tcp", req.Host) {
	//	glog.Errorf("Unauthorized to tcp connect to %s", req.Host)
	//	return
	// }

	cc, err := h.server.Chain().Dial(req.Host)
	if err != nil {
		log.Logf("[http] %s -> %s : %s", conn.RemoteAddr(), req.Host, err)

		b := []byte("HTTP/1.1 503 Service unavailable\r\n" +
			"Proxy-Agent: gost/" + Version + "\r\n\r\n")
		if Debug {
			log.Logf("[http] %s <- %s\n%s", conn.RemoteAddr(), req.Host, string(b))
		}
		conn.Write(b)
		return
	}
	defer cc.Close()

	if req.Method == http.MethodConnect {
		b := []byte("HTTP/1.1 200 Connection established\r\n" +
			"Proxy-Agent: gost/" + Version + "\r\n\r\n")
		if Debug {
			log.Logf("[http] %s <- %s\n%s", conn.RemoteAddr(), req.Host, string(b))
		}
		conn.Write(b)
	} else {
		req.Header.Del("Proxy-Connection")
		// req.Header.Set("Connection", "Keep-Alive")

		if err = req.Write(cc); err != nil {
			log.Logf("[http] %s -> %s : %s", conn.RemoteAddr(), req.Host, err)
			return
		}
	}

	log.Logf("[http] %s <-> %s", conn.RemoteAddr(), req.Host)
	Transport(conn, cc)
	log.Logf("[http] %s >-< %s", conn.RemoteAddr(), req.Host)
}

type Http2Server struct {
	Base      *ProxyServer
	Handler   http.Handler
	TLSConfig *tls.Config
}

func NewHttp2Server(base *ProxyServer) *Http2Server {
	return &Http2Server{Base: base}
}

func (s *Http2Server) ListenAndServeTLS(config *tls.Config) error {
	srv := http.Server{
		Addr:      s.Base.Node.Addr,
		Handler:   s.Handler,
		TLSConfig: config,
	}
	if srv.Handler == nil {
		srv.Handler = http.HandlerFunc(s.HandleRequest)
	}
	http2.ConfigureServer(&srv, nil)
	return srv.ListenAndServeTLS("", "")
}

// Default HTTP2 server handler
func (s *Http2Server) HandleRequest(w http.ResponseWriter, req *http.Request) {
	target := req.Header.Get("Gost-Target")
	if target == "" {
		target = req.Host
	}
	glog.V(LINFO).Infof("[http2] %s %s - %s %s", req.Method, req.RemoteAddr, target, req.Proto)
	if glog.V(LDEBUG) {
		dump, _ := httputil.DumpRequest(req, false)
		glog.Infoln(string(dump))
	}

	w.Header().Set("Proxy-Agent", "gost/"+Version)

	if !s.Base.Node.Can("tcp", target) {
		glog.Errorf("Unauthorized to tcp connect to %s", target)
		return
	}

	// HTTP2 as transport
	if req.Header.Get("Proxy-Switch") == "gost" {
		conn, err := s.Upgrade(w, req)
		if err != nil {
			glog.V(LINFO).Infof("[http2] %s -> %s : %s", req.RemoteAddr, target, err)
			return
		}
		glog.V(LINFO).Infof("[http2] %s - %s : switch to HTTP2 transport mode OK", req.RemoteAddr, target)
		s.Base.handleConn(conn)
		return
	}

	valid := false
	u, p, _ := basicProxyAuth(req.Header.Get("Proxy-Authorization"))
	for _, user := range s.Base.Node.Users {
		username := user.Username()
		password, _ := user.Password()
		if (u == username && p == password) ||
			(u == username && password == "") ||
			(username == "" && p == password) {
			valid = true
			break
		}
	}
	if len(s.Base.Node.Users) > 0 && !valid {
		glog.V(LWARNING).Infof("[http2] %s <- %s : proxy authentication required", req.RemoteAddr, target)
		w.WriteHeader(http.StatusProxyAuthRequired)
		return
	}

	req.Header.Del("Proxy-Authorization")
	req.Header.Del("Proxy-Connection")

	c, err := s.Base.Chain.Dial(target)
	if err != nil {
		glog.V(LWARNING).Infof("[http2] %s -> %s : %s", req.RemoteAddr, target, err)
		w.WriteHeader(http.StatusServiceUnavailable)
		return
	}
	defer c.Close()

	glog.V(LINFO).Infof("[http2] %s <-> %s", req.RemoteAddr, target)

	if req.Method == http.MethodConnect {
		w.WriteHeader(http.StatusOK)
		if fw, ok := w.(http.Flusher); ok {
			fw.Flush()
		}

		// compatible with HTTP1.x
		if hj, ok := w.(http.Hijacker); ok && req.ProtoMajor == 1 {
			// we take over the underly connection
			conn, _, err := hj.Hijack()
			if err != nil {
				glog.V(LWARNING).Infof("[http2] %s -> %s : %s", req.RemoteAddr, target, err)
				w.WriteHeader(http.StatusInternalServerError)
				return
			}
			defer conn.Close()
			glog.V(LINFO).Infof("[http2] %s -> %s : downgrade to HTTP/1.1", req.RemoteAddr, target)
			s.Base.transport(conn, c)
			return
		}

		errc := make(chan error, 2)
		go func() {
			_, err := io.Copy(c, req.Body)
			errc <- err
		}()
		go func() {
			_, err := io.Copy(flushWriter{w}, c)
			errc <- err
		}()

		select {
		case <-errc:
			// glog.V(LWARNING).Infoln("exit", err)
		}
		glog.V(LINFO).Infof("[http2] %s >-< %s", req.RemoteAddr, target)
		return
	}

	// req.Header.Set("Connection", "Keep-Alive")
	if err = req.Write(c); err != nil {
		glog.V(LWARNING).Infof("[http2] %s -> %s : %s", req.RemoteAddr, target, err)
		return
	}

	resp, err := http.ReadResponse(bufio.NewReader(c), req)
	if err != nil {
		glog.V(LWARNING).Infoln("[http2] %s -> %s : %s", req.RemoteAddr, target, err)
		return
	}
	defer resp.Body.Close()

	for k, v := range resp.Header {
		for _, vv := range v {
			w.Header().Add(k, vv)
		}
	}
	w.WriteHeader(resp.StatusCode)
	if _, err := io.Copy(flushWriter{w}, resp.Body); err != nil {
		glog.V(LWARNING).Infof("[http2] %s <- %s : %s", req.RemoteAddr, target, err)
	}
	glog.V(LINFO).Infof("[http2] %s >-< %s", req.RemoteAddr, target)
}

// Upgrade upgrade an HTTP2 request to a bidirectional connection that preparing for tunneling other protocol, just like a websocket connection.
func (s *Http2Server) Upgrade(w http.ResponseWriter, r *http.Request) (net.Conn, error) {
	if r.Method != http.MethodConnect {
		w.WriteHeader(http.StatusMethodNotAllowed)
		return nil, errors.New("Method not allowed")
	}

	w.WriteHeader(http.StatusOK)

	if fw, ok := w.(http.Flusher); ok {
		fw.Flush()
	}

	conn := &http2Conn{r: r.Body, w: flushWriter{w}}
	conn.remoteAddr, _ = net.ResolveTCPAddr("tcp", r.RemoteAddr)
	conn.localAddr, _ = net.ResolveTCPAddr("tcp", r.Host)
	return conn, nil
}

// HTTP2 client connection, wrapped up just like a net.Conn
type http2Conn struct {
	r          io.Reader
	w          io.Writer
	remoteAddr net.Addr
	localAddr  net.Addr
}

func (c *http2Conn) Read(b []byte) (n int, err error) {
	return c.r.Read(b)
}

func (c *http2Conn) Write(b []byte) (n int, err error) {
	return c.w.Write(b)
}

func (c *http2Conn) Close() (err error) {
	if rc, ok := c.r.(io.Closer); ok {
		err = rc.Close()
	}
	if w, ok := c.w.(io.Closer); ok {
		err = w.Close()
	}
	return
}

func (c *http2Conn) LocalAddr() net.Addr {
	return c.localAddr
}

func (c *http2Conn) RemoteAddr() net.Addr {
	return c.remoteAddr
}

func (c *http2Conn) SetDeadline(t time.Time) error {
	return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
}

func (c *http2Conn) SetReadDeadline(t time.Time) error {
	return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
}

func (c *http2Conn) SetWriteDeadline(t time.Time) error {
	return &net.OpError{Op: "set", Net: "http2", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
}

type flushWriter struct {
	w io.Writer
}

func (fw flushWriter) Write(p []byte) (n int, err error) {
	defer func() {
		if r := recover(); r != nil {
			if s, ok := r.(string); ok {
				err = errors.New(s)
				return
			}
			err = r.(error)
		}
	}()

	n, err = fw.w.Write(p)
	if err != nil {
		// glog.V(LWARNING).Infoln("flush writer:", err)
		return
	}
	if f, ok := fw.w.(http.Flusher); ok {
		f.Flush()
	}
	return
}

type PureHttpServer struct {
	Base    *ProxyServer
	Handler func(net.Conn)
}

func NewPureHttpServer(base *ProxyServer) *PureHttpServer {
	return &PureHttpServer{
		Base: base,
	}
}

func (s *PureHttpServer) ListenAndServe() error {
	server := pht.Server{
		Addr: s.Base.Node.Addr,
		Key:  s.Base.Node.Get("key"),
	}
	if server.Handler == nil {
		server.Handler = s.handleConn
	}
	return server.ListenAndServe()
}

func (s *PureHttpServer) handleConn(conn net.Conn) {
	glog.V(LINFO).Infof("[pht] %s - %s", conn.RemoteAddr(), conn.LocalAddr())
	s.Base.handleConn(conn)
}
