package idp

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

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

const (
	// UnsetAccountID is a special key to map users without an account ID
	UnsetAccountID = "unset"
)

// Manager idp manager interface
type Manager interface {
	UpdateUserAppMetadata(ctx context.Context, userId string, appMetadata AppMetadata) error
	GetUserDataByID(ctx context.Context, userId string, appMetadata AppMetadata) (*UserData, error)
	GetAccount(ctx context.Context, accountId string) ([]*UserData, error)
	GetAllAccounts(ctx context.Context) (map[string][]*UserData, error)
	CreateUser(ctx context.Context, email, name, accountID, invitedByEmail string) (*UserData, error)
	GetUserByEmail(ctx context.Context, email string) ([]*UserData, error)
	InviteUserByID(ctx context.Context, userID string) error
	DeleteUser(ctx context.Context, userID string) error
}

// ClientConfig defines common client configuration for all IdP manager
type ClientConfig struct {
	Issuer        string
	TokenEndpoint string
	ClientID      string
	ClientSecret  string
	GrantType     string
}

// ExtraConfig stores IdP specific config that are unique to individual IdPs
type ExtraConfig map[string]string

// Config an idp configuration struct to be loaded from management server's config file
type Config struct {
	ManagerType               string
	ClientConfig              *ClientConfig
	ExtraConfig               ExtraConfig
	Auth0ClientCredentials    *Auth0ClientConfig
	AzureClientCredentials    *AzureClientConfig
	KeycloakClientCredentials *KeycloakClientConfig
	ZitadelClientCredentials  *ZitadelClientConfig
}

// ManagerCredentials interface that authenticates using the credential of each type of idp
type ManagerCredentials interface {
	Authenticate(ctx context.Context) (JWTToken, error)
}

// ManagerHTTPClient http client interface for API calls
type ManagerHTTPClient interface {
	Do(req *http.Request) (*http.Response, error)
}

// ManagerHelper helper
type ManagerHelper interface {
	Marshal(v interface{}) ([]byte, error)
	Unmarshal(data []byte, v interface{}) error
}

type UserData struct {
	Email       string      `json:"email"`
	Name        string      `json:"name"`
	ID          string      `json:"user_id"`
	AppMetadata AppMetadata `json:"app_metadata"`
}

// AppMetadata user app metadata to associate with a profile
type AppMetadata struct {
	// WTAccountID is a NetBird (previously Wiretrustee) account id to update in the IDP
	// maps to wt_account_id when json.marshal
	WTAccountID     string `json:"wt_account_id,omitempty"`
	WTPendingInvite *bool  `json:"wt_pending_invite,omitempty"`
	WTInvitedBy     string `json:"wt_invited_by_email,omitempty"`
}

// JWTToken a JWT object that holds information of a token
type JWTToken struct {
	AccessToken   string `json:"access_token"`
	ExpiresIn     int    `json:"expires_in"`
	expiresInTime time.Time
	Scope         string `json:"scope"`
	TokenType     string `json:"token_type"`
}

// NewManager returns a new idp manager based on the configuration that it receives
func NewManager(ctx context.Context, config Config, appMetrics telemetry.AppMetrics) (Manager, error) {
	if config.ClientConfig != nil {
		config.ClientConfig.Issuer = strings.TrimSuffix(config.ClientConfig.Issuer, "/")
	}

	switch strings.ToLower(config.ManagerType) {
	case "none", "":
		return nil, nil //nolint:nilnil
	case "auth0":
		auth0ClientConfig := config.Auth0ClientCredentials
		if config.ClientConfig != nil {
			auth0ClientConfig = &Auth0ClientConfig{
				Audience:     config.ExtraConfig["Audience"],
				AuthIssuer:   config.ClientConfig.Issuer,
				ClientID:     config.ClientConfig.ClientID,
				ClientSecret: config.ClientConfig.ClientSecret,
				GrantType:    config.ClientConfig.GrantType,
			}
		}

		return NewAuth0Manager(*auth0ClientConfig, appMetrics)
	case "azure":
		azureClientConfig := config.AzureClientCredentials
		if config.ClientConfig != nil {
			azureClientConfig = &AzureClientConfig{
				ClientID:         config.ClientConfig.ClientID,
				ClientSecret:     config.ClientConfig.ClientSecret,
				GrantType:        config.ClientConfig.GrantType,
				TokenEndpoint:    config.ClientConfig.TokenEndpoint,
				ObjectID:         config.ExtraConfig["ObjectId"],
				GraphAPIEndpoint: config.ExtraConfig["GraphApiEndpoint"],
			}
		}

		return NewAzureManager(*azureClientConfig, appMetrics)
	case "keycloak":
		keycloakClientConfig := config.KeycloakClientCredentials
		if config.ClientConfig != nil {
			keycloakClientConfig = &KeycloakClientConfig{
				ClientID:      config.ClientConfig.ClientID,
				ClientSecret:  config.ClientConfig.ClientSecret,
				GrantType:     config.ClientConfig.GrantType,
				TokenEndpoint: config.ClientConfig.TokenEndpoint,
				AdminEndpoint: config.ExtraConfig["AdminEndpoint"],
			}
		}

		return NewKeycloakManager(*keycloakClientConfig, appMetrics)
	case "zitadel":
		zitadelClientConfig := config.ZitadelClientCredentials
		if config.ClientConfig != nil {
			zitadelClientConfig = &ZitadelClientConfig{
				ClientID:           config.ClientConfig.ClientID,
				ClientSecret:       config.ClientConfig.ClientSecret,
				GrantType:          config.ClientConfig.GrantType,
				TokenEndpoint:      config.ClientConfig.TokenEndpoint,
				ManagementEndpoint: config.ExtraConfig["ManagementEndpoint"],
				PAT:                config.ExtraConfig["PAT"],
			}
		}

		return NewZitadelManager(*zitadelClientConfig, appMetrics)
	case "authentik":
		authentikConfig := AuthentikClientConfig{
			Issuer:        config.ClientConfig.Issuer,
			ClientID:      config.ClientConfig.ClientID,
			TokenEndpoint: config.ClientConfig.TokenEndpoint,
			GrantType:     config.ClientConfig.GrantType,
			Username:      config.ExtraConfig["Username"],
			Password:      config.ExtraConfig["Password"],
		}
		return NewAuthentikManager(authentikConfig, appMetrics)
	case "okta":
		oktaClientConfig := OktaClientConfig{
			Issuer:        config.ClientConfig.Issuer,
			TokenEndpoint: config.ClientConfig.TokenEndpoint,
			GrantType:     config.ClientConfig.GrantType,
			APIToken:      config.ExtraConfig["ApiToken"],
		}
		return NewOktaManager(oktaClientConfig, appMetrics)
	case "google":
		googleClientConfig := GoogleWorkspaceClientConfig{
			ServiceAccountKey: config.ExtraConfig["ServiceAccountKey"],
			CustomerID:        config.ExtraConfig["CustomerId"],
		}
		return NewGoogleWorkspaceManager(ctx, googleClientConfig, appMetrics)
	case "jumpcloud":
		jumpcloudConfig := JumpCloudClientConfig{
			APIToken: config.ExtraConfig["ApiToken"],
		}
		return NewJumpCloudManager(jumpcloudConfig, appMetrics)
	default:
		return nil, fmt.Errorf("invalid manager type: %s", config.ManagerType)
	}
}
