package keycloak

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
)

const (
	adminClientID = "admin-cli"
	masterRealm   = "master"
)

// Token represents a Keycloak token.
type Token struct {
	AccessToken      string `json:"access_token"`
	IDToken          string `json:"id_token"`
	ExpiresIn        int    `json:"expires_in"`
	RefreshExpiresIn int    `json:"refresh_expires_in"`
	RefreshToken     string `json:"refresh_token"`
	TokenType        string `json:"token_type"`
	NotBeforePolicy  int    `json:"not-before-policy"`
	SessionState     string `json:"session_state"`
	Scope            string `json:"scope"`
}

// Client represents a Keycloak client(https://www.keycloak.org/docs-api/19.0.3/javadocs/org/keycloak/representations/idm/ClientRepresentation.html).
type Client struct {
	Access                             *map[string]interface{} `json:"access,omitempty"`
	AdminURL                           *string                 `json:"adminUrl,omitempty"`
	Attributes                         *map[string]string      `json:"attributes,omitempty"`
	AuthenticationFlowBindingOverrides *map[string]string      `json:"authenticationFlowBindingOverrides,omitempty"`
	AuthorizationServicesEnabled       *bool                   `json:"authorizationServicesEnabled,omitempty"`
	BaseURL                            *string                 `json:"baseUrl,omitempty"`
	BearerOnly                         *bool                   `json:"bearerOnly,omitempty"`
	ClientAuthenticatorType            *string                 `json:"clientAuthenticatorType,omitempty"`
	ClientID                           *string                 `json:"clientId,omitempty"`
	ConsentRequired                    *bool                   `json:"consentRequired,omitempty"`
	DefaultClientScopes                *[]string               `json:"defaultClientScopes,omitempty"`
	DefaultRoles                       *[]string               `json:"defaultRoles,omitempty"`
	Description                        *string                 `json:"description,omitempty"`
	DirectAccessGrantsEnabled          *bool                   `json:"directAccessGrantsEnabled,omitempty"`
	Enabled                            *bool                   `json:"enabled,omitempty"`
	FrontChannelLogout                 *bool                   `json:"frontchannelLogout,omitempty"`
	FullScopeAllowed                   *bool                   `json:"fullScopeAllowed,omitempty"`
	ID                                 *string                 `json:"id,omitempty"`
	ImplicitFlowEnabled                *bool                   `json:"implicitFlowEnabled,omitempty"`
	Name                               *string                 `json:"name,omitempty"`
	NodeReRegistrationTimeout          *int32                  `json:"nodeReRegistrationTimeout,omitempty"`
	NotBefore                          *int32                  `json:"notBefore,omitempty"`
	OptionalClientScopes               *[]string               `json:"optionalClientScopes,omitempty"`
	Origin                             *string                 `json:"origin,omitempty"`
	Protocol                           *string                 `json:"protocol,omitempty"`
	PublicClient                       *bool                   `json:"publicClient,omitempty"`
	RedirectURIs                       *[]string               `json:"redirectUris,omitempty"`
	RegisteredNodes                    *map[string]int         `json:"registeredNodes,omitempty"`
	RegistrationAccessToken            *string                 `json:"registrationAccessToken,omitempty"`
	RootURL                            *string                 `json:"rootUrl,omitempty"`
	Secret                             *string                 `json:"secret,omitempty"`
	ServiceAccountsEnabled             *bool                   `json:"serviceAccountsEnabled,omitempty"`
	StandardFlowEnabled                *bool                   `json:"standardFlowEnabled,omitempty"`
	SurrogateAuthRequired              *bool                   `json:"surrogateAuthRequired,omitempty"`
	WebOrigins                         *[]string               `json:"webOrigins,omitempty"`
}

// AdminClient is a Keycloak admin client.
type AdminClient struct {
	ServerURL string
	Realm     string
	Username  string
	Password  string
	ClientID  string
	UseTLS    bool

	client *http.Client
}

// NewAdminClient creates a new Keycloak admin client.
func NewAdminClient(ctx *context.Context, serverURL, username, password string) (*AdminClient, error) {
	adminClient := &AdminClient{
		ServerURL: serverURL,
		Realm:     masterRealm,
		Username:  username,
		Password:  password,
		ClientID:  adminClientID,
	}

	if (*ctx).Value(http.Client{}) == nil {
		tr := &http.Transport{
			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
		}
		adminClient.client = &http.Client{Transport: tr}
	} else {
		adminClient.client = (*ctx).Value(http.Client{}).(*http.Client)
	}

	// test connection
	if _, err := adminClient.getToken(); err != nil {
		return nil, err
	}

	return adminClient, nil
}

// GetClient returns a Keycloak client.
func (a *AdminClient) GetClient(realm string, clientID string) (*Client, error) {
	token, err := a.getToken()
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("GET", a.ServerURL+"/admin/realms/"+realm+"/clients", nil)
	if err != nil {
		return nil, err
	}

	req.Header.Add("Authorization", "Bearer "+token.AccessToken)

	resp, err := a.client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var clients []Client
	if err = json.NewDecoder(resp.Body).Decode(&clients); err != nil {
		return nil, err
	}

	for _, c := range clients {
		if *c.ClientID == clientID {
			return &c, nil
		}
	}

	return nil, fmt.Errorf("client not found")
}

func (a *AdminClient) getToken() (*Token, error) {
	var token Token

	resp, err := a.client.PostForm(
		a.ServerURL+"/realms/"+a.Realm+"/protocol/openid-connect/token",
		url.Values{
			"grant_type": {"password"},
			"client_id":  {a.ClientID},
			"username":   {a.Username},
			"password":   {a.Password},
		},
	)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if err = json.NewDecoder(resp.Body).Decode(&token); err != nil {
		return nil, err
	}

	return &token, nil
}

// ClientContext returns a new context with the given HTTP client
// Used to pass a custom HTTP client to the AdminClient
func ClientContext(ctx context.Context, client *http.Client) context.Context {
	return context.WithValue(ctx, http.Client{}, client)
}