package runtimegrpc

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

	identitysemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/identityv1"
	attachmentsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordattachmentsv1"
	viewsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordviewv1"
	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"
)

// RuntimeRecordViewClient is the released historical record-view client. It
// deliberately retains both methods and the historic one-capability delegation
// behavior for existing consumers. New D2A callers must use
// RuntimeRecordViewProofClient instead: that distinct type has no unproven
// attachment-source method to select by accident.
type RuntimeRecordViewClient struct {
	base       *runtimeRecordViewBase
	recordView *RecordViewClient
}

// RuntimeRecordViewProofClient owns the D2A proof-bearing record-view family.
// Record view and attachment-source resolution have distinct Runtime
// capabilities, so each call exchanges only the authority it needs. Product
// route grammar, relationship envelopes and attachment URL parsing remain in
// L1. The historical source-value-only method is intentionally absent.
type RuntimeRecordViewProofClient struct {
	base              *runtimeRecordViewBase
	recordView        *RecordViewReadClient
	attachmentSources *AttachmentSourceClient
}

type runtimeRecordViewBase struct {
	connection          *grpc.ClientConn
	identity            *IdentityExchangeClient
	identityCoordinator *identityExchangeCoordinator
	appInstallationID   string
	workloadToken       string
	attemptTimeout      time.Duration
	identityCalls       atomic.Uint64
	recordsCalls        atomic.Uint64
}

// DialRecordViewRuntime preserves the released historic client. It may be
// used only with a Runtime that explicitly still serves the historical
// RecordView attachment method. New D2A composition calls
// DialRecordViewProofRuntime.
func DialRecordViewRuntime(ctx context.Context, config RuntimeClientConfig) (*RuntimeRecordViewClient, error) {
	return dialRecordViewRuntime(ctx, config, newIdentityExchangeCoordinator(config))
}

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

func dialRecordViewRuntime(ctx context.Context, config RuntimeClientConfig, coordinator *identityExchangeCoordinator) (*RuntimeRecordViewClient, error) {
	transport, err := recordViewRuntimeTransport(ctx, config)
	if err != nil {
		return nil, err
	}
	return dialRecordViewRuntimeWithCredentialsAndCoordinator(ctx, config, transport, coordinator)
}

func dialRecordViewRuntimeWithCredentials(ctx context.Context, config RuntimeClientConfig, transport credentials.TransportCredentials) (*RuntimeRecordViewClient, error) {
	return dialRecordViewRuntimeWithCredentialsAndCoordinator(ctx, config, transport, newIdentityExchangeCoordinator(config))
}

func dialRecordViewRuntimeWithCredentialsAndCoordinator(ctx context.Context, config RuntimeClientConfig, transport credentials.TransportCredentials, coordinator *identityExchangeCoordinator) (*RuntimeRecordViewClient, error) {
	base, err := newRuntimeRecordViewBase(ctx, config, transport, coordinator)
	if err != nil {
		return nil, err
	}
	generatedRecordView := runtimev1.NewRecordViewReadServiceClient(base.connection)
	client := &RuntimeRecordViewClient{base: base}
	client.recordView, err = NewRecordViewClient(
		func(callCtx context.Context, request *runtimev1.ReadRecordViewRequest, options ...grpc.CallOption) (*runtimev1.ReadRecordViewResponse, error) {
			base.recordsCalls.Add(1)
			return generatedRecordView.ReadRecordView(withWorkloadAuthorization(callCtx, base.workloadToken), request, options...)
		},
		func(callCtx context.Context, request *runtimev1.ResolveRecordAttachmentSourcesRequest, options ...grpc.CallOption) (*runtimev1.ResolveRecordAttachmentSourcesResponse, error) {
			base.recordsCalls.Add(1)
			return generatedRecordView.ResolveRecordAttachmentSources(withWorkloadAuthorization(callCtx, base.workloadToken), request, options...)
		},
	)
	if err != nil {
		_ = base.connection.Close()
		return nil, err
	}
	if err := base.waitReady(ctx); err != nil {
		_ = base.connection.Close()
		return nil, err
	}
	return client, nil
}

// DialRecordViewProofRuntime constructs the only D2A record-view client. Its
// type has no method for the historical source-value-only RPC, so an L1
// migration must provide the field-bound proof contract explicitly.
func DialRecordViewProofRuntime(ctx context.Context, config RuntimeClientConfig) (*RuntimeRecordViewProofClient, error) {
	return dialRecordViewProofRuntime(ctx, config, newIdentityExchangeCoordinator(config))
}

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

func dialRecordViewProofRuntime(ctx context.Context, config RuntimeClientConfig, coordinator *identityExchangeCoordinator) (*RuntimeRecordViewProofClient, error) {
	transport, err := recordViewRuntimeTransport(ctx, config)
	if err != nil {
		return nil, err
	}
	return dialRecordViewProofRuntimeWithCredentialsAndCoordinator(ctx, config, transport, coordinator)
}

func dialRecordViewProofRuntimeWithCredentials(ctx context.Context, config RuntimeClientConfig, transport credentials.TransportCredentials) (*RuntimeRecordViewProofClient, error) {
	return dialRecordViewProofRuntimeWithCredentialsAndCoordinator(ctx, config, transport, newIdentityExchangeCoordinator(config))
}

func dialRecordViewProofRuntimeWithCredentialsAndCoordinator(ctx context.Context, config RuntimeClientConfig, transport credentials.TransportCredentials, coordinator *identityExchangeCoordinator) (*RuntimeRecordViewProofClient, error) {
	base, err := newRuntimeRecordViewBase(ctx, config, transport, coordinator)
	if err != nil {
		return nil, err
	}
	generatedRecordView := runtimev1.NewRecordViewReadServiceClient(base.connection)
	generatedAttachmentSources := runtimev1.NewRecordAttachmentSourceResolutionServiceClient(base.connection)
	client := &RuntimeRecordViewProofClient{base: base}
	client.recordView, err = NewRecordViewReadClient(
		func(callCtx context.Context, request *runtimev1.ReadRecordViewRequest, options ...grpc.CallOption) (*runtimev1.ReadRecordViewResponse, error) {
			base.recordsCalls.Add(1)
			return generatedRecordView.ReadRecordView(withWorkloadAuthorization(callCtx, base.workloadToken), request, options...)
		},
	)
	if err != nil {
		_ = base.connection.Close()
		return nil, err
	}
	client.attachmentSources, err = NewAttachmentSourceClient(
		func(callCtx context.Context, request *runtimev1.ResolveAttachmentSourcesRequest, options ...grpc.CallOption) (*runtimev1.ResolveAttachmentSourcesResponse, error) {
			base.recordsCalls.Add(1)
			return generatedAttachmentSources.ResolveAttachmentSources(withWorkloadAuthorization(callCtx, base.workloadToken), request, options...)
		},
	)
	if err != nil {
		_ = base.connection.Close()
		return nil, err
	}
	if err := base.waitReady(ctx); err != nil {
		_ = base.connection.Close()
		return nil, err
	}
	return client, nil
}

func recordViewRuntimeTransport(ctx context.Context, config RuntimeClientConfig) (credentials.TransportCredentials, error) {
	if ctx == nil {
		return nil, errors.New("runtime record-view 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 record-view client: target, AppInstallation, workload token and attempt timeout are required")
	}
	return runtimeClientTransportCredentials(config)
}

func newRuntimeRecordViewBase(ctx context.Context, config RuntimeClientConfig, transport credentials.TransportCredentials, coordinator *identityExchangeCoordinator) (*runtimeRecordViewBase, error) {
	if transport == nil {
		return nil, errors.New("runtime record-view client: transport credentials are required")
	}
	connection, err := runtimeClientConnection(config.Target, transport, coordinator)
	if err != nil {
		return nil, fmt.Errorf("runtime record-view client: create connection: %w", err)
	}
	base := &runtimeRecordViewBase{
		connection: connection, appInstallationID: config.AppInstallationID, workloadToken: config.WorkloadToken, attemptTimeout: config.AttemptTimeout,
		identityCoordinator: coordinator,
	}
	generatedIdentity := runtimev1.NewIdentityContextServiceClient(connection)
	base.identity, err = NewIdentityExchangeClient(func(callCtx context.Context, request *runtimev1.ExchangeRequest, options ...grpc.CallOption) (*runtimev1.ExchangeResponse, error) {
		base.identityCalls.Add(1)
		return generatedIdentity.Exchange(withWorkloadAuthorization(callCtx, base.workloadToken), request, options...)
	})
	if err != nil {
		_ = connection.Close()
		return nil, err
	}
	return base, nil
}

func (b *runtimeRecordViewBase) waitReady(ctx context.Context) error {
	if b == nil || b.connection == nil {
		return errors.New("runtime record-view client: connection is required")
	}
	b.connection.Connect()
	for {
		state := b.connection.GetState()
		if state == connectivity.Ready {
			return nil
		}
		if state == connectivity.Shutdown {
			return errors.New("runtime record-view client: connection shut down during startup")
		}
		if !b.connection.WaitForStateChange(ctx, state) {
			return fmt.Errorf("runtime record-view client: Runtime not ready: %w", ctx.Err())
		}
	}
}

func (c *RuntimeRecordViewClient) Read(ctx context.Context, bearer string, request viewsemantic.ReadRequest) (viewsemantic.ReadResponse, error) {
	if c == nil || c.base == nil || c.base.identity == nil || c.recordView == nil || c.base.connection == nil {
		return viewsemantic.ReadResponse{}, &viewsemantic.Failure{Category: viewsemantic.FailureUnavailable}
	}
	delegation, err := c.base.exchange(ctx, bearer, viewsemantic.CapabilityName)
	if err != nil {
		return viewsemantic.ReadResponse{}, err
	}
	callCtx, cancel := context.WithTimeout(ctx, c.base.attemptTimeout)
	defer cancel()
	return c.recordView.ReadRecordView(withRuntimeDelegation(callCtx, delegation), request)
}

func (c *RuntimeRecordViewClient) ResolveAttachmentSources(ctx context.Context, bearer string, request viewsemantic.ResolveAttachmentSourcesRequest) (viewsemantic.ResolveAttachmentSourcesResponse, error) {
	if c == nil || c.base == nil || c.base.identity == nil || c.recordView == nil || c.base.connection == nil {
		return viewsemantic.ResolveAttachmentSourcesResponse{}, &viewsemantic.Failure{Category: viewsemantic.FailureUnavailable}
	}
	delegation, err := c.base.exchange(ctx, bearer, viewsemantic.CapabilityName)
	if err != nil {
		return viewsemantic.ResolveAttachmentSourcesResponse{}, err
	}
	callCtx, cancel := context.WithTimeout(ctx, c.base.attemptTimeout)
	defer cancel()
	return c.recordView.ResolveRecordAttachmentSources(withRuntimeDelegation(callCtx, delegation), request)
}

func (c *RuntimeRecordViewProofClient) Read(ctx context.Context, bearer string, request viewsemantic.ReadRequest) (viewsemantic.ReadResponse, error) {
	if c == nil || c.base == nil || c.base.identity == nil || c.recordView == nil || c.base.connection == nil {
		return viewsemantic.ReadResponse{}, &viewsemantic.Failure{Category: viewsemantic.FailureUnavailable}
	}
	delegation, err := c.base.exchange(ctx, bearer, viewsemantic.CapabilityName)
	if err != nil {
		return viewsemantic.ReadResponse{}, err
	}
	callCtx, cancel := context.WithTimeout(ctx, c.base.attemptTimeout)
	defer cancel()
	return c.recordView.ReadRecordView(withRuntimeDelegation(callCtx, delegation), request)
}

func (c *RuntimeRecordViewProofClient) ResolveAttachmentSourceProofs(ctx context.Context, bearer string, request attachmentsemantic.ResolveRequest) (attachmentsemantic.ResolveResponse, error) {
	if c == nil || c.base == nil || c.base.identity == nil || c.attachmentSources == nil || c.base.connection == nil {
		return attachmentsemantic.ResolveResponse{}, &attachmentsemantic.Failure{Category: attachmentsemantic.FailureUnavailable}
	}
	delegation, err := c.base.exchange(ctx, bearer, attachmentsemantic.CapabilityName)
	if err != nil {
		return attachmentsemantic.ResolveResponse{}, err
	}
	callCtx, cancel := context.WithTimeout(ctx, c.base.attemptTimeout)
	defer cancel()
	return c.attachmentSources.ResolveAttachmentSources(withRuntimeDelegation(callCtx, delegation), request)
}

func (b *runtimeRecordViewBase) exchange(ctx context.Context, bearer, capability string) (string, error) {
	if delegation, ok := outgoingRuntimeDelegation(ctx, b.identityCoordinator, b.appInstallationID); ok {
		return delegation, nil
	}
	identityCtx, cancel := context.WithTimeout(ctx, b.attemptTimeout)
	defer cancel()
	delegation, err := b.identityCoordinator.exchange(identityCtx, bearer, identitysemantic.Request{
		AppInstallationID: b.appInstallationID, RequestedCapabilities: []string{capability},
	}, b.identity.Exchange)
	if err != nil {
		return "", err
	}
	return delegation.OpaqueDelegation, nil
}

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

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

func (c *RuntimeRecordViewClient) Metrics() RuntimeClientMetrics {
	if c == nil || c.base == nil {
		return RuntimeClientMetrics{}
	}
	return c.base.metrics()
}

func (c *RuntimeRecordViewProofClient) Metrics() RuntimeClientMetrics {
	if c == nil || c.base == nil {
		return RuntimeClientMetrics{}
	}
	return c.base.metrics()
}

func (b *runtimeRecordViewBase) metrics() RuntimeClientMetrics {
	return RuntimeClientMetrics{IdentityCalls: b.identityCalls.Load(), RecordsCalls: b.recordsCalls.Load()}
}

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

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