// Package mock — заглушка NSDSender для локальных стендов без реального
// Интеграционного шлюза НРД. Возвращает синтетические M2MTransferResponse
// (синхронно, сразу) и эмитит M2MTransferDecision (асинхронно, через
// настраиваемую задержку) для каждой отправленной заявки.
//
// Подходит для:
//   - сквозного дев-теста (ЛК → m2m-core → mock → callback в ЛК);
//   - демонстраций «увидеть как оно работает» без подключения к НРД;
//   - юнит-тестов компонентов выше уровня транспорта.
package mock

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

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

// DecisionOutcome — что должен вернуть mock после задержки.
type DecisionOutcome int

const (
	OutcomeConfirm DecisionOutcome = iota
	OutcomeReject
	OutcomeTimeout
)

// Config — настройки mock-сендера.
type Config struct {
	// DecisionDelay — задержка между Send и эмиссией Decision в канал.
	DecisionDelay time.Duration
	// DefaultOutcome — каким будет Decision, если не задан per-Request override.
	DefaultOutcome DecisionOutcome
	// RejectionCode — какой код отказа возвращать в OutcomeReject (6 chars max).
	RejectionCode string
	// SenderCode/ReceiverCode для эмитированной Decision Header (берётся из
	// исходного Request обменом местами).
	NSDSenderCode m2m.DeponentCode
}

// DefaultConfig — разумные дефолты: подтверждение через 3 секунды.
func DefaultConfig() Config {
	return Config{
		DecisionDelay:  3 * time.Second,
		DefaultOutcome: OutcomeConfirm,
		RejectionCode:  "07",
	}
}

// Sender — mock реализация m2mcore.NSDSender.
type Sender struct {
	cfg        Config
	decisions  chan *m2m.M2MTransferDecision
	outcomes   sync.Map // map[m2m.UUID]DecisionOutcome — override per GUID
	mu         sync.Mutex
	stats      stats
	lifeCtx    context.Context // независимый от HTTP-запроса контекст для эмиссии Decision
	lifeCancel context.CancelFunc
}

type stats struct {
	Sent      uint64
	Confirmed uint64
	Rejected  uint64
	TimedOut  uint64
}

// NewSender создаёт mock с указанной конфигурацией.
func NewSender(cfg Config) *Sender {
	ctx, cancel := context.WithCancel(context.Background())
	return &Sender{
		cfg:        cfg,
		decisions:  make(chan *m2m.M2MTransferDecision, 64),
		lifeCtx:    ctx,
		lifeCancel: cancel,
	}
}

// Stop отменяет внутренний контекст mock'а, останавливая все запущенные
// emit-горутины.
func (s *Sender) Stop() { s.lifeCancel() }

// SetOutcome задаёт исход для конкретной заявки по GUID (вызывается до Send).
func (s *Sender) SetOutcome(guid m2m.UUID, out DecisionOutcome) {
	s.outcomes.Store(guid, out)
}

// Decisions — канал эмитированных от имени принимающей стороны Decision'ов.
// Подписаться, считать в горутине, передавать в m2mcore.Deal.ReceiveDecision.
func (s *Sender) Decisions() <-chan *m2m.M2MTransferDecision { return s.decisions }

// Stats — текущие счётчики (для UI).
func (s *Sender) Stats() (sent, confirmed, rejected, timedOut uint64) {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.stats.Sent, s.stats.Confirmed, s.stats.Rejected, s.stats.TimedOut
}

// Send имитирует отправку в НРД. Возвращает синтетический Response
// с StatusCode=INFO. Через DecisionDelay в канал прилетает Decision.
func (s *Sender) Send(ctx context.Context, req *m2m.M2MTransferRequest) (*m2m.M2MTransferResponse, error) {
	if req == nil {
		return nil, errors.New("nsdadapter/mock: Send: req=nil")
	}
	if err := req.Validate(); err != nil {
		return nil, err
	}

	s.mu.Lock()
	s.stats.Sent++
	s.mu.Unlock()

	// Синтетический Response: принимаем заявку, по каждой ЦБ — INFO 01.
	resp := &m2m.M2MTransferResponse{
		GUID:       req.Header.GUID,
		StatusCode: m2m.StatusInfo,
	}
	for _, sec := range req.Data.TransferredSecurities.Securities {
		refID := sec.ReferenceID
		resp.Responses = append(resp.Responses, m2m.Response{
			ReferenceID: &refID,
			Code:        "01",
			Text:        "Запрос на перевод принят НРД и направлен принимающей стороне (mock).",
		})
	}

	// Эмитим Decision в отдельной горутине. Используем lifeCtx, чтобы
	// HTTP-таймауты вызывающего не прерывали эмиссию.
	outcome := s.cfg.DefaultOutcome
	if v, ok := s.outcomes.Load(req.Header.GUID); ok {
		outcome = v.(DecisionOutcome)
	}
	go s.emitDecision(s.lifeCtx, req, outcome)
	_ = ctx

	return resp, nil
}

// SendDecision имитирует отправку нашего Decision принимающей стороне (мы — реципиент).
// Mock просто фиксирует статистику.
func (s *Sender) SendDecision(_ context.Context, decision *m2m.M2MTransferDecision) error {
	if decision == nil {
		return errors.New("nsdadapter/mock: SendDecision: decision=nil")
	}
	if err := decision.Validate(); err != nil {
		return err
	}
	s.mu.Lock()
	s.stats.Sent++
	s.mu.Unlock()
	return nil
}

func (s *Sender) emitDecision(ctx context.Context, req *m2m.M2MTransferRequest, outcome DecisionOutcome) {
	select {
	case <-ctx.Done():
		return
	case <-time.After(s.cfg.DecisionDelay):
	}

	if outcome == OutcomeTimeout {
		s.mu.Lock()
		s.stats.TimedOut++
		s.mu.Unlock()
		// При таймауте Decision не приходит вообще — это и эмулируем.
		return
	}

	// Робот-автотест НРД: если ReceiverCode == MC0012500000 и DocumentSeries
	// задан (1111/2001/2002/3333) — формируем ответ по сценарию из
	// «Инструкции по тестированию с роботом» (DOC/instruktsiya-po-...pdf).
	// Это позволяет проверить нашу логику обработки ответов до того, как
	// у нас будет реальный ИШ + сертификат + доступ к TEST3.
	if decision := simulateRobotDecision(req); decision != nil {
		s.mu.Lock()
		// Грубая статистика: считаем «робот-ответ» как Confirmed если хоть
		// одна бумага подтверждена, иначе Rejected.
		hasConfirm := false
		for _, ds := range decision.Data.Securities {
			if ds.TransferDecision.Confirmation != nil {
				hasConfirm = true
				break
			}
		}
		if hasConfirm {
			s.stats.Confirmed++
		} else {
			s.stats.Rejected++
		}
		s.mu.Unlock()
		select {
		case <-ctx.Done():
		case s.decisions <- decision:
		}
		return
	}

	decision := &m2m.M2MTransferDecision{
		Header: m2m.DecisionHeader{
			GUID:              req.Header.GUID,
			CreationTimestamp: nsdxml.Now(),
			SenderCode:        req.Header.ReceiverCode,
			ReceiverCode:      req.Header.SenderCode,
			CostInfo:          m2m.CostInfo{No: &m2m.CostInfoNo{}},
		},
		Data: m2m.DecisionData{
			ReceivingDepository: req.Data.ReceivingDepository,
		},
	}
	for _, sec := range req.Data.TransferredSecurities.Securities {
		ds := m2m.DecisionSecurity{ReferenceID: sec.ReferenceID}
		if outcome == OutcomeConfirm {
			ds.TransferDecision = m2m.DecisionTransfer{
				Confirmation: &m2m.Confirmation{
					SettlementAccount: sec.SettlementAccount[0],
				},
			}
		} else {
			ds.TransferDecision = m2m.DecisionTransfer{
				Rejection: &m2m.Rejection{
					Codes: []string{s.cfg.RejectionCode},
				},
			}
		}
		decision.Data.Securities = append(decision.Data.Securities, ds)
	}

	s.mu.Lock()
	switch outcome {
	case OutcomeConfirm:
		s.stats.Confirmed++
	case OutcomeReject:
		s.stats.Rejected++
	}
	s.mu.Unlock()

	select {
	case <-ctx.Done():
	case s.decisions <- decision:
	}
}

// Verify тип Sender удовлетворяет m2mcore.NSDSender.
var _ m2mcore.NSDSender = (*Sender)(nil)
