package connector_outboundintent

import (
	"bytes"
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"sort"
	"strconv"
	"strings"

	connector_callout "github.com/pingcap-inc/nexuscrm-connectors/features/connector/callout"
	catalogv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	connectorv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/connector"
	outboundintentv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/outboundintentv1"
)

type Authority struct {
	view     catalogv1.IntegrationBindingView
	bindings map[string]catalogv1.IntegrationBinding
}

type ResolvedAuthority struct {
	Contract           connectorv1.ConnectorPushDurableMetadataContract
	ContractBinding    catalogv1.IntegrationBinding
	Profile            *connector_callout.ConnectorCalloutProfile
	ProfileBinding     catalogv1.IntegrationBinding
	MappingBindings    []catalogv1.IntegrationBinding
	MappingFingerprint string
	BodyFingerprint    string
	RetryPolicyJSON    string
	ProfileHash        string
}

type resolvedProfileAuthority struct {
	Profile         *connector_callout.ConnectorCalloutProfile
	Binding         catalogv1.IntegrationBinding
	RetryPolicyJSON string
	ProfileHash     string
}

func NewAuthority(view catalogv1.IntegrationBindingView) (*Authority, error) {
	if catalogv1.ValidateIntegrationBindingView(view) != nil {
		return nil, ErrInvalidInput
	}
	bindings := make(map[string]catalogv1.IntegrationBinding, len(view.Bindings))
	for _, binding := range view.Bindings {
		bindings[integrationBindingKey(binding)] = binding
	}
	return &Authority{view: cloneBindingView(view), bindings: bindings}, nil
}

func (a *Authority) Scope() Scope {
	if a == nil {
		return Scope{}
	}
	return Scope{
		OrgID: a.view.OrgID, AppInstallationID: a.view.AppInstallationID,
		AuthorizedWorkloadID: a.view.AuthorizedWorkloadID,
		CatalogRevision:      a.view.Lineage.CatalogRevision,
	}
}

func (a *Authority) Resolve(ctx context.Context, db *sql.DB, command Command) (ResolvedAuthority, error) {
	if a == nil || db == nil || ctx == nil || a.Scope() != command.Scope ||
		outboundintentv1.ValidateIntent(command.Intent) != nil ||
		command.LinkKey != outboundLinkKey(command.Scope, command.Intent) {
		return ResolvedAuthority{}, ErrInvalidInput
	}
	var push *catalogv1.IntegrationBinding
	var profile *catalogv1.IntegrationBinding
	var mappings []catalogv1.IntegrationBinding
	for _, reference := range command.Intent.Bindings {
		binding, found := a.bindings[outboundReferenceKey(reference)]
		if !found || !sameBindingReference(binding, reference) {
			return ResolvedAuthority{}, ErrIdentityConflict
		}
		copy := binding
		switch binding.Kind {
		case catalogv1.BindingConnectorPushContract:
			if push != nil {
				return ResolvedAuthority{}, ErrIdentityConflict
			}
			push = &copy
		case catalogv1.BindingConnectorCalloutProfile:
			if profile != nil {
				return ResolvedAuthority{}, ErrIdentityConflict
			}
			profile = &copy
		case catalogv1.BindingConnectorFieldMapping:
			mappings = append(mappings, copy)
		default:
			return ResolvedAuthority{}, ErrIdentityConflict
		}
	}
	if push == nil || profile == nil || len(mappings) == 0 {
		return ResolvedAuthority{}, ErrIdentityConflict
	}
	contract, err := loadExactPushContract(ctx, db, *push)
	if err != nil {
		return ResolvedAuthority{}, &PreparationError{Code: "binding_unavailable", Retryable: authorityLoadRetryable(err), Cause: err}
	}
	if err := validateContractForIntent(contract, *push, *profile, command.Intent); err != nil {
		return ResolvedAuthority{}, &PreparationError{Code: "binding_mismatch", Cause: err}
	}
	if _, ok := bindingLocalName(profile.OwnerPack, profile.BusinessKey); !ok {
		return ResolvedAuthority{}, &PreparationError{Code: "binding_mismatch", Cause: ErrIdentityConflict}
	}
	resolvedProfile, err := loadExactProfileAuthority(ctx, db, *profile)
	if err != nil {
		code := "profile_unavailable"
		if errors.Is(err, ErrIdentityConflict) {
			code = "profile_mismatch"
		}
		return ResolvedAuthority{}, &PreparationError{
			Code: code, Retryable: authorityLoadRetryable(err), Cause: err,
		}
	}
	mappingFingerprint := digestBindings(mappings)
	contractJSON, err := json.Marshal(contract)
	if err != nil {
		return ResolvedAuthority{}, &PreparationError{Code: "binding_mismatch", Cause: err}
	}
	return ResolvedAuthority{
		Contract: contract, ContractBinding: *push, Profile: resolvedProfile.Profile,
		ProfileBinding: *profile, MappingBindings: mappings,
		MappingFingerprint: mappingFingerprint,
		BodyFingerprint:    digestStrings(string(contractJSON), mappingFingerprint),
		RetryPolicyJSON:    resolvedProfile.RetryPolicyJSON,
		ProfileHash:        resolvedProfile.ProfileHash,
	}, nil
}

func (a *Authority) resolveNamedProfile(
	ctx context.Context,
	db *sql.DB,
	ownerPack string,
	profileName string,
) (resolvedProfileAuthority, error) {
	if a == nil || db == nil || ctx == nil || strings.TrimSpace(ownerPack) == "" ||
		strings.TrimSpace(profileName) == "" {
		return resolvedProfileAuthority{}, ErrInvalidInput
	}
	var selected *catalogv1.IntegrationBinding
	for index := range a.view.Bindings {
		binding := a.view.Bindings[index]
		name, ok := bindingLocalName(binding.OwnerPack, binding.BusinessKey)
		if binding.Kind != catalogv1.BindingConnectorCalloutProfile ||
			binding.OwnerPack != ownerPack || !ok || name != profileName {
			continue
		}
		if selected != nil {
			return resolvedProfileAuthority{}, ErrIdentityConflict
		}
		copy := binding
		selected = &copy
	}
	if selected == nil {
		return resolvedProfileAuthority{}, ErrIdentityConflict
	}
	return loadExactProfileAuthority(ctx, db, *selected)
}

func loadExactProfileAuthority(
	ctx context.Context,
	db *sql.DB,
	binding catalogv1.IntegrationBinding,
) (resolvedProfileAuthority, error) {
	profileName, ok := bindingLocalName(binding.OwnerPack, binding.BusinessKey)
	if !ok || binding.Kind != catalogv1.BindingConnectorCalloutProfile {
		return resolvedProfileAuthority{}, ErrIdentityConflict
	}
	if err := requireExactActivePackGeneration(ctx, db, binding); err != nil {
		return resolvedProfileAuthority{}, err
	}
	profile, err := connector_callout.LoadProfileForPackInstall(
		ctx, db, binding.OwnerPack, binding.PackInstallationID, profileName,
	)
	if err != nil {
		return resolvedProfileAuthority{}, err
	}
	if profile == nil || profile.OwnerPack != binding.OwnerPack ||
		profile.InstalledPackID != binding.PackInstallationID ||
		profile.ProfileName != profileName ||
		!containsString(binding.CredentialRefs, profile.CredentialRef) {
		return resolvedProfileAuthority{}, ErrIdentityConflict
	}
	retryJSON, err := json.Marshal(profile.RetryPolicy)
	if err != nil {
		return resolvedProfileAuthority{}, err
	}
	return resolvedProfileAuthority{
		Profile: profile, Binding: binding, RetryPolicyJSON: string(retryJSON),
		ProfileHash: digestStrings(
			profile.OwnerPack, profile.ProfileName, profile.InstalledPackID,
			profile.CredentialRef, profile.EndpointClass, string(retryJSON),
		),
	}, nil
}

func authorityLoadRetryable(err error) bool {
	if err == nil {
		return false
	}
	return !errors.Is(err, sql.ErrNoRows) &&
		!errors.Is(err, ErrInvalidInput) &&
		!errors.Is(err, ErrIdentityConflict) &&
		!errors.Is(err, ErrDependency) &&
		!errors.Is(err, connector_callout.ErrProfileNotFound) &&
		!errors.Is(err, connector_callout.ErrProfileUnavailable)
}

func requireExactActivePackGeneration(ctx context.Context, db *sql.DB, binding catalogv1.IntegrationBinding) error {
	if ctx == nil || db == nil || strings.TrimSpace(binding.OwnerPack) == "" ||
		strings.TrimSpace(binding.PackInstallationID) == "" || strings.TrimSpace(binding.PackPayloadDigest) == "" {
		return ErrInvalidInput
	}
	var present int
	err := db.QueryRowContext(ctx, `
SELECT 1
  FROM _System_PackGeneration
 WHERE PackName = ? AND PackInstallId = ? AND Status = 'active'
   AND AppliedPayloadHash = ? AND DesiredPayloadHash = AppliedPayloadHash
   AND COALESCE(IsDeleted, 0) = 0
 LIMIT 1`, binding.OwnerPack, binding.PackInstallationID, binding.PackPayloadDigest).Scan(&present)
	if errors.Is(err, sql.ErrNoRows) {
		return ErrDependency
	}
	if err != nil {
		return err
	}
	if present != 1 {
		return ErrIdentityConflict
	}
	return nil
}

type persistedRuntimeRow struct {
	Kind        string `json:"kind"`
	BusinessKey string `json:"businessKey"`
	Projection  string `json:"projection,omitempty"`
}

func loadExactPushContract(ctx context.Context, db *sql.DB, binding catalogv1.IntegrationBinding) (connectorv1.ConnectorPushDurableMetadataContract, error) {
	rows, err := loadExactRuntimeRows(ctx, db, binding)
	if err != nil {
		return connectorv1.ConnectorPushDurableMetadataContract{}, err
	}
	projection, err := exactRuntimeProjection(rows, "connector_push_contract", binding.BusinessKey)
	if err != nil {
		return connectorv1.ConnectorPushDurableMetadataContract{}, err
	}
	var contract connectorv1.ConnectorPushDurableMetadataContract
	if err := decodeStrictJSON([]byte(projection), &contract); err != nil {
		return connectorv1.ConnectorPushDurableMetadataContract{}, ErrIdentityConflict
	}
	return contract, nil
}

func loadExactRuntimeRows(ctx context.Context, db *sql.DB, binding catalogv1.IntegrationBinding) ([]persistedRuntimeRow, error) {
	var declaration sql.NullString
	err := db.QueryRowContext(ctx, `
SELECT RuntimeStartupShallowDeclarations
  FROM _System_PackGeneration
 WHERE PackName = ? AND PackInstallId = ? AND Status = 'active'
   AND AppliedPayloadHash = ? AND DesiredPayloadHash = AppliedPayloadHash
   AND COALESCE(IsDeleted, 0) = 0
 LIMIT 1`, binding.OwnerPack, binding.PackInstallationID, binding.PackPayloadDigest).Scan(&declaration)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrDependency
	}
	if err != nil {
		return nil, err
	}
	if !declaration.Valid || strings.TrimSpace(declaration.String) == "" {
		return nil, ErrDependency
	}
	var rows []persistedRuntimeRow
	if err := decodeStrictJSON([]byte(declaration.String), &rows); err != nil || len(rows) == 0 {
		return nil, ErrIdentityConflict
	}
	return rows, nil
}

func exactRuntimeProjection(rows []persistedRuntimeRow, kind, businessKey string) (string, error) {
	var selected *persistedRuntimeRow
	for index := range rows {
		row := rows[index]
		if row.Kind != kind || row.BusinessKey != businessKey {
			continue
		}
		if selected != nil {
			return "", ErrIdentityConflict
		}
		copy := row
		selected = &copy
	}
	if selected == nil || strings.TrimSpace(selected.Projection) == "" {
		return "", ErrIdentityConflict
	}
	return selected.Projection, nil
}

func validateContractForIntent(
	contract connectorv1.ConnectorPushDurableMetadataContract,
	push catalogv1.IntegrationBinding,
	profile catalogv1.IntegrationBinding,
	intent outboundintentv1.Intent,
) error {
	expectedBusinessKey := strings.Join([]string{
		contract.ConnectorOwnerKey, contract.FlowDeveloperName, contract.FlowActionName,
		contract.SourceObject, contract.DestinationSlotID,
	}, "/")
	profileOwner := strings.TrimSpace(contract.ProfileOwnerPack)
	if profileOwner == "" {
		profileOwner = contract.ConnectorOwnerKey
	}
	if push.BusinessKey != expectedBusinessKey || contract.ConnectorOwnerKey != intent.ConnectorOwnerKey ||
		contract.FlowDeveloperName != intent.FlowDeveloperName ||
		contract.FlowActionName != intent.FlowActionName || contract.ActionSlotID != intent.ActionSlotID ||
		contract.ActionType != connectorPushActionType || contract.SourceObject != intent.SourceObject ||
		contract.DestinationSlotID != intent.DestinationSlotID ||
		profile.OwnerPack != profileOwner || profile.BusinessKey != profileOwner+"/"+contract.ProfileName {
		return ErrIdentityConflict
	}
	for _, value := range []string{
		contract.TargetObject, contract.ProfileName, contract.CreatePathTemplate,
		contract.UpdatePathTemplate, contract.ResponseIDPath, contract.RemoteIDField,
	} {
		if strings.TrimSpace(value) == "" || value != strings.TrimSpace(value) {
			return ErrIdentityConflict
		}
	}
	return nil
}

func cloneBindingView(view catalogv1.IntegrationBindingView) catalogv1.IntegrationBindingView {
	copy := view
	copy.Bindings = make([]catalogv1.IntegrationBinding, len(view.Bindings))
	for index := range view.Bindings {
		copy.Bindings[index] = view.Bindings[index]
		copy.Bindings[index].CredentialRefs = append([]string(nil), view.Bindings[index].CredentialRefs...)
	}
	return copy
}

func integrationBindingKey(binding catalogv1.IntegrationBinding) string {
	return strings.Join([]string{
		string(binding.Kind), binding.OwnerPack, binding.PackInstallationID,
		binding.PackPayloadDigest, binding.BusinessKey,
		strconv.FormatUint(binding.DefinitionVersion, 10),
	}, "\x00")
}

func outboundReferenceKey(reference outboundintentv1.BindingReference) string {
	return strings.Join([]string{
		string(reference.Kind), reference.OwnerPack, reference.PackInstallationID,
		reference.PackPayloadDigest, reference.BusinessKey,
		strconv.FormatUint(reference.DefinitionVersion, 10),
	}, "\x00")
}

func sameBindingReference(binding catalogv1.IntegrationBinding, reference outboundintentv1.BindingReference) bool {
	return integrationBindingKey(binding) == outboundReferenceKey(reference)
}

func bindingLocalName(owner, businessKey string) (string, bool) {
	prefix := owner + "/"
	if !strings.HasPrefix(businessKey, prefix) {
		return "", false
	}
	name := strings.TrimPrefix(businessKey, prefix)
	return name, name != "" && !strings.Contains(name, "/")
}

func containsString(values []string, target string) bool {
	for _, value := range values {
		if value == target {
			return true
		}
	}
	return false
}

func digestBindings(bindings []catalogv1.IntegrationBinding) string {
	keys := make([]string, len(bindings))
	for index := range bindings {
		keys[index] = integrationBindingKey(bindings[index])
	}
	sort.Strings(keys)
	return digestStrings(keys...)
}

func digestStrings(values ...string) string {
	sum := sha256.Sum256([]byte(strings.Join(values, "\x1f")))
	return hex.EncodeToString(sum[:])
}

func decodeStrictJSON(raw []byte, target any) error {
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	decoder.UseNumber()
	if err := decoder.Decode(target); err != nil {
		return err
	}
	var trailing any
	if err := decoder.Decode(&trailing); err != io.EOF {
		if err == nil {
			return fmt.Errorf("unexpected trailing JSON")
		}
		return err
	}
	return nil
}
