package records

import (
	"context"
	"fmt"
	"time"

	appErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

// generateAuditLogs is the legacy _System_AuditLog fallback used only while
// audit v2 is disabled. It must share recordAuditFieldChanges with v2 so the
// remaining bridge has one field-filter owner. Death condition: delete this
// legacy writer once v2 is default-on and every reader consumes the v2 history
// projection.
func (ps *PersistenceService) generateAuditLogs(txCtx context.Context, schema *models.ObjectMetadata, objectName, id string, effectiveUpdates, oldRecord models.SObject, currentUser *models.UserSession) error {
	changes := ps.recordAuditFieldChanges(txCtx, schema, objectName, effectiveUpdates, oldRecord)
	if len(changes) == 0 {
		return nil
	}
	if err := ps.reserveRecordAuditSequences(txCtx, changes); err != nil {
		return appErrors.NewInternalError("failed to reserve audit history sequence", err)
	}
	changedByID := recordAuditChangedByID(currentUser)
	if ps.txManager == nil || !ps.txManager.IsTransactionActive(txCtx) {
		return appErrors.NewInternalError("audit event time requires an active transaction", nil)
	}
	clock, ok := ps.txManager.(interface {
		ReadTransactionTime(context.Context) (time.Time, error)
	})
	if !ok || clock == nil {
		return appErrors.NewInternalError("audit event time requires a transaction-bound DB clock", nil)
	}
	eventAt, err := clock.ReadTransactionTime(txCtx)
	if err != nil {
		return appErrors.NewInternalError("failed to read audit event time", err)
	}
	now := time.Now()
	entries := make([]models.SObject, 0, len(changes))
	for _, change := range changes {
		auditID := fmt.Sprintf("%s-%d-%s", id, time.Now().UnixNano(), change.fieldName)
		oldValue := change.oldValue
		newValue := change.newValue
		auditEntryStruct := models.SystemAuditLog{
			ID:                   auditID,
			ObjectAPIName:        objectName,
			RecordID:             id,
			FieldName:            change.fieldName,
			OldValue:             &oldValue,
			NewValue:             &newValue,
			ChangedByID:          changedByID,
			ChangedAt:            now,
			FieldHistoryEligible: change.fieldHistoryEligible,
			CreatedDate:          now,
			LastModifiedDate:     now,
		}
		entry := auditEntryStruct.ToSObject()
		// Keep one concrete temporal ordering token for every field row in
		// this event. BulkInsert binds values, so DB expressions must be
		// resolved once through the active transaction before rows are built.
		entry[constants.FieldSysAuditLog_EventAt] = eventAt
		if change.historySequence != nil {
			entry[constants.FieldSysAuditLog_HistorySequence] = *change.historySequence
		}
		entries = append(entries, entry)
	}
	if len(entries) == 1 {
		if err := ps.repo.Insert(txCtx, constants.TableAuditLog, entries[0]); err != nil {
			return appErrors.NewInternalError("failed to write audit log", err)
		}
		return nil
	}
	columns := make([]string, 0, len(entries[0]))
	for k := range entries[0] {
		columns = append(columns, k)
	}
	const (
		auditBulkChunkSize  = 10
		auditBulkMaxPayload = 512 * 1024 // 512KB per chunk
	)
	var chunk []models.SObject
	chunkBytes := 0
	for _, entry := range entries {
		entrySize := estimateAuditEntrySize(entry)
		if len(chunk) > 0 && (len(chunk) >= auditBulkChunkSize || chunkBytes+entrySize > auditBulkMaxPayload) {
			if err := ps.flushAuditChunk(txCtx, chunk, columns); err != nil {
				return err
			}
			chunk = chunk[:0]
			chunkBytes = 0
		}
		chunk = append(chunk, entry)
		chunkBytes += entrySize
	}
	if len(chunk) > 0 {
		if err := ps.flushAuditChunk(txCtx, chunk, columns); err != nil {
			return err
		}
	}
	return nil
}

func (ps *PersistenceService) flushAuditChunk(txCtx context.Context, chunk []models.SObject, columns []string) error {
	if len(chunk) == 1 {
		if err := ps.repo.Insert(txCtx, constants.TableAuditLog, chunk[0]); err != nil {
			return appErrors.NewInternalError("failed to write audit log", err)
		}
		return nil
	}
	if err := ps.repo.BulkInsert(txCtx, constants.TableAuditLog, chunk, columns, nil); err != nil {
		return appErrors.NewInternalError("failed to write audit logs", err)
	}
	return nil
}

func estimateAuditEntrySize(entry models.SObject) int {
	size := 0
	for _, v := range entry {
		if s, ok := v.(string); ok {
			size += len(s)
		}
	}
	return size
}
