package openstack

import (
	"encoding/json"
	"fmt"
	"net/url"
	"os"
	"time"

	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promauth"
)

// authResponse represents identity api response
//
// See https://docs.openstack.org/api-ref/identity/v3/#authentication-and-token-management
type authResponse struct {
	Token struct {
		ExpiresAt time.Time     `json:"expires_at,omitempty"`
		Catalog   []catalogItem `json:"catalog,omitempty"`
	}
}

type catalogItem struct {
	Name      string     `json:"name"`
	Type      string     `json:"type"`
	Endpoints []endpoint `json:"endpoints"`
}

// openstack api endpoint
//
// See https://docs.openstack.org/api-ref/identity/v3/#list-endpoints
type endpoint struct {
	RegionID   string `json:"region_id"`
	RegionName string `json:"region_name"`
	URL        string `json:"url"`
	Name       string `json:"name"`
	Type       string `json:"type"`
	Interface  string `json:"interface"`
}

// getComputeEndpointURL extracts compute endpoint url with given filters from keystone catalog
func getComputeEndpointURL(catalog []catalogItem, availability, region string) (*url.URL, error) {
	for _, eps := range catalog {
		if eps.Type != "compute" {
			continue
		}
		for _, ep := range eps.Endpoints {
			if ep.Interface == availability && (len(region) == 0 || region == ep.RegionID || region == ep.RegionName) {
				return url.Parse(ep.URL)
			}
		}
	}
	return nil, fmt.Errorf("cannot find compute url for the given availability: %q, region: %q", availability, region)
}

// buildAuthRequestBody builds request for authentication
func buildAuthRequestBody(sdc *SDConfig) ([]byte, error) {
	if sdc.Password == nil && len(sdc.ApplicationCredentialID) == 0 && len(sdc.ApplicationCredentialName) == 0 {
		return nil, fmt.Errorf("password and application credentials are missing")
	}
	type domainReq struct {
		ID   *string `json:"id,omitempty"`
		Name *string `json:"name,omitempty"`
	}
	type userReq struct {
		ID       *string    `json:"id,omitempty"`
		Name     *string    `json:"name,omitempty"`
		Password *string    `json:"password,omitempty"`
		Passcode *string    `json:"passcode,omitempty"`
		Domain   *domainReq `json:"domain,omitempty"`
	}
	type passwordReq struct {
		User userReq `json:"user"`
	}
	type tokenReq struct {
		ID string `json:"id"`
	}
	type applicationCredentialReq struct {
		ID     *string  `json:"id,omitempty"`
		Name   *string  `json:"name,omitempty"`
		User   *userReq `json:"user,omitempty"`
		Secret *string  `json:"secret,omitempty"`
	}
	type identityReq struct {
		Methods               []string                  `json:"methods"`
		Password              *passwordReq              `json:"password,omitempty"`
		Token                 *tokenReq                 `json:"token,omitempty"`
		ApplicationCredential *applicationCredentialReq `json:"application_credential,omitempty"`
	}
	type authReq struct {
		Identity identityReq            `json:"identity"`
		Scope    map[string]interface{} `json:"scope,omitempty"`
	}
	type request struct {
		Auth authReq `json:"auth"`
	}

	// Populate the request structure based on the provided arguments. Create and return an error
	// if insufficient or incompatible information is present.
	var req request

	if sdc.Password == nil {
		// There are three kinds of possible application_credential requests
		// 1. application_credential id + secret
		// 2. application_credential name + secret + user_id
		// 3. application_credential name + secret + username + domain_id / domain_name
		if len(sdc.ApplicationCredentialID) > 0 {
			if sdc.ApplicationCredentialSecret == nil {
				return nil, fmt.Errorf("ApplicationCredentialSecret is empty")
			}
			req.Auth.Identity.Methods = []string{"application_credential"}
			secret := sdc.ApplicationCredentialSecret.String()
			req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{
				ID:     &sdc.ApplicationCredentialID,
				Secret: &secret,
			}
			return json.Marshal(req)
		}

		if sdc.ApplicationCredentialSecret == nil {
			return nil, fmt.Errorf("missing application_credential_secret when application_credential_name is set")
		}
		var userRequest *userReq
		if len(sdc.UserID) > 0 {
			// UserID could be used without the domain information
			userRequest = &userReq{
				ID: &sdc.UserID,
			}
		}
		if userRequest == nil && len(sdc.Username) == 0 {
			return nil, fmt.Errorf("username and userid is empty")
		}
		if userRequest == nil && len(sdc.DomainID) > 0 {
			userRequest = &userReq{
				Name:   &sdc.Username,
				Domain: &domainReq{ID: &sdc.DomainID},
			}
		}
		if userRequest == nil && len(sdc.DomainName) > 0 {
			userRequest = &userReq{
				Name:   &sdc.Username,
				Domain: &domainReq{Name: &sdc.DomainName},
			}
		}
		if userRequest == nil {
			return nil, fmt.Errorf("domain_id and domain_name cannot be empty for application_credential_name auth")
		}
		req.Auth.Identity.Methods = []string{"application_credential"}
		secret := sdc.ApplicationCredentialSecret.String()
		req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{
			Name:   &sdc.ApplicationCredentialName,
			User:   userRequest,
			Secret: &secret,
		}
		return json.Marshal(req)
	}

	// Password authentication.
	req.Auth.Identity.Methods = append(req.Auth.Identity.Methods, "password")
	if len(sdc.Username) == 0 && len(sdc.UserID) == 0 {
		return nil, fmt.Errorf("username and userid is empty for username/password auth")
	}
	if len(sdc.Username) > 0 {
		if len(sdc.UserID) > 0 {
			return nil, fmt.Errorf("both username and userid is present")
		}
		if len(sdc.DomainID) == 0 && len(sdc.DomainName) == 0 {
			return nil, fmt.Errorf(" domain_id or domain_name is missing for username/password auth: %s", sdc.Username)
		}
		if len(sdc.DomainID) > 0 {
			if sdc.DomainName != "" {
				return nil, fmt.Errorf("both domain_id and domain_name is present")
			}
			// Configure the request for Username and Password authentication with a DomainID.
			if sdc.Password != nil {
				password := sdc.Password.String()
				req.Auth.Identity.Password = &passwordReq{
					User: userReq{
						Name:     &sdc.Username,
						Password: &password,
						Domain:   &domainReq{ID: &sdc.DomainID},
					},
				}
			}
		}
		if len(sdc.DomainName) > 0 {
			// Configure the request for Username and Password authentication with a DomainName.
			if sdc.Password != nil {
				password := sdc.Password.String()
				req.Auth.Identity.Password = &passwordReq{
					User: userReq{
						Name:     &sdc.Username,
						Password: &password,
						Domain:   &domainReq{Name: &sdc.DomainName},
					},
				}
			}
		}
	}
	if len(sdc.UserID) > 0 {
		if len(sdc.DomainID) > 0 {
			return nil, fmt.Errorf("both user_id and domain_id is present")
		}
		if len(sdc.DomainName) > 0 {
			return nil, fmt.Errorf("both user_id and domain_name is present")
		}
		// Configure the request for UserID and Password authentication.
		if sdc.Password != nil {
			password := sdc.Password.String()
			req.Auth.Identity.Password = &passwordReq{
				User: userReq{
					ID:       &sdc.UserID,
					Password: &password,
				},
			}
		}

	}

	// build scope for password auth
	scope, err := buildScope(sdc)
	if err != nil {
		return nil, err
	}
	if len(scope) > 0 {
		req.Auth.Scope = scope
	}
	return json.Marshal(req)
}

// buildScope adds scope information into auth request
//
// See https://docs.openstack.org/api-ref/identity/v3/#password-authentication-with-unscoped-authorization
func buildScope(sdc *SDConfig) (map[string]interface{}, error) {
	if len(sdc.ProjectName) == 0 && len(sdc.ProjectID) == 0 && len(sdc.DomainID) == 0 && len(sdc.DomainName) == 0 {
		return nil, nil
	}
	if len(sdc.ProjectName) > 0 {
		// ProjectName provided: either DomainID or DomainName must also be supplied.
		// ProjectID may not be supplied.
		if len(sdc.DomainID) == 0 && len(sdc.DomainName) == 0 {
			return nil, fmt.Errorf("domain_id or domain_name must present")
		}
		if len(sdc.DomainID) > 0 {
			return map[string]interface{}{
				"project": map[string]interface{}{
					"name":   &sdc.ProjectName,
					"domain": map[string]interface{}{"id": &sdc.DomainID},
				},
			}, nil
		}
		if len(sdc.DomainName) > 0 {
			return map[string]interface{}{
				"project": map[string]interface{}{
					"name":   &sdc.ProjectName,
					"domain": map[string]interface{}{"name": &sdc.DomainName},
				},
			}, nil
		}
	} else if len(sdc.ProjectID) > 0 {
		return map[string]interface{}{
			"project": map[string]interface{}{
				"id": &sdc.ProjectID,
			},
		}, nil
	} else if len(sdc.DomainID) > 0 {
		if len(sdc.DomainName) > 0 {
			return nil, fmt.Errorf("both domain_id and domain_name present")
		}
		return map[string]interface{}{
			"domain": map[string]interface{}{
				"id": &sdc.DomainID,
			},
		}, nil
	} else if len(sdc.DomainName) > 0 {
		return map[string]interface{}{
			"domain": map[string]interface{}{
				"name": &sdc.DomainName,
			},
		}, nil
	}
	return nil, nil
}

// readCredentialsFromEnv obtains serviceDiscoveryConfig from env variables for openstack
func readCredentialsFromEnv() SDConfig {
	authURL := os.Getenv("OS_AUTH_URL")
	username := os.Getenv("OS_USERNAME")
	userID := os.Getenv("OS_USERID")
	password := os.Getenv("OS_PASSWORD")
	tenantID := os.Getenv("OS_TENANT_ID")
	tenantName := os.Getenv("OS_TENANT_NAME")
	domainID := os.Getenv("OS_DOMAIN_ID")
	domainName := os.Getenv("OS_DOMAIN_NAME")
	applicationCredentialID := os.Getenv("OS_APPLICATION_CREDENTIAL_ID")
	applicationCredentialName := os.Getenv("OS_APPLICATION_CREDENTIAL_NAME")
	applicationCredentialSecret := os.Getenv("OS_APPLICATION_CREDENTIAL_SECRET")
	// If OS_PROJECT_ID is set, overwrite tenantID with the value.
	if v := os.Getenv("OS_PROJECT_ID"); v != "" {
		tenantID = v
	}
	// If OS_PROJECT_NAME is set, overwrite tenantName with the value.
	if v := os.Getenv("OS_PROJECT_NAME"); v != "" {
		tenantName = v
	}
	return SDConfig{
		IdentityEndpoint:            authURL,
		Username:                    username,
		UserID:                      userID,
		Password:                    promauth.NewSecret(password),
		ProjectName:                 tenantName,
		ProjectID:                   tenantID,
		DomainName:                  domainName,
		DomainID:                    domainID,
		ApplicationCredentialName:   applicationCredentialName,
		ApplicationCredentialID:     applicationCredentialID,
		ApplicationCredentialSecret: promauth.NewSecret(applicationCredentialSecret),
	}
}