package memory

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"strings"
	"time"

	"gorm.io/gorm"
)

const (
	AssetTypeMemory = "memory"
	AssetTypeSkill  = "skill"
	AssetTypeKB     = "kb"

	AgentAssetAccessSourceOutbox = "outbox"

	AgentAssetAccessOutboxStatusPending   = "pending"
	AgentAssetAccessOutboxStatusLeased    = "leased"
	AgentAssetAccessOutboxStatusDelivered = "delivered"
	AgentAssetAccessOutboxStatusFailed    = "failed"

	defaultAgentAssetAccessMaxAttempts = 10
	maxAgentAssetAccessLastError       = 4096
)

// AgentAssetAccessChangeInput describes a single access-change event to append
// to the durable outbox.
type AgentAssetAccessChangeInput struct {
	WorkspaceID string
	AgentID     string
	AssetType   string
	OccurredAt  time.Time
}

// AgentAssetAccessAttemptResult reports whether a leased event accepted a
// delivery-failure update and whether retries are now exhausted.
type AgentAssetAccessAttemptResult struct {
	Updated bool
	Failed  bool
}

// EnqueueAgentAssetAccessChange appends a durable access-change intent. Callers
// should invoke this from the same transaction as the access mutation that made
// the event necessary.
func (s *Store) EnqueueAgentAssetAccessChange(ctx context.Context, input AgentAssetAccessChangeInput) (AgentAssetAccessChangeOutbox, error) {
	if err := s.ensureDB(); err != nil {
		return AgentAssetAccessChangeOutbox{}, err
	}
	workspaceID := strings.TrimSpace(input.WorkspaceID)
	agentID := strings.TrimSpace(input.AgentID)
	assetType := strings.TrimSpace(input.AssetType)
	if workspaceID == "" {
		return AgentAssetAccessChangeOutbox{}, fmt.Errorf("agent asset access outbox: workspace id is required")
	}
	if agentID == "" {
		return AgentAssetAccessChangeOutbox{}, fmt.Errorf("agent asset access outbox: agent id is required")
	}
	if assetType == "" {
		return AgentAssetAccessChangeOutbox{}, fmt.Errorf("agent asset access outbox: asset type is required")
	}
	now := input.OccurredAt.UTC()
	if now.IsZero() {
		now = time.Now().UTC()
	}
	version, err := s.nextAgentAssetAccessVersion(ctx, workspaceID, agentID, assetType, now)
	if err != nil {
		return AgentAssetAccessChangeOutbox{}, err
	}
	row := AgentAssetAccessChangeOutbox{
		ID:            newAgentAssetAccessEventID(),
		WorkspaceID:   workspaceID,
		AgentID:       agentID,
		AssetType:     assetType,
		Version:       version,
		Source:        AgentAssetAccessSourceOutbox,
		Status:        AgentAssetAccessOutboxStatusPending,
		MaxAttempts:   defaultAgentAssetAccessMaxAttempts,
		NextAttemptAt: now,
		OccurredAt:    now,
		CreatedAt:     now,
		UpdatedAt:     now,
	}
	if err := s.db.WithContext(ctx).Create(&row).Error; err != nil {
		return AgentAssetAccessChangeOutbox{}, fmt.Errorf("enqueue agent asset access change: %w", err)
	}
	return row, nil
}

func (s *Store) nextAgentAssetAccessVersion(ctx context.Context, workspaceID, agentID, assetType string, fallback time.Time) (int64, error) {
	var maxVersion int64
	if err := s.db.WithContext(ctx).Model(&AgentAssetAccessChangeOutbox{}).
		Where("workspace_id = ? AND agent_id = ? AND asset_type = ?", workspaceID, agentID, assetType).
		Select("COALESCE(MAX(version), 0)").
		Scan(&maxVersion).Error; err != nil {
		return 0, fmt.Errorf("read agent asset access version: %w", err)
	}
	version := maxVersion + 1
	if fallbackVersion := fallback.UnixNano(); fallbackVersion > version {
		version = fallbackVersion
	}
	return version, nil
}

// LeaseDueAgentAssetAccessChanges claims due access-change outbox rows for
// delivery. Expired leases are recoverable and can be claimed again.
func (s *Store) LeaseDueAgentAssetAccessChanges(ctx context.Context, now time.Time, leaseOwner string, leaseUntil time.Time, limit int, workspaceIDs []string) ([]AgentAssetAccessChangeOutbox, error) {
	if err := s.ensureDB(); err != nil {
		return nil, err
	}
	leaseOwner = strings.TrimSpace(leaseOwner)
	if leaseOwner == "" {
		return nil, fmt.Errorf("agent asset access outbox: lease owner is required")
	}
	if limit <= 0 {
		limit = 25
	}
	if now.IsZero() {
		now = time.Now().UTC()
	} else {
		now = now.UTC()
	}
	if leaseUntil.IsZero() || !leaseUntil.After(now) {
		leaseUntil = now.Add(time.Minute)
	} else {
		leaseUntil = leaseUntil.UTC()
	}
	workspaceIDs = normalizeAgentAssetAccessWorkspaceIDs(workspaceIDs)
	var leased []AgentAssetAccessChangeOutbox
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var candidates []AgentAssetAccessChangeOutbox
		query := tx.Where("((status = ? AND next_attempt_at <= ?) OR (status = ? AND lease_expires_at IS NOT NULL AND lease_expires_at <= ?))",
			AgentAssetAccessOutboxStatusPending, now, AgentAssetAccessOutboxStatusLeased, now)
		if len(workspaceIDs) > 0 {
			query = query.Where("workspace_id IN ?", workspaceIDs)
		}
		err := query.
			Order("next_attempt_at ASC").Order("id ASC").
			Limit(limit).
			Find(&candidates).Error
		if err != nil {
			return fmt.Errorf("list due agent asset access outbox: %w", err)
		}
		for _, candidate := range candidates {
			result := tx.Model(&AgentAssetAccessChangeOutbox{}).
				Where("id = ?", candidate.ID).
				Where("((status = ? AND next_attempt_at <= ?) OR (status = ? AND lease_expires_at IS NOT NULL AND lease_expires_at <= ?))",
					AgentAssetAccessOutboxStatusPending, now, AgentAssetAccessOutboxStatusLeased, now).
				Updates(map[string]any{
					"status":           AgentAssetAccessOutboxStatusLeased,
					"lease_owner":      leaseOwner,
					"lease_expires_at": leaseUntil,
					"updated_at":       now,
				})
			if result.Error != nil {
				return fmt.Errorf("lease agent asset access outbox: %w", result.Error)
			}
			if result.RowsAffected == 0 {
				continue
			}
			candidate.Status = AgentAssetAccessOutboxStatusLeased
			candidate.LeaseOwner = leaseOwner
			candidate.LeaseExpiresAt = &leaseUntil
			candidate.UpdatedAt = now
			leased = append(leased, candidate)
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return leased, nil
}

func normalizeAgentAssetAccessWorkspaceIDs(workspaceIDs []string) []string {
	if len(workspaceIDs) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(workspaceIDs))
	out := make([]string, 0, len(workspaceIDs))
	for _, workspaceID := range workspaceIDs {
		trimmed := strings.TrimSpace(workspaceID)
		if trimmed == "" {
			continue
		}
		if _, ok := seen[trimmed]; ok {
			continue
		}
		seen[trimmed] = struct{}{}
		out = append(out, trimmed)
	}
	return out
}

// GetAgentAssetAccessChange loads one outbox row by event id.
func (s *Store) GetAgentAssetAccessChange(ctx context.Context, eventID string) (AgentAssetAccessChangeOutbox, error) {
	if err := s.ensureDB(); err != nil {
		return AgentAssetAccessChangeOutbox{}, err
	}
	var row AgentAssetAccessChangeOutbox
	err := s.db.WithContext(ctx).Where("id = ?", strings.TrimSpace(eventID)).First(&row).Error
	return row, err
}

// MarkAgentAssetAccessChangeDelivered marks a leased event as delivered.
func (s *Store) MarkAgentAssetAccessChangeDelivered(ctx context.Context, eventID, leaseOwner string, deliveredAt time.Time) (bool, error) {
	if err := s.ensureDB(); err != nil {
		return false, err
	}
	eventID = strings.TrimSpace(eventID)
	leaseOwner = strings.TrimSpace(leaseOwner)
	if eventID == "" {
		return false, fmt.Errorf("agent asset access outbox: event id is required")
	}
	if leaseOwner == "" {
		return false, fmt.Errorf("agent asset access outbox: lease owner is required")
	}
	if deliveredAt.IsZero() {
		deliveredAt = time.Now().UTC()
	} else {
		deliveredAt = deliveredAt.UTC()
	}
	result := s.db.WithContext(ctx).Model(&AgentAssetAccessChangeOutbox{}).
		Where("id = ? AND status = ? AND lease_owner = ?", eventID, AgentAssetAccessOutboxStatusLeased, leaseOwner).
		Updates(map[string]any{
			"status":           AgentAssetAccessOutboxStatusDelivered,
			"lease_owner":      "",
			"lease_expires_at": nil,
			"delivered_at":     deliveredAt,
			"last_error":       "",
			"updated_at":       deliveredAt,
		})
	if result.Error != nil {
		return false, fmt.Errorf("mark agent asset access delivered: %w", result.Error)
	}
	return result.RowsAffected > 0, nil
}

// MarkAgentAssetAccessChangeAttemptFailed records a delivery failure for a
// leased event and either schedules another retry or permanently fails it.
func (s *Store) MarkAgentAssetAccessChangeAttemptFailed(ctx context.Context, event AgentAssetAccessChangeOutbox, leaseOwner string, lastError string, nextAttemptAt time.Time, now time.Time) (AgentAssetAccessAttemptResult, error) {
	if err := s.ensureDB(); err != nil {
		return AgentAssetAccessAttemptResult{}, err
	}
	leaseOwner = strings.TrimSpace(leaseOwner)
	if strings.TrimSpace(event.ID) == "" {
		return AgentAssetAccessAttemptResult{}, fmt.Errorf("agent asset access outbox: event id is required")
	}
	if leaseOwner == "" {
		return AgentAssetAccessAttemptResult{}, fmt.Errorf("agent asset access outbox: lease owner is required")
	}
	if now.IsZero() {
		now = time.Now().UTC()
	} else {
		now = now.UTC()
	}
	if nextAttemptAt.IsZero() || nextAttemptAt.Before(now) {
		nextAttemptAt = now
	} else {
		nextAttemptAt = nextAttemptAt.UTC()
	}
	attempts := event.Attempts + 1
	maxAttempts := event.MaxAttempts
	if maxAttempts <= 0 {
		maxAttempts = defaultAgentAssetAccessMaxAttempts
	}
	status := AgentAssetAccessOutboxStatusPending
	failed := false
	if attempts >= maxAttempts {
		status = AgentAssetAccessOutboxStatusFailed
		failed = true
	}
	result := s.db.WithContext(ctx).Model(&AgentAssetAccessChangeOutbox{}).
		Where("id = ? AND status = ? AND lease_owner = ?", event.ID, AgentAssetAccessOutboxStatusLeased, leaseOwner).
		Updates(map[string]any{
			"status":           status,
			"attempts":         attempts,
			"next_attempt_at":  nextAttemptAt,
			"lease_owner":      "",
			"lease_expires_at": nil,
			"last_error":       trimAgentAssetAccessLastError(lastError),
			"updated_at":       now,
		})
	if result.Error != nil {
		return AgentAssetAccessAttemptResult{}, fmt.Errorf("mark agent asset access attempt failed: %w", result.Error)
	}
	if result.RowsAffected == 0 {
		return AgentAssetAccessAttemptResult{}, nil
	}
	return AgentAssetAccessAttemptResult{Updated: true, Failed: failed}, nil
}

func trimAgentAssetAccessLastError(value string) string {
	value = strings.TrimSpace(value)
	if len(value) <= maxAgentAssetAccessLastError {
		return value
	}
	return value[:maxAgentAssetAccessLastError]
}

func newAgentAssetAccessEventID() string {
	var bytes [8]byte
	if _, err := rand.Read(bytes[:]); err == nil {
		return "aaac_" + hex.EncodeToString(bytes[:])
	}
	return fmt.Sprintf("aaac_%d", time.Now().UnixNano())
}
