// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package vhost

import (
	"fmt"
	"strings"
	"sync"
	"time"

	"github.com/fatedier/frp/utils/log"
	frpNet "github.com/fatedier/frp/utils/net"

	"github.com/fatedier/golib/errors"
)

type muxFunc func(frpNet.Conn) (frpNet.Conn, map[string]string, error)
type httpAuthFunc func(frpNet.Conn, string, string, string) (bool, error)
type hostRewriteFunc func(frpNet.Conn, string) (frpNet.Conn, error)

type VhostMuxer struct {
	listener       frpNet.Listener
	timeout        time.Duration
	vhostFunc      muxFunc
	authFunc       httpAuthFunc
	rewriteFunc    hostRewriteFunc
	registryRouter *VhostRouters
	mutex          sync.RWMutex
}

func NewVhostMuxer(listener frpNet.Listener, vhostFunc muxFunc, authFunc httpAuthFunc, rewriteFunc hostRewriteFunc, timeout time.Duration) (mux *VhostMuxer, err error) {
	mux = &VhostMuxer{
		listener:       listener,
		timeout:        timeout,
		vhostFunc:      vhostFunc,
		authFunc:       authFunc,
		rewriteFunc:    rewriteFunc,
		registryRouter: NewVhostRouters(),
	}
	go mux.run()
	return mux, nil
}

type CreateConnFunc func() (frpNet.Conn, error)

type VhostRouteConfig struct {
	Domain      string
	Location    string
	RewriteHost string
	Username    string
	Password    string
	Headers     map[string]string

	CreateConnFn CreateConnFunc
}

// listen for a new domain name, if rewriteHost is not empty  and rewriteFunc is not nil
// then rewrite the host header to rewriteHost
func (v *VhostMuxer) Listen(cfg *VhostRouteConfig) (l *Listener, err error) {
	v.mutex.Lock()
	defer v.mutex.Unlock()

	_, ok := v.registryRouter.Exist(cfg.Domain, cfg.Location)
	if ok {
		return nil, fmt.Errorf("hostname [%s] location [%s] is already registered", cfg.Domain, cfg.Location)
	}

	l = &Listener{
		name:        cfg.Domain,
		location:    cfg.Location,
		rewriteHost: cfg.RewriteHost,
		userName:    cfg.Username,
		passWord:    cfg.Password,
		mux:         v,
		accept:      make(chan frpNet.Conn),
		Logger:      log.NewPrefixLogger(""),
	}
	v.registryRouter.Add(cfg.Domain, cfg.Location, l)
	return l, nil
}

func (v *VhostMuxer) getListener(name, path string) (l *Listener, exist bool) {
	v.mutex.RLock()
	defer v.mutex.RUnlock()

	// first we check the full hostname
	// if not exist, then check the wildcard_domain such as *.example.com
	vr, found := v.registryRouter.Get(name, path)
	if found {
		return vr.payload.(*Listener), true
	}

	domainSplit := strings.Split(name, ".")
	if len(domainSplit) < 3 {
		return l, false
	}
	domainSplit[0] = "*"
	name = strings.Join(domainSplit, ".")

	vr, found = v.registryRouter.Get(name, path)
	if !found {
		return
	}

	return vr.payload.(*Listener), true
}

func (v *VhostMuxer) run() {
	for {
		conn, err := v.listener.Accept()
		if err != nil {
			return
		}
		go v.handle(conn)
	}
}

func (v *VhostMuxer) handle(c frpNet.Conn) {
	if err := c.SetDeadline(time.Now().Add(v.timeout)); err != nil {
		c.Close()
		return
	}

	sConn, reqInfoMap, err := v.vhostFunc(c)
	if err != nil {
		log.Warn("get hostname from http/https request error: %v", err)
		c.Close()
		return
	}

	name := strings.ToLower(reqInfoMap["Host"])
	path := strings.ToLower(reqInfoMap["Path"])
	l, ok := v.getListener(name, path)
	if !ok {
		res := notFoundResponse()
		res.Write(c)
		log.Debug("http request for host [%s] path [%s] not found", name, path)
		c.Close()
		return
	}

	// if authFunc is exist and userName/password is set
	// then verify user access
	if l.mux.authFunc != nil && l.userName != "" && l.passWord != "" {
		bAccess, err := l.mux.authFunc(c, l.userName, l.passWord, reqInfoMap["Authorization"])
		if bAccess == false || err != nil {
			l.Debug("check http Authorization failed")
			res := noAuthResponse()
			res.Write(c)
			c.Close()
			return
		}
	}

	if err = sConn.SetDeadline(time.Time{}); err != nil {
		c.Close()
		return
	}
	c = sConn

	l.Debug("get new http request host [%s] path [%s]", name, path)
	err = errors.PanicToError(func() {
		l.accept <- c
	})
	if err != nil {
		l.Warn("listener is already closed, ignore this request")
	}
}

type Listener struct {
	name        string
	location    string
	rewriteHost string
	userName    string
	passWord    string
	mux         *VhostMuxer // for closing VhostMuxer
	accept      chan frpNet.Conn
	log.Logger
}

func (l *Listener) Accept() (frpNet.Conn, error) {
	conn, ok := <-l.accept
	if !ok {
		return nil, fmt.Errorf("Listener closed")
	}

	// if rewriteFunc is exist
	// rewrite http requests with a modified host header
	// if l.rewriteHost is empty, nothing to do
	if l.mux.rewriteFunc != nil {
		sConn, err := l.mux.rewriteFunc(conn, l.rewriteHost)
		if err != nil {
			l.Warn("host header rewrite failed: %v", err)
			return nil, fmt.Errorf("host header rewrite failed")
		}
		l.Debug("rewrite host to [%s] success", l.rewriteHost)
		conn = sConn
	}

	for _, prefix := range l.GetAllPrefix() {
		conn.AddLogPrefix(prefix)
	}
	return conn, nil
}

func (l *Listener) Close() error {
	l.mux.registryRouter.Del(l.name, l.location)
	close(l.accept)
	return nil
}

func (l *Listener) Name() string {
	return l.name
}
