package idp

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

	"github.com/okta/okta-sdk-golang/v2/okta"
	"github.com/okta/okta-sdk-golang/v2/okta/query"

	"github.com/netbirdio/netbird/management/server/telemetry"
)

// OktaManager okta manager client instance.
type OktaManager struct {
	client      *okta.Client
	httpClient  ManagerHTTPClient
	credentials ManagerCredentials
	helper      ManagerHelper
	appMetrics  telemetry.AppMetrics
}

// OktaClientConfig okta manager client configurations.
type OktaClientConfig struct {
	APIToken      string
	Issuer        string
	TokenEndpoint string
	GrantType     string
}

// OktaCredentials okta authentication information.
type OktaCredentials struct {
	clientConfig OktaClientConfig
	helper       ManagerHelper
	httpClient   ManagerHTTPClient
	appMetrics   telemetry.AppMetrics
}

// NewOktaManager creates a new instance of the OktaManager.
func NewOktaManager(config OktaClientConfig, appMetrics telemetry.AppMetrics) (*OktaManager, error) {
	httpTransport := http.DefaultTransport.(*http.Transport).Clone()
	httpTransport.MaxIdleConns = 5

	httpClient := &http.Client{
		Timeout:   idpTimeout(),
		Transport: httpTransport,
	}

	helper := JsonParser{}
	config.Issuer = baseURL(config.Issuer)

	if config.APIToken == "" {
		return nil, fmt.Errorf("okta IdP configuration is incomplete, APIToken is missing")
	}

	if config.Issuer == "" {
		return nil, fmt.Errorf("okta IdP configuration is incomplete, Issuer is missing")
	}

	if config.TokenEndpoint == "" {
		return nil, fmt.Errorf("okta IdP configuration is incomplete, TokenEndpoint is missing")
	}

	if config.GrantType == "" {
		return nil, fmt.Errorf("okta IdP configuration is incomplete, GrantType is missing")
	}

	_, client, err := okta.NewClient(context.Background(),
		okta.WithOrgUrl(config.Issuer),
		okta.WithToken(config.APIToken),
		okta.WithHttpClientPtr(httpClient),
	)
	if err != nil {
		return nil, err
	}

	credentials := &OktaCredentials{
		clientConfig: config,
		httpClient:   httpClient,
		helper:       helper,
		appMetrics:   appMetrics,
	}

	return &OktaManager{
		client:      client,
		httpClient:  httpClient,
		credentials: credentials,
		helper:      helper,
		appMetrics:  appMetrics,
	}, nil
}

// Authenticate retrieves access token to use the okta user API.
func (oc *OktaCredentials) Authenticate(_ context.Context) (JWTToken, error) {
	return JWTToken{}, nil
}

// CreateUser creates a new user in okta Idp and sends an invitation.
func (om *OktaManager) CreateUser(_ context.Context, _, _, _, _ string) (*UserData, error) {
	return nil, fmt.Errorf("method CreateUser not implemented")
}

// GetUserDataByID requests user data from keycloak via ID.
func (om *OktaManager) GetUserDataByID(_ context.Context, userID string, appMetadata AppMetadata) (*UserData, error) {
	user, resp, err := om.client.User.GetUser(context.Background(), userID)
	if err != nil {
		return nil, err
	}

	if om.appMetrics != nil {
		om.appMetrics.IDPMetrics().CountGetUserDataByID()
	}

	if resp.StatusCode != http.StatusOK {
		if om.appMetrics != nil {
			om.appMetrics.IDPMetrics().CountRequestStatusError()
		}
		return nil, fmt.Errorf("unable to get user %s, statusCode %d", userID, resp.StatusCode)
	}

	userData, err := parseOktaUser(user)
	if err != nil {
		return nil, err
	}
	userData.AppMetadata = appMetadata

	return userData, nil
}

// GetUserByEmail searches users with a given email.
// If no users have been found, this function returns an empty list.
func (om *OktaManager) GetUserByEmail(_ context.Context, email string) ([]*UserData, error) {
	user, resp, err := om.client.User.GetUser(context.Background(), url.QueryEscape(email))
	if err != nil {
		return nil, err
	}

	if om.appMetrics != nil {
		om.appMetrics.IDPMetrics().CountGetUserByEmail()
	}

	if resp.StatusCode != http.StatusOK {
		if om.appMetrics != nil {
			om.appMetrics.IDPMetrics().CountRequestStatusError()
		}
		return nil, fmt.Errorf("unable to get user %s, statusCode %d", email, resp.StatusCode)
	}

	userData, err := parseOktaUser(user)
	if err != nil {
		return nil, err
	}
	users := make([]*UserData, 0)
	users = append(users, userData)

	return users, nil
}

// GetAccount returns all the users for a given profile.
func (om *OktaManager) GetAccount(_ context.Context, accountID string) ([]*UserData, error) {
	users, err := om.getAllUsers()
	if err != nil {
		return nil, err
	}

	if om.appMetrics != nil {
		om.appMetrics.IDPMetrics().CountGetAccount()
	}

	for index, user := range users {
		user.AppMetadata.WTAccountID = accountID
		users[index] = user
	}

	return users, nil
}

// GetAllAccounts gets all registered accounts with corresponding user data.
// It returns a list of users indexed by accountID.
func (om *OktaManager) GetAllAccounts(_ context.Context) (map[string][]*UserData, error) {
	users, err := om.getAllUsers()
	if err != nil {
		return nil, err
	}

	indexedUsers := make(map[string][]*UserData)
	indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], users...)

	if om.appMetrics != nil {
		om.appMetrics.IDPMetrics().CountGetAllAccounts()
	}

	return indexedUsers, nil
}

// getAllUsers returns all users in an Okta account.
func (om *OktaManager) getAllUsers() ([]*UserData, error) {
	qp := query.NewQueryParams(query.WithLimit(200))
	userList, resp, err := om.client.User.ListUsers(context.Background(), qp)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != http.StatusOK {
		if om.appMetrics != nil {
			om.appMetrics.IDPMetrics().CountRequestStatusError()
		}
		return nil, fmt.Errorf("unable to get all accounts, statusCode %d", resp.StatusCode)
	}

	for resp.HasNextPage() {
		paginatedUsers := make([]*okta.User, 0)
		resp, err = resp.Next(context.Background(), &paginatedUsers)
		if err != nil {
			return nil, err
		}

		if resp.StatusCode != http.StatusOK {
			if om.appMetrics != nil {
				om.appMetrics.IDPMetrics().CountRequestStatusError()
			}
			return nil, fmt.Errorf("unable to get all accounts, statusCode %d", resp.StatusCode)
		}

		userList = append(userList, paginatedUsers...)
	}

	users := make([]*UserData, 0, len(userList))
	for _, user := range userList {
		userData, err := parseOktaUser(user)
		if err != nil {
			return nil, err
		}

		users = append(users, userData)
	}

	return users, nil
}

// UpdateUserAppMetadata updates user app metadata based on userID and metadata map.
func (om *OktaManager) UpdateUserAppMetadata(_ context.Context, _ string, _ AppMetadata) error {
	return nil
}

// InviteUserByID resend invitations to users who haven't activated,
// their accounts prior to the expiration period.
func (om *OktaManager) InviteUserByID(_ context.Context, _ string) error {
	return fmt.Errorf("method InviteUserByID not implemented")
}

// DeleteUser from Okta
func (om *OktaManager) DeleteUser(_ context.Context, userID string) error {
	resp, err := om.client.User.DeactivateOrDeleteUser(context.Background(), userID, nil)
	if err != nil {
		return err
	}

	if om.appMetrics != nil {
		om.appMetrics.IDPMetrics().CountDeleteUser()
	}

	if resp.StatusCode != http.StatusOK {
		if om.appMetrics != nil {
			om.appMetrics.IDPMetrics().CountRequestStatusError()
		}
		return fmt.Errorf("unable to delete user, statusCode %d", resp.StatusCode)
	}

	return nil
}

// parseOktaUser parse okta user to UserData.
func parseOktaUser(user *okta.User) (*UserData, error) {
	var oktaUser struct {
		Email     string `json:"email"`
		FirstName string `json:"firstName"`
		LastName  string `json:"lastName"`
	}

	if user == nil {
		return nil, fmt.Errorf("invalid okta user")
	}

	if user.Profile != nil {
		helper := JsonParser{}
		buf, err := helper.Marshal(*user.Profile)
		if err != nil {
			return nil, err
		}

		err = helper.Unmarshal(buf, &oktaUser)
		if err != nil {
			return nil, err
		}
	}

	return &UserData{
		Email: oktaUser.Email,
		Name:  strings.Join([]string{oktaUser.FirstName, oktaUser.LastName}, " "),
		ID:    user.Id,
	}, nil
}
