/*
 * SPDX-License-Identifier: Apache-2.0
 * Copyright 2021 The LibreGraph Authors.
 */

package server

import (
	"context"
	"fmt"
	"log"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"

	"github.com/bombsimon/logrusr/v3"
	"github.com/go-ldap/ldap/v3"
	"github.com/sirupsen/logrus"

	"github.com/libregraph/idm/pkg/ldapserver"
	"github.com/libregraph/idm/server/handler"
	"github.com/libregraph/idm/server/handler/boltdb"
	"github.com/libregraph/idm/server/handler/ldif"
)

const DefaultGeneratedPasswordLength = 16

// Server is our server implementation.
type Server struct {
	config *Config

	logger logrus.FieldLogger

	LDAPServer  *ldapserver.Server
	LDAPHandler handler.Handler
}

// NewServer constructs a server from the provided parameters.
func NewServer(c *Config) (*Server, error) {
	s := &Server{
		config: c,

		logger: c.Logger,
	}

	s.LDAPServer = ldapserver.NewServer()
	s.LDAPServer.EnforceLDAP = false
	s.LDAPServer.GeneratedPasswordLength = DefaultGeneratedPasswordLength
	ldapserver.Logger(logrusr.New(c.Logger))

	var err error
	switch c.LDAPHandler {
	case "ldif":
		ldifHandlerOptions := &ldif.Options{
			BaseDN:                  s.config.LDAPBaseDN,
			AdminDN:                 s.config.LDAPAdminDN,
			AllowLocalAnonymousBind: s.config.LDAPAllowLocalAnonymousBind,

			DefaultCompany:    s.config.LDIFDefaultCompany,
			DefaultMailDomain: s.config.LDIFDefaultMailDomain,
			TemplateExtraVars: s.config.LDIFTemplateExtraVars,

			TemplateDebug: os.Getenv("KIDM_TEMPLATE_DEBUG") != "",
		}

		s.LDAPHandler, err = ldif.NewLDIFHandler(s.logger, s.config.LDIFMain, ldifHandlerOptions)
		if err != nil {
			return nil, fmt.Errorf("failed to create LDIF source handler: %w", err)
		}
		if s.config.LDIFConfig != "" {
			middleware, middlewareErr := ldif.NewLDIFMiddleware(s.logger, s.config.LDIFConfig, ldifHandlerOptions)
			if middlewareErr != nil {
				return nil, fmt.Errorf("failed to create LDIF config handler: %w", middlewareErr)
			}
			s.LDAPHandler = middleware.WithHandler(s.LDAPHandler)
		}
	case "boltdb":
		boltOptions := &boltdb.Options{
			BaseDN:  s.config.LDAPBaseDN,
			AdminDN: s.config.LDAPAdminDN,

			AllowLocalAnonymousBind: s.config.LDAPAllowLocalAnonymousBind,
		}
		s.LDAPHandler, err = boltdb.NewBoltDBHandler(s.logger, s.config.BoltDBFile, boltOptions)
		if err != nil {
			return nil, fmt.Errorf("failed to create BoltDB handler: %w", err)
		}

		// FIXME Let the frontend (LDAPServer) handle filtering and attribute list until we added backend support
		s.LDAPServer.EnforceLDAP = true
	default:
		return nil, fmt.Errorf("unknown LDAPHandler: '%s'", c.LDAPHandler)
	}

	if c.Metrics != nil {
		s.LDAPServer.SetStats(true)
		MustRegister(c.Metrics, NewLDAPServerCollector(s.LDAPServer))
	}

	return s, nil
}

// Serve starts all the accociated servers resources and listeners and blocks
// forever until signals or error occurs.
func (s *Server) Serve(ctx context.Context) error {
	var err error

	serveCtx, serveCtxCancel := context.WithCancel(ctx)
	defer serveCtxCancel()

	logger := s.logger

	errCh := make(chan error, 2)
	exitCh := make(chan struct{}, 1)
	signalCh := make(chan os.Signal, 1)
	readyCh := make(chan struct{}, 1)
	triggerCh := make(chan bool, 1)

	go func() {
		select {
		case <-serveCtx.Done():
			return
		case <-readyCh:
		}
		logger.WithFields(logrus.Fields{}).Infoln("ready")
	}()

	var serversWg sync.WaitGroup

	// NOTE(rhafer): since v3.4.3 the ldap package allows to set a custom logger.
	// Set that to use to our logger.
	loggerWriter := logger.WithField("scope", "ldap").WriterLevel(logrus.DebugLevel)
	defer loggerWriter.Close()
	ldap.Logger(log.New(loggerWriter, "", 0))

	ldapHandler := s.LDAPHandler.WithContext(serveCtx)
	s.LDAPServer.AddFunc("", ldapHandler)
	s.LDAPServer.BindFunc("", ldapHandler)
	s.LDAPServer.DeleteFunc("", ldapHandler)
	s.LDAPServer.ModifyFunc("", ldapHandler)
	s.LDAPServer.ModifyDNFunc("", ldapHandler)
	s.LDAPServer.PasswordExOpFunc("", ldapHandler)
	s.LDAPServer.SearchFunc("", ldapHandler)
	s.LDAPServer.CloseFunc("", ldapHandler)

	serversWg.Add(1)
	go func() {
		defer serversWg.Done()
		for {
			select {
			case <-triggerCh:
				reloadErr := ldapHandler.Reload(serveCtx)
				if reloadErr != nil {
					logger.Debugln("reload error: %w", reloadErr)
				} else {
					logger.Debugln("reload complete")
				}
			case <-serveCtx.Done():
				return
			}
		}
	}()

	if s.config.LDAPListenAddr != "" {
		serversWg.Add(1)
		go func() {
			defer serversWg.Done()
			logger.WithField("listen_addr", s.config.LDAPListenAddr).Infoln("starting LDAP listener")
			serveErr := s.LDAPServer.ListenAndServe(s.config.LDAPListenAddr)
			if serveErr != nil {
				errCh <- serveErr
			}
		}()
	}
	if s.config.LDAPSListenAddr != "" {
		serversWg.Add(1)
		go func() {
			defer serversWg.Done()
			logger.WithField("listen_addr_tls", s.config.LDAPSListenAddr).Infoln("starting LDAPS listener")
			serveErr := s.LDAPServer.ListenAndServeTLS(s.config.LDAPSListenAddr, s.config.TLSCertFile, s.config.TLSKeyFile)
			if serveErr != nil {
				errCh <- serveErr
			}
		}()
	}

	go func() {
		serversWg.Wait()
		logger.Debugln("server listeners stopped")
		close(exitCh)
	}()

	go func() {
		close(readyCh) // TODO(longsleep): Implement real ready.
		if s.config.OnReady != nil {
			go s.config.OnReady(s)
		}
	}()

	// Wait for exit or error, with support for HUP to reload
	err = func() error {
		signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
		for {
			select {
			case errFromChannel := <-errCh:
				return errFromChannel
			case reason := <-signalCh:
				if reason == syscall.SIGHUP {
					logger.Infoln("reload signal received")
					select {
					case triggerCh <- true:
					default:
					}
					continue
				}
				logger.WithField("signal", reason).Warnln("received signal")
				return nil
			}
		}
	}()

	// Shutdown, server will stop to accept new connections, requires Go 1.8+.
	logger.Infoln("clean server shutdown start")
	_, shutdownCtxCancel := context.WithTimeout(ctx, 10*time.Second)
	go func() {
		close(s.LDAPServer.Quit)
	}()

	// Cancel our own context,
	serveCtxCancel()
	func() {
		for {
			select {
			case <-exitCh:
				logger.Infoln("clean server shutdown complete, exiting")
				return
			default:
				// Services have not quit yet.
				logger.Info("waiting for services to exit")
			}
			select {
			case reason := <-signalCh:
				logger.WithField("signal", reason).Warn("received signal")
				return
			case <-time.After(100 * time.Millisecond):
			}
		}
	}()
	shutdownCtxCancel() // Prevents leak.

	return err
}
