package orgsso

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

	"github.com/tidbcloud/account/thirdparty/auth0"

	"github.com/tidbcloud/tiinf/errs"

	"github.com/tidbcloud/account/commons/config"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

const (
	CreateConnectionUrlFormat = "https://%s/api/v2/connections"
	DeleteConnectionUrlFormat = "https://%s/api/v2/connections/%s"
	UpdateConnectionUrlFormat = "https://%s/api/v2/connections/%s"

	UpdateConnectionClientsUrlFormat = "https://%s/api/v2/connections/%s/clients"

	enableClientSeparator = ","
)

var (
	auth0ReqClient = &http.Client{
		Timeout: time.Second * 6,
	}
)

type Auth0ConnectionErrResp struct {
	StatusCode float64 `json:"statusCode"`
	Error      string  `json:"error"`
	Message    string  `json:"message"`
	ErrorCode  string  `json:"errorCode"`
}
type Auth0ConnectionResp struct {
	Id   string `json:"id"`
	Name string `json:"name"`
}

func GenSamlConnectionParamWithMetadataUrl(_ context.Context, connectionName, metadataUrl string) *CreateConnectionParam {
	clients := config.Global().SSOConfig.EnableClients
	enableClients := strings.Split(clients, enableClientSeparator)
	return &CreateConnectionParam{
		ConnectionName: connectionName,
		Strategy:       "samlp",
		Options: map[string]string{
			"metadataUrl": metadataUrl,
		},
		EnabledClients: enableClients,
	}
}

func GenSamlConnectionParamWithSignOnUrl(_ context.Context, connectionName, signOnUrl, cert string) *CreateConnectionParam {
	clients := config.Global().SSOConfig.EnableClients
	enableClients := strings.Split(clients, enableClientSeparator)
	signingCert := base64.URLEncoding.EncodeToString([]byte(cert))
	return &CreateConnectionParam{
		ConnectionName: connectionName,
		Strategy:       "samlp",
		Options: map[string]string{
			"signInEndpoint": signOnUrl,
			"signingCert":    signingCert,
		},
		EnabledClients: enableClients,
	}
}

func GenOidcConnectionParamWithDiscoveryUrl(_ context.Context, connectionName, idpDomain, clientID, clientSecret string) *CreateConnectionParam {
	clients := config.Global().SSOConfig.EnableClients
	enableClients := strings.Split(clients, enableClientSeparator)
	return &CreateConnectionParam{
		ConnectionName: connectionName,
		Strategy:       "oidc",
		Options: map[string]string{
			"type":          "back_channel",
			"client_id":     clientID,
			"client_secret": clientSecret,
			"scopes":        "openid profile email",
			"discovery_url": fmt.Sprintf("https://%s/.well-known/openid-configuration", idpDomain),
		},
		EnabledClients: enableClients,
	}
}

func UpdateAuth0Connection(ctx context.Context, connectionID string, param *CreateConnectionParam) error {
	// auth0 not support change strategy name
	updateParam := UpdateConnectionParam{
		Options: param.Options,
		// auth0 not to support enable clients on update connection api, need to call update clients api to enable
		// https://auth0.com/docs/troubleshoot/product-lifecycle/deprecations-and-migrations/migrate-dedicated-connection-endpoints?mkt_tok=ODU1LVFBSC02OTkAAAGhx0N3UjIksvWOwiv-OBmNQ3A7NWGBdayTFDF2tffYYj-ke4EtXS-vI4IV78OHp8SSHUrtZ2VubLHaNxZAWGalWCKJBtG4qfm2WMODg4WSKte_GslR
		//EnabledClients: param.EnabledClients,
	}
	marshal, _ := json.Marshal(&updateParam)
	jsonBody := string(marshal)
	data := strings.NewReader(jsonBody)

	ssoConfig := config.Global().SSOConfig

	updateConnectionUrl := fmt.Sprintf(UpdateConnectionUrlFormat, ssoConfig.Auth0Domain, connectionID)

	accessToken, err := auth0.GetAuth0AccessToken(ctx)
	if err != nil {
		tilog.Error(ctx, "UpdateAuth0Connection GetAuth0AccessToken error", zap.Any("url", updateConnectionUrl), zap.Any("data", data))
		return err
	}

	req, _ := http.NewRequest("PATCH", updateConnectionUrl, data)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))

	tilog.Info(ctx, "UpdateAuth0Connection request", zap.Any("url", updateConnectionUrl), zap.Any("jsonBody", jsonBody))
	rsp, err := auth0ReqClient.Do(req)
	if err != nil {
		tilog.Error(ctx, "UpdateAuth0Connection patch error", zap.Any("err", err))
		return err
	}
	defer func() {
		err := rsp.Body.Close()
		if err != nil {
			tilog.Error(ctx, "UpdateAuth0Connection close body error", zap.Any("err", err))
		}
	}()

	httpCode := rsp.StatusCode
	body, err := io.ReadAll(rsp.Body)
	if err != nil {
		tilog.Error(ctx, "UpdateAuth0Connection read body error", zap.Any("err", err))
		return err
	}
	updateConnectionRet := string(body)
	tilog.Info(ctx, "UpdateAuth0Connection response", zap.Any("httpCode", httpCode), zap.Any("rspBody", updateConnectionRet))

	if rsp.StatusCode == 200 {
		tilog.Info(ctx, "UpdateAuth0Connection success", zap.Any("connectionID", connectionID))
		return UpdateConnectionClients(ctx, connectionID)
	}

	var errResp Auth0ConnectionErrResp
	err = json.Unmarshal(body, &errResp)
	if err != nil {
		tilog.Error(ctx, "UpdateAuth0Connection response unmarshal body error", zap.Any("err", err))
		return err
	}
	tilog.Error(ctx, "UpdateAuth0Connection errResp", zap.Any("connectionID", connectionID), zap.Any("errResp", errResp))
	return errs.NewBadRequest(errResp.Message, nil)
}

func CreateAuth0Connection(ctx context.Context, param *CreateConnectionParam) (*Auth0ConnectionResp, error) {
	ssoConfig := config.Global().SSOConfig

	createConnectionUrl := fmt.Sprintf(CreateConnectionUrlFormat, ssoConfig.Auth0Domain)
	marshal, _ := json.Marshal(param)
	jsonBody := string(marshal)
	data := strings.NewReader(jsonBody)

	accessToken, err := auth0.GetAuth0AccessToken(ctx)
	if err != nil {
		tilog.Error(ctx, "CreateAuth0Connection GetAuth0AccessToken error", zap.Any("url", createConnectionUrl), zap.Any("data", data))
		return nil, err
	}

	req, _ := http.NewRequest("POST", createConnectionUrl, data)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))

	tilog.Info(ctx, "CreateAuth0Connection request", zap.Any("url", createConnectionUrl), zap.Any("jsonBody", jsonBody))
	rsp, err := auth0ReqClient.Do(req)
	if err != nil {
		tilog.Error(ctx, "CreateAuth0Connection post error", zap.Any("err", err))
		return nil, err
	}
	defer func() {
		err := rsp.Body.Close()
		if err != nil {
			tilog.Error(ctx, "CreateAuth0Connection close body error", zap.Any("err", err))
		}
	}()

	httpCode := rsp.StatusCode
	body, err := io.ReadAll(rsp.Body)
	if err != nil {
		tilog.Error(ctx, "CreateAuth0Connection read body error", zap.Any("err", err))
		return nil, err
	}
	createConnectionRet := string(body)
	tilog.Info(ctx, "CreateAuth0Connection response", zap.Any("httpCode", httpCode), zap.Any("rspBody", createConnectionRet))

	if rsp.StatusCode == 201 {
		var respBean Auth0ConnectionResp
		err = json.Unmarshal(body, &respBean)
		if err != nil {
			tilog.Error(ctx, "CreateAuth0Connection response unmarshal body error", zap.Any("err", err))
			return nil, err
		}
		tilog.Info(ctx, "CreateAuth0Connection respBean", zap.Any("respBean", respBean))
		return &respBean, nil
	}

	var errResp Auth0ConnectionErrResp
	err = json.Unmarshal(body, &errResp)
	if err != nil {
		tilog.Error(ctx, "CreateAuth0Connection response unmarshal body error", zap.Any("err", err))
		return nil, err
	}
	tilog.Info(ctx, "CreateAuth0Connection errResp", zap.Any("errResp", errResp))
	err = errs.NewBadRequest(errResp.Message, nil)
	return nil, err
}

func DeleteAuth0Connection(ctx context.Context, connectionID string) error {
	ssoConfig := config.Global().SSOConfig

	deleteConnectionUrl := fmt.Sprintf(DeleteConnectionUrlFormat, ssoConfig.Auth0Domain, connectionID)

	accessToken, err := auth0.GetAuth0AccessToken(ctx)
	if err != nil {
		tilog.Error(ctx, "DeleteAuth0Connection GetAuth0AccessToken error", zap.Any("url", deleteConnectionUrl))
		return err
	}

	req, _ := http.NewRequest("DELETE", deleteConnectionUrl, nil)
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))

	tilog.Info(ctx, "DeleteAuth0Connection request", zap.Any("url", deleteConnectionUrl))
	rsp, err := auth0ReqClient.Do(req)

	httpCode := rsp.StatusCode
	body, _ := io.ReadAll(rsp.Body)
	tilog.Info(ctx, "DeleteAuth0Connection response", zap.Any("httpCode", httpCode), zap.Any("body", string(body)))

	if err != nil {
		tilog.Error(ctx, "DeleteAuth0Connection delete error", zap.Any("err", err))
		return err
	}
	defer func() {
		err := rsp.Body.Close()
		if err != nil {
			tilog.Error(ctx, "DeleteAuth0Connection close body error", zap.Any("err", err))
		}
	}()

	return nil
}

type CreateConnectionParam struct {
	ConnectionName string            `json:"name"`
	Strategy       string            `json:"strategy"`
	Options        map[string]string `json:"options"`
	EnabledClients []string          `json:"enabled_clients"`
}

type UpdateConnectionParam struct {
	Options        map[string]string `json:"options"`
	EnabledClients []string          `json:"enabled_clients"`
}

type ConnectionClientParam struct {
	ClientID string `json:"client_id"`
	Status   bool   `json:"status"`
}

// https://auth0.com/docs/api/management/v2/connections/patch-clients
func UpdateConnectionClients(ctx context.Context, connectionID string) error {
	ssoConfig := config.Global().SSOConfig
	enableClients := strings.Split(ssoConfig.EnableClients, enableClientSeparator)

	var params []ConnectionClientParam
	for _, c := range enableClients {
		params = append(params, ConnectionClientParam{
			ClientID: c,
			Status:   true,
		})
	}
	marshal, _ := json.Marshal(&params)
	jsonBody := string(marshal)
	data := strings.NewReader(jsonBody)

	updateClientsUrl := fmt.Sprintf(UpdateConnectionClientsUrlFormat, ssoConfig.Auth0Domain, connectionID)

	accessToken, err := auth0.GetAuth0AccessToken(ctx)
	if err != nil {
		tilog.Error(ctx, "UpdateConnectionClients GetAuth0AccessToken error", zap.Any("url", updateClientsUrl), zap.Any("data", data))
		return err
	}

	req, _ := http.NewRequest("PATCH", updateClientsUrl, data)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))

	tilog.Info(ctx, "UpdateConnectionClients request", zap.Any("url", updateClientsUrl), zap.Any("jsonBody", jsonBody))
	rsp, err := auth0ReqClient.Do(req)
	if err != nil {
		tilog.Error(ctx, "UpdateConnectionClients patch error", zap.Any("err", err))
		return err
	}
	defer func() {
		err := rsp.Body.Close()
		if err != nil {
			tilog.Error(ctx, "UpdateConnectionClients close body error", zap.Any("err", err))
		}
	}()

	httpCode := rsp.StatusCode
	body, err := io.ReadAll(rsp.Body)
	if err != nil {
		tilog.Error(ctx, "UpdateConnectionClients read body error", zap.Any("err", err))
		return err
	}
	updateConnectionRet := string(body)
	tilog.Info(ctx, "UpdateConnectionClients response", zap.Any("httpCode", httpCode), zap.Any("rspBody", updateConnectionRet))

	if rsp.StatusCode == 200 || rsp.StatusCode == 204 {
		tilog.Info(ctx, "UpdateConnectionClients success", zap.Any("connectionID", connectionID))
		return nil
	}

	var errResp Auth0ConnectionErrResp
	err = json.Unmarshal(body, &errResp)
	if err != nil {
		tilog.Error(ctx, "UpdateConnectionClients response unmarshal body error", zap.Any("err", err))
		return err
	}
	tilog.Error(ctx, "UpdateConnectionClients errResp", zap.Any("connectionID", connectionID), zap.Any("errResp", errResp))
	return errs.NewBadRequest(errResp.Message, nil)
}
