package main

import (
	"context"
	"fmt"
	"net/http"

	"isc.org/stork-hook-ldap/internal"
	"isc.org/stork/hooks/server/authenticationcallouts"
)

// Callout carrier structure.
type calloutCarrier struct {
	settings internal.Settings
}

// GetMetadata implements authenticationcallouts.AuthenticationCallouts.
func (*calloutCarrier) GetMetadata() authenticationcallouts.AuthenticationMetadata {
	return &internal.Metadata{}
}

// Authenticate implements authenticationcallouts.AuthenticationCallouts.
func (c *calloutCarrier) Authenticate(ctx context.Context, request *http.Request, userName *string, password *string) (*authenticationcallouts.User, error) {
	// Validate the input data.
	if userName == nil {
		return nil, fmt.Errorf("you must provide the username: %w", errValidation)
	}
	if password == nil {
		return nil, fmt.Errorf("you must provide the password: %w", errValidation)
	}

	// Run the authentication flow.
	controller := internal.NewLDAPController(c.settings, internal.NewLDAPLibraryDriver(
		c.settings.Debug,
	))
	// Establish connection to the LDAP server.
	if err := controller.Connect(); err != nil {
		return nil, err
	}
	defer controller.Close()

	// Authenticate as a bind user (usually readonly).
	if err := controller.BindAsMaintenanceUser(); err != nil {
		return nil, err
	}

	// Fetch the user DN (distinguished name).
	userDN, err := controller.SearchForUserDN(*userName)
	if err != nil {
		return nil, err
	}

	// Verify a user password. Just try to bind to the user.
	if err = controller.BindAsUser(userDN, *password); err != nil {
		return nil, err
	}

	// Re-bind to the bind user.
	if err = controller.BindAsMaintenanceUser(); err != nil {
		return nil, err
	}

	// Fetch the user profile from LDAP.
	userProfile, err := controller.SearchForUserProfile(*userName)
	if err != nil {
		return nil, err
	}

	// Returns if fetching the group membership was not requested.
	if !c.settings.EnableGroupMapping && c.settings.MandatoryAllowGroup == "" {
		return userProfile, nil
	}

	groups, hasAllowGroup, err := controller.SearchForUserGroupMembership(userDN)
	if err != nil {
		return nil, err
	}
	userProfile.Groups = groups

	// Verify allow group.
	if c.settings.MandatoryAllowGroup != "" && !hasAllowGroup {
		return nil, fmt.Errorf("user (%s) has no mandatory group (%s): %w",
			userDN, c.settings.MandatoryAllowGroup, errUnauthorized)
	}

	return userProfile, nil
}

// Unauthenticate implements authenticationcallouts.AuthenticationCallouts.
func (*calloutCarrier) Unauthenticate(ctx context.Context) error {
	// Not applicable.
	return nil
}

// Closer interface implementation.
func (c *calloutCarrier) Close() error {
	// No resources to free.
	return nil
}

// Interface checks.
var _ authenticationcallouts.AuthenticationCallouts = (*calloutCarrier)(nil)
