package runtimegrpc

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"sync/atomic"
	"time"

	identitysemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/identityv1"
	defaultsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recorddefaultsv1"
	runtimev1 "github.com/pingcap-inc/nexuscrm-contract/wire-go/gen/runtime/v1"
	"google.golang.org/grpc"
	"google.golang.org/grpc/connectivity"
	"google.golang.org/grpc/credentials"
)

// RuntimeCreateDefaultPreviewClient owns production Identity exchange plus one
// bounded neutral create-default preview RPC. It has no product route grammar
// and no database or metadata implementation.
type RuntimeCreateDefaultPreviewClient struct {
	connection          *grpc.ClientConn
	identity            *IdentityExchangeClient
	identityCoordinator *identityExchangeCoordinator
	defaults            *CreateDefaultPreviewClient
	appInstallationID   string
	workloadToken       string
	attemptTimeout      time.Duration
	identityCalls       atomic.Uint64
	recordsCalls        atomic.Uint64
}

func DialCreateDefaultPreviewRuntime(
	ctx context.Context,
	config RuntimeClientConfig,
) (*RuntimeCreateDefaultPreviewClient, error) {
	return dialCreateDefaultPreviewRuntime(ctx, config, newIdentityExchangeCoordinator(config))
}

func (group *RuntimeClientGroup) DialCreateDefaultPreviewRuntime(
	ctx context.Context,
) (*RuntimeCreateDefaultPreviewClient, error) {
	if group == nil {
		return nil, errors.New("runtime client group: configuration is required")
	}
	return dialCreateDefaultPreviewRuntime(ctx, group.config, group.identityCoordinator)
}

func dialCreateDefaultPreviewRuntime(
	ctx context.Context,
	config RuntimeClientConfig,
	coordinator *identityExchangeCoordinator,
) (*RuntimeCreateDefaultPreviewClient, error) {
	if ctx == nil {
		return nil, errors.New("runtime create-default preview client: startup context is required")
	}
	if strings.TrimSpace(config.Target) == "" || strings.TrimSpace(config.Target) != config.Target ||
		strings.TrimSpace(config.AppInstallationID) == "" || strings.TrimSpace(config.AppInstallationID) != config.AppInstallationID ||
		strings.TrimSpace(config.WorkloadToken) == "" || strings.TrimSpace(config.WorkloadToken) != config.WorkloadToken ||
		config.AttemptTimeout <= 0 {
		return nil, errors.New("runtime create-default preview client: target, AppInstallation, workload token and attempt timeout are required")
	}
	transport, err := runtimeClientTransportCredentials(config)
	if err != nil {
		return nil, err
	}
	return dialCreateDefaultPreviewRuntimeWithCredentialsAndCoordinator(
		ctx, config, transport, coordinator,
	)
}

func dialCreateDefaultPreviewRuntimeWithCredentials(
	ctx context.Context,
	config RuntimeClientConfig,
	transport credentials.TransportCredentials,
) (*RuntimeCreateDefaultPreviewClient, error) {
	return dialCreateDefaultPreviewRuntimeWithCredentialsAndCoordinator(
		ctx, config, transport, newIdentityExchangeCoordinator(config),
	)
}

func dialCreateDefaultPreviewRuntimeWithCredentialsAndCoordinator(
	ctx context.Context,
	config RuntimeClientConfig,
	transport credentials.TransportCredentials,
	coordinator *identityExchangeCoordinator,
) (*RuntimeCreateDefaultPreviewClient, error) {
	if transport == nil {
		return nil, errors.New("runtime create-default preview client: transport credentials are required")
	}
	connection, err := runtimeClientConnection(config.Target, transport, coordinator)
	if err != nil {
		return nil, fmt.Errorf("runtime create-default preview client: create connection: %w", err)
	}
	generatedIdentity := runtimev1.NewIdentityContextServiceClient(connection)
	generatedDefaults := runtimev1.NewRecordCreateDefaultPreviewServiceClient(connection)
	client := &RuntimeCreateDefaultPreviewClient{
		connection: connection, appInstallationID: config.AppInstallationID,
		workloadToken: config.WorkloadToken, attemptTimeout: config.AttemptTimeout,
		identityCoordinator: coordinator,
	}
	client.identity, err = NewIdentityExchangeClient(func(
		callCtx context.Context,
		request *runtimev1.ExchangeRequest,
		options ...grpc.CallOption,
	) (*runtimev1.ExchangeResponse, error) {
		client.identityCalls.Add(1)
		return generatedIdentity.Exchange(
			withWorkloadAuthorization(callCtx, client.workloadToken), request, options...,
		)
	})
	if err != nil {
		_ = connection.Close()
		return nil, err
	}
	client.defaults, err = NewCreateDefaultPreviewClient(func(
		callCtx context.Context,
		request *runtimev1.PreviewCreateDefaultsRequest,
		options ...grpc.CallOption,
	) (*runtimev1.PreviewCreateDefaultsResponse, error) {
		client.recordsCalls.Add(1)
		return generatedDefaults.PreviewCreateDefaults(
			withWorkloadAuthorization(callCtx, client.workloadToken), request, options...,
		)
	})
	if err != nil {
		_ = connection.Close()
		return nil, err
	}
	connection.Connect()
	for {
		state := connection.GetState()
		if state == connectivity.Ready {
			return client, nil
		}
		if state == connectivity.Shutdown {
			_ = connection.Close()
			return nil, errors.New("runtime create-default preview client: connection shut down during startup")
		}
		if !connection.WaitForStateChange(ctx, state) {
			_ = connection.Close()
			return nil, fmt.Errorf("runtime create-default preview client: Runtime not ready: %w", ctx.Err())
		}
	}
}

func (c *RuntimeCreateDefaultPreviewClient) Preview(
	ctx context.Context,
	bearer string,
	request defaultsemantic.Request,
) (defaultsemantic.Response, error) {
	if c == nil || c.identity == nil || c.defaults == nil || c.connection == nil {
		return defaultsemantic.Response{}, &defaultsemantic.Failure{Category: defaultsemantic.FailureUnavailable}
	}
	delegation, delegated := outgoingRuntimeDelegation(ctx, c.identityCoordinator, c.appInstallationID)
	if !delegated {
		identityCtx, cancelIdentity := context.WithTimeout(ctx, c.attemptTimeout)
		exchanged, err := c.identityCoordinator.exchange(
			identityCtx, bearer,
			identitysemantic.Request{
				AppInstallationID:     c.appInstallationID,
				RequestedCapabilities: []string{defaultsemantic.CapabilityName},
			},
			c.identity.Exchange,
		)
		cancelIdentity()
		if err != nil {
			return defaultsemantic.Response{}, err
		}
		delegation = exchanged.OpaqueDelegation
	}
	previewCtx, cancelPreview := context.WithTimeout(ctx, c.attemptTimeout)
	defer cancelPreview()
	return c.defaults.PreviewCreateDefaults(
		withRuntimeDelegation(previewCtx, delegation), request,
	)
}

func (c *RuntimeCreateDefaultPreviewClient) Ready() bool {
	return c != nil && runtimeConnectionReady(c.connection)
}

func (c *RuntimeCreateDefaultPreviewClient) Metrics() RuntimeClientMetrics {
	if c == nil {
		return RuntimeClientMetrics{}
	}
	return RuntimeClientMetrics{
		IdentityCalls: c.identityCalls.Load(), RecordsCalls: c.recordsCalls.Load(),
	}
}

func (c *RuntimeCreateDefaultPreviewClient) Close() error {
	if c == nil || c.connection == nil {
		return nil
	}
	return c.connection.Close()
}
