package runtimegrpc

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

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

// RuntimeRelatedCountClient owns production Identity exchange plus one bounded
// neutral related-count RPC. It has no CRM route grammar or database access.
type RuntimeRelatedCountClient struct {
	connection          *grpc.ClientConn
	identity            *IdentityExchangeClient
	identityCoordinator *identityExchangeCoordinator
	counts              *RelatedCountClient
	appInstallationID   string
	workloadToken       string
	attemptTimeout      time.Duration
	identityCalls       atomic.Uint64
	recordsCalls        atomic.Uint64
}

func DialRelatedCountRuntime(ctx context.Context, config RuntimeClientConfig) (*RuntimeRelatedCountClient, error) {
	return dialRelatedCountRuntime(ctx, config, newIdentityExchangeCoordinator(config))
}

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

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

func dialRelatedCountRuntimeWithCredentials(ctx context.Context, config RuntimeClientConfig, transport credentials.TransportCredentials) (*RuntimeRelatedCountClient, error) {
	return dialRelatedCountRuntimeWithCredentialsAndCoordinator(ctx, config, transport, newIdentityExchangeCoordinator(config))
}

func dialRelatedCountRuntimeWithCredentialsAndCoordinator(ctx context.Context, config RuntimeClientConfig, transport credentials.TransportCredentials, coordinator *identityExchangeCoordinator) (*RuntimeRelatedCountClient, error) {
	if transport == nil {
		return nil, errors.New("runtime related-count client: transport credentials are required")
	}
	connection, err := runtimeClientConnection(config.Target, transport, coordinator)
	if err != nil {
		return nil, fmt.Errorf("runtime related-count client: create connection: %w", err)
	}
	generatedIdentity := runtimev1.NewIdentityContextServiceClient(connection)
	generatedCounts := runtimev1.NewRelatedRecordCountServiceClient(connection)
	client := &RuntimeRelatedCountClient{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.counts, err = NewRelatedCountClient(func(callCtx context.Context, request *runtimev1.CountRelatedRecordsRequest, options ...grpc.CallOption) (*runtimev1.CountRelatedRecordsResponse, error) {
		client.recordsCalls.Add(1)
		return generatedCounts.CountRelatedRecords(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 related-count client: connection shut down during startup")
		}
		if !connection.WaitForStateChange(ctx, state) {
			_ = connection.Close()
			return nil, fmt.Errorf("runtime related-count client: Runtime not ready: %w", ctx.Err())
		}
	}
}

func (c *RuntimeRelatedCountClient) Count(ctx context.Context, bearer string, request relatedsemantic.Request) (relatedsemantic.Response, error) {
	if c == nil || c.identity == nil || c.counts == nil || c.connection == nil {
		return relatedsemantic.Response{}, &relatedsemantic.Failure{Category: relatedsemantic.FailureUnavailable}
	}
	identityCtx, cancelIdentity := context.WithTimeout(ctx, c.attemptTimeout)
	delegation, err := c.identityCoordinator.exchange(identityCtx, bearer, identitysemantic.Request{AppInstallationID: c.appInstallationID, RequestedCapabilities: []string{relatedsemantic.CapabilityName}}, c.identity.Exchange)
	cancelIdentity()
	if err != nil {
		return relatedsemantic.Response{}, err
	}
	countCtx, cancelCount := context.WithTimeout(ctx, c.attemptTimeout)
	defer cancelCount()
	return c.counts.CountRelatedRecords(withRuntimeDelegation(countCtx, delegation.OpaqueDelegation), request)
}

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

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

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