package backend

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"

	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
	cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
	rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
	types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
	"github.com/cs3org/reva/v2/pkg/auth/scope"
	revactx "github.com/cs3org/reva/v2/pkg/ctx"
	"github.com/cs3org/reva/v2/pkg/token"
	libregraph "github.com/owncloud/libre-graph-api-go"
	"github.com/owncloud/ocis/v2/ocis-pkg/log"
	"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
	"github.com/owncloud/ocis/v2/ocis-pkg/registry"
	settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
	"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
	settingsService "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0"
	merrors "go-micro.dev/v4/errors"
	"go-micro.dev/v4/selector"
)

type cs3backend struct {
	graphSelector       selector.Selector
	settingsRoleService settingssvc.RoleService
	authProvider        RevaAuthenticator
	oidcISS             string
	machineAuthAPIKey   string
	tokenManager        token.Manager
	logger              log.Logger
}

// NewCS3UserBackend creates a user-provider which fetches users from a CS3 UserBackend
func NewCS3UserBackend(rs settingssvc.RoleService, ap RevaAuthenticator, machineAuthAPIKey string, oidcISS string, tokenManager token.Manager, logger log.Logger) UserBackend {
	reg := registry.GetRegistry()
	sel := selector.NewSelector(selector.Registry(reg))
	return &cs3backend{
		graphSelector:       sel,
		settingsRoleService: rs,
		authProvider:        ap,
		oidcISS:             oidcISS,
		machineAuthAPIKey:   machineAuthAPIKey,
		tokenManager:        tokenManager,
		logger:              logger,
	}
}

func (c *cs3backend) GetUserByClaims(ctx context.Context, claim, value string, withRoles bool) (*cs3.User, string, error) {
	res, err := c.authProvider.Authenticate(ctx, &gateway.AuthenticateRequest{
		Type:         "machine",
		ClientId:     claim + ":" + value,
		ClientSecret: c.machineAuthAPIKey,
	})

	switch {
	case err != nil:
		return nil, "", fmt.Errorf("could not get user by claim %v with value %v: %w", claim, value, err)
	case res.Status.Code != rpcv1beta1.Code_CODE_OK:
		if res.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND {
			return nil, "", ErrAccountNotFound
		}
		return nil, "", fmt.Errorf("could not get user by claim %v with value %v : %s ", claim, value, res.Status.Message)
	}

	user := res.User

	if !withRoles {
		return user, res.Token, nil
	}

	var roleIDs []string
	if user.Id.Type != cs3.UserType_USER_TYPE_LIGHTWEIGHT {
		roleIDs, err = loadRolesIDs(ctx, user.Id.OpaqueId, c.settingsRoleService)
		if err != nil {
			var merr *merrors.Error
			if errors.As(err, &merr) && merr.Code == http.StatusNotFound {
				// This user doesn't have a role assignment yet. Assign a
				// default user role. At least until proper roles are provided. See
				// https://github.com/owncloud/ocis/v2/issues/1825 for more context.
				if user.Id.Type == cs3.UserType_USER_TYPE_PRIMARY {
					c.logger.Info().Str("userid", user.Id.OpaqueId).Msg("user has no role assigned, assigning default user role")
					_, err := c.settingsRoleService.AssignRoleToUser(ctx, &settingssvc.AssignRoleToUserRequest{
						AccountUuid: user.Id.OpaqueId,
						RoleId:      settingsService.BundleUUIDRoleUser,
					})
					if err != nil {
						c.logger.Error().Err(err).Msg("Could not add default role")
						return nil, "", err
					}
					roleIDs = append(roleIDs, settingsService.BundleUUIDRoleUser)
				}
			} else {
				c.logger.Error().Err(err).Msgf("Could not load roles")
				return nil, "", err
			}
		}
	}

	enc, err := encodeRoleIDs(roleIDs)
	if err != nil {
		c.logger.Error().Err(err).Msg("Could not encode loaded roles")
	}

	if user.Opaque == nil {
		user.Opaque = &types.Opaque{
			Map: map[string]*types.OpaqueEntry{
				"roles": enc,
			},
		}
	} else {
		user.Opaque.Map["roles"] = enc
	}

	return user, res.Token, nil
}

func (c *cs3backend) Authenticate(ctx context.Context, username string, password string) (*cs3.User, string, error) {
	res, err := c.authProvider.Authenticate(ctx, &gateway.AuthenticateRequest{
		Type:         "basic",
		ClientId:     username,
		ClientSecret: password,
	})

	switch {
	case err != nil:
		return nil, "", fmt.Errorf("could not authenticate with username and password user: %s, %w", username, err)
	case res.Status.Code != rpcv1beta1.Code_CODE_OK:
		return nil, "", fmt.Errorf("could not authenticate with username and password user: %s, got code: %d", username, res.Status.Code)
	}

	return res.User, res.Token, nil
}

// CreateUserFromClaims creates a new user via libregraph users API, taking the
// attributes from the provided `claims` map. On success it returns the new
// user. If the user already exist this is not considered an error and the
// function will just return the existing user.
func (c *cs3backend) CreateUserFromClaims(ctx context.Context, claims map[string]interface{}) (*cs3.User, error) {
	newctx := context.Background()
	token, err := c.generateAutoProvisionAdminToken(newctx)
	if err != nil {
		c.logger.Error().Err(err).Msg("Error generating token for autoprovisioning user.")
		return nil, err
	}
	lgClient, err := c.setupLibregraphClient(ctx, token)
	if err != nil {
		c.logger.Error().Err(err).Msg("Error setting up libregraph client.")
		return nil, err
	}

	newUser, err := c.libregraphUserFromClaims(newctx, claims)
	if err != nil {
		c.logger.Error().Err(err).Interface("claims", claims).Msg("Error creating user from claims")
		return nil, fmt.Errorf("Error creating user from claims: %w", err)
	}

	req := lgClient.UsersApi.CreateUser(newctx).User(newUser)

	created, resp, err := req.Execute()
	var reread bool
	if err != nil {
		if resp == nil {
			return nil, err
		}

		// If the user already exists here, some other request did already create it in parallel.
		// So just issue a Debug message and ignore the libregraph error otherwise
		var lerr error
		if reread, lerr = c.isAlreadyExists(resp); lerr != nil {
			c.logger.Error().Err(lerr).Msg("extracting error from ibregraph response body failed.")
			return nil, err
		}
		if !reread {
			c.logger.Error().Err(err).Msg("Error creating user")
			return nil, err
		}
	}

	// User has been created meanwhile, re-read it to get the user id
	if reread {
		c.logger.Debug().Msg("User already exist, re-reading via libregraph")
		gureq := lgClient.UserApi.GetUser(newctx, newUser.GetOnPremisesSamAccountName())
		created, resp, err = gureq.Execute()
		if err != nil {
			c.logger.Error().Err(err).Msg("Error trying to re-read user from graphAPI")
			return nil, err
		}
	}

	cs3UserCreated := c.cs3UserFromLibregraph(newctx, created)

	return &cs3UserCreated, nil
}

func (c cs3backend) GetUserGroups(ctx context.Context, userID string) {
	panic("implement me")
}

func (c cs3backend) setupLibregraphClient(ctx context.Context, cs3token string) (*libregraph.APIClient, error) {
	// Use micro registry to resolve next graph service endpoint
	next, err := c.graphSelector.Select("com.owncloud.graph.graph")
	if err != nil {
		c.logger.Debug().Err(err).Msg("setupLibregraphClient: error during Select")
		return nil, err
	}
	node, err := next()
	if err != nil {
		c.logger.Debug().Err(err).Msg("setupLibregraphClient: error getting next Node")
		return nil, err
	}
	lgconf := libregraph.NewConfiguration()
	lgconf.Servers = libregraph.ServerConfigurations{
		{
			URL: fmt.Sprintf("%s://%s/graph/v1.0", node.Metadata["protocol"], node.Address),
		},
	}

	lgconf.DefaultHeader = map[string]string{revactx.TokenHeader: cs3token}
	return libregraph.NewAPIClient(lgconf), nil
}

func (c cs3backend) isAlreadyExists(resp *http.Response) (bool, error) {
	oDataErr := libregraph.NewOdataErrorWithDefaults()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		c.logger.Debug().Err(err).Msg("Error trying to read libregraph response")
		return false, err
	}
	err = json.Unmarshal(body, oDataErr)
	if err != nil {
		c.logger.Debug().Err(err).Msg("Error unmarshalling libregraph response")
		return false, err
	}

	if oDataErr.Error.Code == errorcode.NameAlreadyExists.String() {
		return true, nil
	}
	return false, nil
}

func (c cs3backend) libregraphUserFromClaims(ctx context.Context, claims map[string]interface{}) (libregraph.User, error) {
	var ok bool
	var dn, mail, username string
	user := libregraph.User{}
	if dn, ok = claims[oidc.Name].(string); !ok {
		return user, fmt.Errorf("Missing claim '%s'", oidc.Name)
	}
	if mail, ok = claims[oidc.Email].(string); !ok {
		return user, fmt.Errorf("Missing claim '%s'", oidc.Email)
	}
	if username, ok = claims[oidc.PreferredUsername].(string); !ok {
		c.logger.Warn().Str("claim", oidc.PreferredUsername).Msg("Missing claim for username, falling back to email address")
		username = mail
	}
	user.DisplayName = &dn
	user.OnPremisesSamAccountName = &username
	user.Mail = &mail
	return user, nil
}

func (c cs3backend) cs3UserFromLibregraph(ctx context.Context, lu *libregraph.User) cs3.User {
	cs3id := cs3.UserId{
		Type: cs3.UserType_USER_TYPE_PRIMARY,
		Idp:  c.oidcISS,
	}

	cs3id.OpaqueId = lu.GetId()

	cs3user := cs3.User{
		Id: &cs3id,
	}
	cs3user.Username = lu.GetOnPremisesSamAccountName()
	cs3user.DisplayName = lu.GetDisplayName()
	cs3user.Mail = lu.GetMail()
	return cs3user
}

// This returns an hardcoded internal User, that is privileged to create new User via
// the Graph API. This user is needed for autoprovisioning of users from incoming OIDC
// claims.
func getAutoProvisionUserCreator() (*cs3.User, error) {
	encRoleID, err := encodeRoleIDs([]string{settingsService.BundleUUIDRoleAdmin})
	if err != nil {
		return nil, err
	}

	autoProvisionUserCreator := &cs3.User{
		DisplayName: "Autoprovision User",
		Username:    "autoprovisioner",
		Id: &cs3.UserId{
			Idp:      "internal",
			OpaqueId: "autoprov-user-id00-0000-000000000000",
		},
		Opaque: &types.Opaque{
			Map: map[string]*types.OpaqueEntry{
				"roles": encRoleID,
			},
		},
	}
	return autoProvisionUserCreator, nil
}

func (c cs3backend) generateAutoProvisionAdminToken(ctx context.Context) (string, error) {
	userCreator, err := getAutoProvisionUserCreator()
	if err != nil {
		return "", err
	}

	s, err := scope.AddOwnerScope(nil)
	if err != nil {
		c.logger.Error().Err(err).Msg("could not get owner scope")
		return "", err
	}

	token, err := c.tokenManager.MintToken(ctx, userCreator, s)
	if err != nil {
		c.logger.Error().Err(err).Msg("could not mint token")
		return "", err
	}
	return token, nil
}
