package runtimegrpc

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

	identitysemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/identityv1"
	querysemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/queryv1"
	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"
)

// RuntimeQueryClient owns the production Identity exchange plus one bounded
// neutral query data RPC. It has no CRM grammar or database dependency.
type RuntimeQueryClient struct {
	connection          *grpc.ClientConn
	identity            *IdentityExchangeClient
	identityCoordinator *identityExchangeCoordinator
	query               *QueryClient
	appInstallationID   string
	workloadToken       string
	attemptTimeout      time.Duration
	identityCalls       atomic.Uint64
	recordsCalls        atomic.Uint64
}

func DialQueryRuntime(ctx context.Context, config RuntimeClientConfig) (*RuntimeQueryClient, error) {
	return dialQueryRuntime(ctx, config, newIdentityExchangeCoordinator(config))
}

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

func dialQueryRuntime(ctx context.Context, config RuntimeClientConfig, coordinator *identityExchangeCoordinator) (*RuntimeQueryClient, error) {
	if ctx == nil {
		return nil, errors.New("runtime query 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 query client: target, AppInstallation, workload token and attempt timeout are required")
	}
	transport, err := runtimeClientTransportCredentials(config)
	if err != nil {
		return nil, err
	}
	return dialQueryRuntimeWithCredentialsAndCoordinator(ctx, config, transport, coordinator)
}

func dialQueryRuntimeWithCredentials(ctx context.Context, config RuntimeClientConfig, transport credentials.TransportCredentials) (*RuntimeQueryClient, error) {
	return dialQueryRuntimeWithCredentialsAndCoordinator(ctx, config, transport, newIdentityExchangeCoordinator(config))
}

func dialQueryRuntimeWithCredentialsAndCoordinator(ctx context.Context, config RuntimeClientConfig, transport credentials.TransportCredentials, coordinator *identityExchangeCoordinator) (*RuntimeQueryClient, error) {
	if transport == nil {
		return nil, errors.New("runtime query client: transport credentials are required")
	}
	connection, err := runtimeClientConnection(config.Target, transport, coordinator)
	if err != nil {
		return nil, fmt.Errorf("runtime query client: create connection: %w", err)
	}
	generatedIdentity := runtimev1.NewIdentityContextServiceClient(connection)
	generatedQuery := runtimev1.NewRecordQueryServiceClient(connection)
	client := &RuntimeQueryClient{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.query, err = NewQueryClient(
		func(callCtx context.Context, request *runtimev1.QueryRecordsRequest, options ...grpc.CallOption) (*runtimev1.QueryRecordsResponse, error) {
			client.recordsCalls.Add(1)
			return generatedQuery.QueryRecords(withWorkloadAuthorization(callCtx, client.workloadToken), request, options...)
		},
		func(callCtx context.Context, request *runtimev1.QueryHistoryRequest, options ...grpc.CallOption) (*runtimev1.QueryHistoryResponse, error) {
			client.recordsCalls.Add(1)
			return generatedQuery.QueryHistory(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 query client: connection shut down during startup")
		}
		if !connection.WaitForStateChange(ctx, state) {
			_ = connection.Close()
			return nil, fmt.Errorf("runtime query client: Runtime not ready: %w", ctx.Err())
		}
	}
}

func (c *RuntimeQueryClient) Query(ctx context.Context, bearer string, request querysemantic.Request) (querysemantic.Response, error) {
	if c == nil || c.identity == nil || c.query == nil || c.connection == nil {
		return querysemantic.Response{}, &querysemantic.Failure{Category: querysemantic.FailureUnavailable}
	}
	identityCtx, cancelIdentity := context.WithTimeout(ctx, c.attemptTimeout)
	delegation, err := c.identityCoordinator.exchange(identityCtx, bearer, identitysemantic.Request{AppInstallationID: c.appInstallationID, RequestedCapabilities: []string{querysemantic.CapabilityName}}, c.identity.Exchange)
	cancelIdentity()
	if err != nil {
		return querysemantic.Response{}, err
	}
	queryCtx, cancelQuery := context.WithTimeout(ctx, c.attemptTimeout)
	defer cancelQuery()
	return c.query.QueryRecords(withRuntimeDelegation(queryCtx, delegation.OpaqueDelegation), request)
}

func (c *RuntimeQueryClient) QueryHistory(ctx context.Context, bearer string, request querysemantic.HistoryRequest) (querysemantic.HistoryResponse, error) {
	if c == nil || c.identity == nil || c.query == nil || c.connection == nil {
		return querysemantic.HistoryResponse{}, &querysemantic.Failure{Category: querysemantic.FailureUnavailable}
	}
	identityCtx, cancelIdentity := context.WithTimeout(ctx, c.attemptTimeout)
	delegation, err := c.identityCoordinator.exchange(identityCtx, bearer, identitysemantic.Request{AppInstallationID: c.appInstallationID, RequestedCapabilities: []string{querysemantic.CapabilityName}}, c.identity.Exchange)
	cancelIdentity()
	if err != nil {
		return querysemantic.HistoryResponse{}, err
	}
	queryCtx, cancelQuery := context.WithTimeout(ctx, c.attemptTimeout)
	defer cancelQuery()
	return c.query.QueryHistory(withRuntimeDelegation(queryCtx, delegation.OpaqueDelegation), request)
}

func (c *RuntimeQueryClient) Ready() bool {
	return c != nil && runtimeConnectionReady(c.connection)
}
func (c *RuntimeQueryClient) Metrics() RuntimeClientMetrics {
	if c == nil {
		return RuntimeClientMetrics{}
	}
	return RuntimeClientMetrics{IdentityCalls: c.identityCalls.Load(), RecordsCalls: c.recordsCalls.Load()}
}
func (c *RuntimeQueryClient) Close() error {
	if c == nil || c.connection == nil {
		return nil
	}
	return c.connection.Close()
}
