package m2mcore

import (
	"context"
	"errors"
	"sync"
	"time"

	"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m"
)

// ErrNotFound возвращается, когда сделка не найдена.
var ErrNotFound = errors.New("m2mcore: сделка не найдена")

// Filter описывает фильтры выборки сделок.
type Filter struct {
	State       *State
	InvestorID  string
	CreatedFrom *time.Time
	CreatedTo   *time.Time
	Limit       int
	Offset      int
}

// Repository — порт хранилища сделок.
type Repository interface {
	// Create вставляет сделку с идемпотентностью по GUID: повторный вызов
	// для существующего GUID возвращает уже существующую сделку (без
	// модификации).
	Create(ctx context.Context, deal *Deal) (*Deal, error)
	// GetByGUID находит сделку по M2M GUID.
	GetByGUID(ctx context.Context, guid m2m.UUID) (*Deal, error)
	// GetByID находит сделку по внутреннему UUID.
	GetByID(ctx context.Context, id string) (*Deal, error)
	// Update сохраняет изменения сделки.
	Update(ctx context.Context, deal *Deal) error
	// List возвращает сделки по фильтру.
	List(ctx context.Context, f Filter) ([]*Deal, error)
	// AppendEvent добавляет аудит-событие к сделке.
	AppendEvent(ctx context.Context, dealID string, ev Event) error
}

// MemoryRepository — in-memory реализация Repository для тестов и
// dev-стенда без PostgreSQL.
type MemoryRepository struct {
	mu     sync.RWMutex
	byID   map[string]*Deal
	byGUID map[m2m.UUID]string
	events map[string][]Event
}

// NewMemoryRepository создаёт пустое in-memory хранилище.
func NewMemoryRepository() *MemoryRepository {
	return &MemoryRepository{
		byID:   make(map[string]*Deal),
		byGUID: make(map[m2m.UUID]string),
		events: make(map[string][]Event),
	}
}

// Create вставляет сделку или возвращает существующую по GUID.
func (r *MemoryRepository) Create(_ context.Context, deal *Deal) (*Deal, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	if id, ok := r.byGUID[deal.GUID]; ok {
		return r.byID[id], nil
	}
	r.byID[deal.ID] = deal
	r.byGUID[deal.GUID] = deal.ID
	return deal, nil
}

// GetByGUID возвращает сделку по GUID или ErrNotFound.
func (r *MemoryRepository) GetByGUID(_ context.Context, guid m2m.UUID) (*Deal, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	id, ok := r.byGUID[guid]
	if !ok {
		return nil, ErrNotFound
	}
	return r.byID[id], nil
}

// GetByID возвращает сделку по внутреннему ID или ErrNotFound.
func (r *MemoryRepository) GetByID(_ context.Context, id string) (*Deal, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	d, ok := r.byID[id]
	if !ok {
		return nil, ErrNotFound
	}
	return d, nil
}

// Update в in-memory импликации no-op: указатель уже хранится.
func (r *MemoryRepository) Update(_ context.Context, deal *Deal) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	if _, ok := r.byID[deal.ID]; !ok {
		return ErrNotFound
	}
	return nil
}

// List перебирает сделки и фильтрует на лету.
func (r *MemoryRepository) List(_ context.Context, f Filter) ([]*Deal, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	out := make([]*Deal, 0)
	for _, d := range r.byID {
		if f.State != nil && d.State != *f.State {
			continue
		}
		if f.InvestorID != "" && d.InvestorID != f.InvestorID {
			continue
		}
		if f.CreatedFrom != nil && d.CreatedAt.Before(*f.CreatedFrom) {
			continue
		}
		if f.CreatedTo != nil && d.CreatedAt.After(*f.CreatedTo) {
			continue
		}
		out = append(out, d)
	}
	if f.Offset > 0 && f.Offset < len(out) {
		out = out[f.Offset:]
	} else if f.Offset >= len(out) {
		return nil, nil
	}
	if f.Limit > 0 && f.Limit < len(out) {
		out = out[:f.Limit]
	}
	return out, nil
}

// AppendEvent добавляет событие в журнал сделки.
func (r *MemoryRepository) AppendEvent(_ context.Context, dealID string, ev Event) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	if _, ok := r.byID[dealID]; !ok {
		return ErrNotFound
	}
	r.events[dealID] = append(r.events[dealID], ev)
	return nil
}

// EventsOf возвращает все события сделки (только для тестов и дев-логов).
func (r *MemoryRepository) EventsOf(dealID string) []Event {
	r.mu.RLock()
	defer r.mu.RUnlock()
	src := r.events[dealID]
	out := make([]Event, len(src))
	copy(out, src)
	return out
}
