package automation

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"strings"

	"github.com/nexuscrm/shared/pkg/models"
	contractconstants "github.com/pingcap-inc/nexuscrm-contract/pkg/constants"
	contractmodels "github.com/pingcap-inc/nexuscrm-contract/pkg/models"
)

const connectorInboundContextAuditElementID = "$ConnectorInboundContext"
const connectorInboundTerminalCauseAuditElementID = "$ConnectorInboundTerminalCause"

type connectorInboundAuditLink struct {
	WorkItemID         string `json:"workItemId,omitempty"`
	EventID            string `json:"eventId"`
	Attempt            int    `json:"attempt,omitempty"`
	ObjectKind         string `json:"objectKind,omitempty"`
	SourceIDHash       string `json:"sourceIdHash,omitempty"`
	HydratedRecordHash string `json:"hydratedRecordHash,omitempty"`
	HydratedFieldCount int    `json:"hydratedFieldCount,omitempty"`
}

type connectorInboundAuditPayload struct {
	Namespace string                      `json:"namespace"`
	Links     []connectorInboundAuditLink `json:"links"`
}

func connectorInboundExecutionAuditRow(ctx context.Context, interview *models.FlowInterview) (Row, bool) {
	if interview == nil {
		return Row{}, false
	}
	trace, ok := contractconstants.ConnectorInboundTraceFromContext(ctx)
	if !ok {
		return Row{}, false
	}
	links := make([]connectorInboundAuditLink, 0, 1+len(trace.Related))
	links = append(links, connectorInboundAuditLink{
		WorkItemID:         trace.WorkItemID,
		EventID:            trace.EventID,
		Attempt:            trace.Attempt,
		ObjectKind:         trace.ObjectKind,
		SourceIDHash:       trace.SourceIDHash,
		HydratedRecordHash: trace.HydratedRecordHash,
		HydratedFieldCount: trace.HydratedFieldCount,
	})
	for _, related := range trace.Related {
		if strings.TrimSpace(related.EventID) == "" {
			continue
		}
		links = append(links, connectorInboundAuditLink{
			WorkItemID:         related.WorkItemID,
			EventID:            related.EventID,
			Attempt:            related.Attempt,
			ObjectKind:         related.ObjectKind,
			SourceIDHash:       related.SourceIDHash,
			HydratedRecordHash: related.HydratedRecordHash,
			HydratedFieldCount: related.HydratedFieldCount,
		})
	}
	encoded, err := json.Marshal(connectorInboundAuditPayload{
		Namespace: trace.Namespace,
		Links:     links,
	})
	if err != nil {
		return Row{}, false
	}
	return Row{
		FlowExecutionId:  interview.Id,
		FlowDefinitionId: auditFlowDefinitionIdFromCtx(ctx),
		FlowVersionId:    interview.FlowVersionId,
		ElementId:        connectorInboundContextAuditElementID,
		ElementType:      "ConnectorInboundContext",
		Status:           "success",
		InputJSON:        string(encoded),
	}, true
}

type connectorInboundTerminalCausePayload struct {
	PresentedErrorCode string                                    `json:"presentedErrorCode,omitempty"`
	CauseCode          string                                    `json:"causeCode"`
	CauseCategory      contractmodels.FlowExecutionCauseCategory `json:"causeCategory"`
	CauseElementType   string                                    `json:"causeElementType,omitempty"`
	CauseElementName   string                                    `json:"causeElementName,omitempty"`
	Retriable          bool                                      `json:"retriable"`
}

func connectorInboundTerminalCauseAuditRow(ctx context.Context, interview *models.FlowInterview, cause *contractmodels.FlowExecutionCause, presentedErrorCode string) (Row, bool) {
	if interview == nil || cause == nil {
		return Row{}, false
	}
	if _, ok := contractconstants.ConnectorInboundTraceFromContext(ctx); !ok {
		return Row{}, false
	}
	encoded, err := json.Marshal(connectorInboundTerminalCausePayload{
		PresentedErrorCode: strings.TrimSpace(presentedErrorCode),
		CauseCode:          strings.TrimSpace(cause.Code),
		CauseCategory:      cause.Category,
		CauseElementType:   strings.TrimSpace(cause.ElementType),
		CauseElementName:   strings.TrimSpace(cause.ElementName),
		Retriable:          cause.Retriable,
	})
	if err != nil {
		return Row{}, false
	}
	return Row{
		FlowExecutionId:  interview.Id,
		FlowDefinitionId: auditFlowDefinitionIdFromCtx(ctx),
		FlowVersionId:    interview.FlowVersionId,
		ElementId:        connectorInboundTerminalCauseAuditElementID,
		ElementType:      "ConnectorInboundTerminalCause",
		Status:           "error",
		ErrorPayload:     string(encoded),
	}, true
}

func emitConnectorInboundTerminalCauseAudit(ctx context.Context, interview *models.FlowInterview, cause *contractmodels.FlowExecutionCause, presentedErrorCode string) {
	if row, ok := connectorInboundTerminalCauseAuditRow(ctx, interview, cause, presentedErrorCode); ok {
		emitFlowAudit(ctx, row, 1.0)
	}
}

func emitConnectorInboundExecutionAudit(ctx context.Context, interview *models.FlowInterview) {
	if row, ok := connectorInboundExecutionAuditRow(ctx, interview); ok {
		emitFlowAudit(ctx, row, 1.0)
	}
}

func recordLookupAuditInputJSON(filter string, queryLimit int) string {
	payload := struct {
		FilterHash string `json:"filterHash"`
		QueryLimit int    `json:"queryLimit"`
	}{
		FilterHash: redactedFlowAuditHash("record-lookup-filter", filter),
		QueryLimit: queryLimit,
	}
	encoded, _ := json.Marshal(payload)
	return string(encoded)
}

func recordLookupAuditOutputJSON(result *models.QueryResult, source string) string {
	payload := struct {
		RowCount        int      `json:"rowCount"`
		RecordIDHashes  []string `json:"recordIdHashes,omitempty"`
		HashesTruncated bool     `json:"hashesTruncated,omitempty"`
		Source          string   `json:"source,omitempty"`
	}{Source: strings.TrimSpace(source)}
	if result != nil {
		payload.RowCount = len(result.Records)
		for _, record := range result.Records {
			if len(payload.RecordIDHashes) == 2 {
				payload.HashesTruncated = len(result.Records) > 2
				break
			}
			id, _ := record["Id"].(string)
			if id = strings.TrimSpace(id); id != "" {
				payload.RecordIDHashes = append(payload.RecordIDHashes, redactedFlowAuditHash("record-lookup-record-id", id))
			}
		}
	}
	encoded, _ := json.Marshal(payload)
	return string(encoded)
}

func redactedFlowAuditHash(domain, value string) string {
	if value == "" {
		return ""
	}
	hash := sha256.New()
	_, _ = hash.Write([]byte(domain))
	_, _ = hash.Write([]byte{0})
	_, _ = hash.Write([]byte(value))
	return "sha256:" + hex.EncodeToString(hash.Sum(nil))
}
