package chserver

import (
	"context"
	"errors"
	"log"
	"net/http"
	"net/http/httputil"
	"net/url"
	"regexp"
	"sync"
	"time"

	"github.com/gorilla/websocket"
	chshare "github.com/inverse-inc/packetfence/go/chisel/share"
	"github.com/inverse-inc/packetfence/go/chisel/share/ccrypto"
	"github.com/inverse-inc/packetfence/go/chisel/share/cio"
	"github.com/inverse-inc/packetfence/go/chisel/share/cnet"
	"github.com/inverse-inc/packetfence/go/chisel/share/radius_proxy"
	"github.com/inverse-inc/packetfence/go/chisel/share/settings"
	"github.com/inverse-inc/packetfence/go/connector"
	"github.com/inverse-inc/packetfence/go/pfconfigdriver"
	"github.com/jpillora/requestlog"
	"github.com/redis/go-redis/v9"
	"golang.org/x/crypto/ssh"
)

// Config is the configuration for the chisel service
type Config struct {
	KeySeed   string
	AuthFile  string
	Auth      string
	Proxy     string
	Socks5    bool
	Reverse   bool
	KeepAlive time.Duration
	TLS       TLSConfig
}

// Server respresent a chisel service
type Server struct {
	*cio.Logger
	config                *Config
	connectors            *connector.ConnectorsContainer
	fingerprint           string
	httpServer            *cnet.HTTPServer
	reverseProxy          *httputil.ReverseProxy
	sessCount             int32
	sessions              *settings.Users
	sshConfig             *ssh.ServerConfig
	users                 *settings.UserIndex
	listenProto           string
	redis                 *redis.Client
	redisTunnelsNamespace string
	connectorsIndexes     map[string]int
	connectorIndexesLock  *sync.Mutex
	radiusProxy           *radius_proxy.Proxy
}

var upgrader = websocket.Upgrader{
	CheckOrigin:     func(r *http.Request) bool { return true },
	ReadBufferSize:  settings.EnvInt("WS_BUFF_SIZE", 0),
	WriteBufferSize: settings.EnvInt("WS_BUFF_SIZE", 0),
}

// NewServer creates and returns a new chisel server
func NewServer(c *Config) (*Server, error) {
	server := &Server{
		config:               c,
		connectors:           connector.NewConnectorsContainer(context.Background()),
		httpServer:           cnet.NewHTTPServer(),
		Logger:               cio.NewLogger("server"),
		sessions:             settings.NewUsers(),
		connectorsIndexes:    map[string]int{},
		connectorIndexesLock: &sync.Mutex{},
	}
	server.Info = true
	server.users = settings.NewUserIndex(server.Logger)
	if c.AuthFile != "" {
		if err := server.users.LoadUsers(c.AuthFile); err != nil {
			return nil, err
		}
	}
	if c.Auth != "" {
		u := &settings.User{Addrs: []*regexp.Regexp{settings.UserAllowAll}}
		u.Name, u.Pass = settings.ParseAuth(c.Auth)
		if u.Name != "" {
			server.users.AddUser(u)
		}
	}
	//generate private key (optionally using seed)
	key, err := ccrypto.GenerateKey(c.KeySeed)
	if err != nil {
		log.Fatal("Failed to generate key")
	}
	//convert into ssh.PrivateKey
	private, err := ssh.ParsePrivateKey(key)
	if err != nil {
		log.Fatal("Failed to parse key")
	}
	//fingerprint this key
	server.fingerprint = ccrypto.FingerprintKey(private.PublicKey())
	//create ssh config
	server.sshConfig = &ssh.ServerConfig{
		ServerVersion:    "SSH-" + chshare.ProtocolVersion + "-server",
		PasswordCallback: server.authUser,
	}
	server.sshConfig.AddHostKey(private)
	//setup reverse proxy
	if c.Proxy != "" {
		u, err := url.Parse(c.Proxy)
		if err != nil {
			return nil, err
		}
		if u.Host == "" {
			return nil, server.Errorf("Missing protocol (%s)", u)
		}
		server.reverseProxy = httputil.NewSingleHostReverseProxy(u)
		//always use proxy host
		server.reverseProxy.Director = func(r *http.Request) {
			//enforce origin, keep path
			r.URL.Scheme = u.Scheme
			r.URL.Host = u.Host
			r.Host = u.Host
		}
	}
	//print when reverse tunnelling is enabled
	if c.Reverse {
		server.Infof("Reverse tunnelling enabled")
	}

	return server, nil
}

// Run is responsible for starting the chisel service.
// Internally this calls Start then Wait.
func (s *Server) Run(host, port string) error {
	if err := s.Start(host, port); err != nil {
		return err
	}
	return s.Wait()
}

// Start is responsible for kicking off the http server
func (s *Server) Start(host, port string) error {
	return s.StartContext(context.Background(), host, port)
}

// StartContext is responsible for kicking off the http server,
// and can be closed by cancelling the provided context
func (s *Server) StartContext(ctx context.Context, host, port string) error {
	s.Infof("Fingerprint %s", s.fingerprint)
	if s.users.Len() > 0 {
		s.Infof("User authenication enabled")
	}
	if s.reverseProxy != nil {
		s.Infof("Reverse proxy enabled")
	}
	l, err := s.listener(host, port)
	if err != nil {
		return err
	}
	h := http.Handler(http.HandlerFunc(s.handleClientHandler))
	if s.Debug {
		o := requestlog.DefaultOptions
		o.TrustProxy = true
		h = requestlog.WrapWith(h, o)
	}

	s.setupRedisClient(ctx)

	return s.httpServer.GoServe(ctx, l, h)
}

// Wait waits for the http server to close
func (s *Server) Wait() error {
	return s.httpServer.Wait()
}

// Close forcibly closes the http server
func (s *Server) Close() error {
	return s.httpServer.Close()
}

// GetFingerprint is used to access the server fingerprint
func (s *Server) GetFingerprint() string {
	return s.fingerprint
}

// authUser is responsible for validating the ssh user / password combination
func (s *Server) authUser(c ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
	s.refreshUsersPfconfig()
	// check the user exists and has matching password
	n := c.User()
	user, found := s.users.Get(n)
	if !found || user.Pass != string(password) {
		s.Debugf("Login failed for user: %s", n)
		return nil, errors.New("Invalid authentication for username: %s")
	}
	// insert the user session map
	// TODO this should probably have a lock on it given the map isn't thread-safe
	s.sessions.Set(string(c.SessionID()), user)
	return nil, nil
}

// AddUser adds a new user into the server user index
func (s *Server) AddUser(user, pass string, addrs ...string) error {
	authorizedAddrs := []*regexp.Regexp{}
	for _, addr := range addrs {
		authorizedAddr, err := regexp.Compile(addr)
		if err != nil {
			return err
		}
		authorizedAddrs = append(authorizedAddrs, authorizedAddr)
	}
	s.users.AddUser(&settings.User{
		Name:  user,
		Pass:  pass,
		Addrs: authorizedAddrs,
	})
	return nil
}

var acceptAllAddrs = regexp.MustCompile("")
var connectors = pfconfigdriver.Connectors{}

func (s *Server) refreshUsersPfconfig() {
	ctx := context.Background()
	s.connectors.Refresh(ctx)
	for connectorId, connector := range s.connectors.All(ctx) {
		s.users.AddUser(&settings.User{
			Name:  connectorId,
			Pass:  connector.Secret,
			Addrs: []*regexp.Regexp{acceptAllAddrs},
		})
	}
}

// DeleteUser removes a user from the server user index
func (s *Server) DeleteUser(user string) {
	s.users.Del(user)
}

// ResetUsers in the server user index.
// Use nil to remove all.
func (s *Server) ResetUsers(users []*settings.User) {
	s.users.Reset(users)
}

func (s *Server) setupRedisClient(ctx context.Context) {
	pfconfigdriver.PfconfigPool.AddStruct(ctx, &pfconfigdriver.Config.PfConf.Pfconnector)
	var network string
	if pfconfigdriver.Config.PfConf.Pfconnector.RedisServer[0] == '/' {
		network = "unix"
	} else {
		network = "tcp"
	}

	s.redis = redis.NewClient(&redis.Options{
		Addr:    pfconfigdriver.Config.PfConf.Pfconnector.RedisServer,
		Network: network,
	})

	s.redisTunnelsNamespace = pfconfigdriver.Config.PfConf.Pfconnector.RedisTunnelsNamespace

}

func (s *Server) computeConnectorIndex(connectorId string) int {
	s.connectorIndexesLock.Lock()
	defer s.connectorIndexesLock.Unlock()
	if index, ok := s.connectorsIndexes[connectorId]; ok {
		return index
	} else {
		highestId := 0
		for _, id := range s.connectorsIndexes {
			if id >= highestId {
				highestId = id
			} else if id == highestId {
				panic("There are duplicate s.connectorsIndexes values. This shouldn't have happened")
			}
		}
		s.connectorsIndexes[connectorId] = highestId + 1
		return s.connectorsIndexes[connectorId]
	}

}
