package packs

import (
	"encoding/json"
	"fmt"
	"slices"
	"strings"
)

// ConnectorCalloutProfileFinalizationProjection is the immutable, provider-
// blind subset of a callout profile required to bind inbound remote identity.
// It intentionally excludes pagination, batch reads, object paths, secrets,
// and every external-effect behavior.
type ConnectorCalloutProfileFinalizationProjection struct {
	OwnerPack            string  `json:"ownerPack"`
	ProfileName          string  `json:"profileName"`
	CredentialRef        string  `json:"credentialRef"`
	EndpointClass        string  `json:"endpointClass"`
	MaxAttempts          int     `json:"maxAttempts"`
	BaseDelayNanos       int64   `json:"baseDelayNanos"`
	Multiplier           float64 `json:"multiplier"`
	MaxDelayNanos        int64   `json:"maxDelayNanos"`
	RetryableStatusCodes []int   `json:"retryableStatusCodes,omitempty"`
}

func connectorCalloutProfileFinalizationProjection(
	ownerPack string,
	definition connectorCalloutProfileDefinition,
) ConnectorCalloutProfileFinalizationProjection {
	return ConnectorCalloutProfileFinalizationProjection{
		OwnerPack:            strings.TrimSpace(ownerPack),
		ProfileName:          strings.TrimSpace(definition.ProfileName),
		CredentialRef:        strings.TrimSpace(definition.CredentialRef),
		EndpointClass:        strings.TrimSpace(definition.EndpointClass),
		MaxAttempts:          definition.RetryPolicy.MaxAttempts,
		BaseDelayNanos:       int64(definition.RetryPolicy.BaseDelay),
		Multiplier:           definition.RetryPolicy.Multiplier,
		MaxDelayNanos:        int64(definition.RetryPolicy.MaxDelay),
		RetryableStatusCodes: slices.Clone(definition.RetryPolicy.RetryableStatusCodes),
	}
}

func connectorCalloutProfileFinalizationProjectionFromRow(
	row connectorCalloutProfileProjectionRow,
) ConnectorCalloutProfileFinalizationProjection {
	return ConnectorCalloutProfileFinalizationProjection{
		OwnerPack:            strings.TrimSpace(row.OwnerPack),
		ProfileName:          strings.TrimSpace(row.ProfileName),
		CredentialRef:        strings.TrimSpace(row.CredentialRef),
		EndpointClass:        strings.TrimSpace(row.EndpointClass),
		MaxAttempts:          row.RetryPolicy.MaxAttempts,
		BaseDelayNanos:       int64(row.RetryPolicy.BaseDelay),
		Multiplier:           row.RetryPolicy.Multiplier,
		MaxDelayNanos:        int64(row.RetryPolicy.MaxDelay),
		RetryableStatusCodes: slices.Clone(row.RetryPolicy.RetryableStatusCodes),
	}
}

// LoadPersistedConnectorCalloutProfileFinalization selects one exact profile
// projection from an immutable pack generation. It never opens pack source or
// falls back to the live profile table.
func LoadPersistedConnectorCalloutProfileFinalization(
	raw string,
	ownerPack string,
	businessKey string,
) (ConnectorCalloutProfileFinalizationProjection, error) {
	ownerPack = strings.TrimSpace(ownerPack)
	profileName, err := connectorCalloutProfileNameFromBusinessKey(
		ownerPack,
		businessKey,
	)
	if err != nil {
		return ConnectorCalloutProfileFinalizationProjection{}, err
	}
	rows, err := persistedRuntimeDeclarationRows(raw)
	if err != nil {
		return ConnectorCalloutProfileFinalizationProjection{}, err
	}
	var selected *runtimeStartupShallowDeclaredRow
	for index := range rows {
		row := rows[index]
		if row.Kind != ComponentKindConnectorCalloutProfile ||
			row.BusinessKey != profileName {
			continue
		}
		if selected != nil {
			return ConnectorCalloutProfileFinalizationProjection{}, fmt.Errorf(
				"connector callout profile %q is ambiguous",
				businessKey,
			)
		}
		copy := row
		selected = &copy
	}
	if selected == nil {
		return ConnectorCalloutProfileFinalizationProjection{}, fmt.Errorf(
			"connector callout profile %q is absent from persisted generation",
			businessKey,
		)
	}
	projection, err := decodePersistedConnectorCalloutProfileFinalization(*selected)
	if err != nil {
		return ConnectorCalloutProfileFinalizationProjection{}, err
	}
	if projection.OwnerPack != ownerPack || projection.ProfileName != profileName {
		return ConnectorCalloutProfileFinalizationProjection{}, fmt.Errorf(
			"connector callout profile %q projection identity disagrees with its binding",
			businessKey,
		)
	}
	return projection, nil
}

func normalizePersistedConnectorCalloutProfileFinalizationProjection(
	row runtimeStartupShallowDeclaredRow,
) (string, error) {
	projection, err := decodePersistedConnectorCalloutProfileFinalization(row)
	if err != nil {
		return "", err
	}
	if projection.ProfileName != row.BusinessKey {
		return "", fmt.Errorf(
			"connector callout profile projection name %q disagrees with business key %q",
			projection.ProfileName,
			row.BusinessKey,
		)
	}
	return marshalRuntimeStartupShallowProjection(projection)
}

func decodePersistedConnectorCalloutProfileFinalization(
	row runtimeStartupShallowDeclaredRow,
) (ConnectorCalloutProfileFinalizationProjection, error) {
	if strings.TrimSpace(row.Projection) == "" {
		return ConnectorCalloutProfileFinalizationProjection{}, fmt.Errorf(
			"connector callout profile projection is required",
		)
	}
	var projection ConnectorCalloutProfileFinalizationProjection
	if err := decodeStrictJSON([]byte(row.Projection), &projection); err != nil {
		return ConnectorCalloutProfileFinalizationProjection{}, err
	}
	for name, value := range map[string]string{
		"ownerPack":     projection.OwnerPack,
		"profileName":   projection.ProfileName,
		"credentialRef": projection.CredentialRef,
		"endpointClass": projection.EndpointClass,
	} {
		if strings.TrimSpace(value) == "" || strings.TrimSpace(value) != value {
			return ConnectorCalloutProfileFinalizationProjection{}, fmt.Errorf(
				"connector callout profile %s is blank or non-canonical",
				name,
			)
		}
	}
	if projection.MaxAttempts < 1 {
		return ConnectorCalloutProfileFinalizationProjection{}, fmt.Errorf(
			"connector callout profile maxAttempts must be positive",
		)
	}
	return projection, nil
}

func connectorCalloutProfileNameFromBusinessKey(
	ownerPack string,
	businessKey string,
) (string, error) {
	ownerPack = strings.TrimSpace(ownerPack)
	businessKey = strings.TrimSpace(businessKey)
	parts := strings.SplitN(businessKey, "/", 2)
	if ownerPack == "" || len(parts) != 2 || parts[0] != ownerPack ||
		strings.TrimSpace(parts[1]) == "" || strings.TrimSpace(parts[1]) != parts[1] {
		return "", fmt.Errorf(
			"connector callout profile business key %q is not owned by %q",
			businessKey,
			ownerPack,
		)
	}
	return parts[1], nil
}

func marshalConnectorCalloutProfileFinalizationProjection(
	projection ConnectorCalloutProfileFinalizationProjection,
) (string, error) {
	raw, err := json.Marshal(projection)
	if err != nil {
		return "", err
	}
	return string(raw), nil
}
