package m2mcore

import (
	"context"
	"fmt"
	"sync"
	"time"

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

// StageRecord — запись о входе в состояние FSM, для аудита и метрик.
type StageRecord struct {
	State     State
	EnteredAt time.Time
	LeftAt    *time.Time
	Reason    string
}

// Event — доменное событие сделки (event sourcing для аудита).
type Event struct {
	Type      string
	Payload   any
	CreatedAt time.Time
	Actor     string
}

// Deal — корневая агрегатная сущность M2M-сделки.
type Deal struct {
	ID          string
	GUID        m2m.UUID
	State       State
	InvestorID  string
	SignedClaim []byte
	Request     *m2m.M2MTransferRequest
	Response    *m2m.M2MTransferResponse
	RawResponse []byte // точные байты ответа МОСТ от НРД (для пересылки в ТП)
	Decision    *m2m.M2MTransferDecision
	CreatedAt   time.Time
	UpdatedAt   time.Time
	Stages      []StageRecord

	mu     sync.Mutex
	events []Event
}

// NewDeal создаёт новую сделку в состоянии Draft.
func NewDeal(guid m2m.UUID, investorID string, signedClaim []byte) (*Deal, error) {
	if err := guid.Validate(); err != nil {
		return nil, fmt.Errorf("m2mcore: невалидный GUID при создании Deal: %w", err)
	}
	id, err := NewUUIDv4()
	if err != nil {
		return nil, err
	}
	now := time.Now().UTC()
	return &Deal{
		ID:          id,
		GUID:        guid,
		State:       StateDraft,
		InvestorID:  investorID,
		SignedClaim: signedClaim,
		CreatedAt:   now,
		UpdatedAt:   now,
		Stages:      []StageRecord{{State: StateDraft, EnteredAt: now}},
	}, nil
}

// Events возвращает накопленные события (копия слайса).
func (d *Deal) Events() []Event {
	d.mu.Lock()
	defer d.mu.Unlock()
	out := make([]Event, len(d.events))
	copy(out, d.events)
	return out
}

// recordEvent добавляет событие в журнал сделки.
func (d *Deal) recordEvent(eventType string, payload any, actor string) {
	d.events = append(d.events, Event{
		Type: eventType, Payload: payload,
		CreatedAt: time.Now().UTC(), Actor: actor,
	})
}

// shiftTo переводит FSM в новое состояние, фиксируя историю.
// Должен вызываться под d.mu.Lock.
func (d *Deal) shiftTo(next State, reason string) error {
	if err := transition(d.State, next); err != nil {
		return err
	}
	now := time.Now().UTC()
	if i := len(d.Stages) - 1; i >= 0 {
		d.Stages[i].LeftAt = &now
	}
	d.Stages = append(d.Stages, StageRecord{State: next, EnteredAt: now, Reason: reason})
	d.State = next
	d.UpdatedAt = now
	return nil
}

// Validate переводит Draft -> Validated и фиксирует событие.
// Сама валидация Request делается m2m.M2MTransferRequest.Validate(),
// которое следует вызвать перед этим методом.
func (d *Deal) Validate(_ context.Context, request *m2m.M2MTransferRequest) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	if request == nil {
		return fmt.Errorf("m2mcore: Deal.Validate: request=nil")
	}
	if err := request.Validate(); err != nil {
		return fmt.Errorf("m2mcore: M2MTransferRequest.Validate: %w", err)
	}
	d.Request = request
	if err := d.shiftTo(StateValidated, ""); err != nil {
		return err
	}
	d.recordEvent("validated", request.Header.GUID, "system")
	return nil
}

// Submit переводит Validated -> SubmittedToNSD после успешной отправки.
func (d *Deal) Submit(_ context.Context) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	if err := d.shiftTo(StateSubmittedToNSD, ""); err != nil {
		return err
	}
	d.recordEvent("submitted_to_nsd", nil, "system")
	if err := d.shiftTo(StateAwaitingDecision, ""); err != nil {
		return err
	}
	d.recordEvent("awaiting_decision", nil, "system")
	return nil
}

// ReceiveDecision принимает M2MTransferDecision от принимающей стороны и
// меняет состояние на Confirmed или Rejected в зависимости от содержимого.
func (d *Deal) ReceiveDecision(_ context.Context, decision *m2m.M2MTransferDecision) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	if decision == nil {
		return fmt.Errorf("m2mcore: ReceiveDecision: decision=nil")
	}
	if err := decision.Validate(); err != nil {
		return fmt.Errorf("m2mcore: M2MTransferDecision.Validate: %w", err)
	}
	d.Decision = decision

	// Если все Security содержат Confirmation — Confirmed; если хотя бы
	// одна Rejection — Rejected. Смешанные сценарии XSD НРД не запрещает,
	// но на нашей стороне их трактуем как Rejected (требует ручного разбора).
	rejected := false
	for _, s := range decision.Data.Securities {
		if s.TransferDecision.Rejection != nil {
			rejected = true
			break
		}
	}
	target := StateConfirmed
	reason := ""
	if rejected {
		target = StateRejected
		reason = "decision_contains_rejection"
	}
	if err := d.shiftTo(target, reason); err != nil {
		return err
	}
	d.recordEvent("decision_received", decision.Header.GUID, "nsd")
	return nil
}

// ReceiveServiceResponse принимает M2MTransferResponse — ответ сервиса МОСТ
// (НРД), а не контрагента. Это сервисный уровень: подтверждение приёма в
// обработку (StatusCode=INFO) либо отказ сервиса (StatusCode=ERROR, код
// M2Mxx — напр. M2M14 «код отправителя отсутствует в справочнике M2M»).
//
//   - INFO  — заявка принята МОСТ в обработку; состояние не меняем, ждём
//     M2MTransferDecision контрагента. Сохраняем ответ для отображения.
//   - ERROR — сервис отклонил заявку до контрагента; Decision уже не придёт,
//     переводим сделку в Rejected. Сохраняем ответ, чтобы в карточке был
//     виден код и текст отказа НРД.
// raw — точные байты ответа (unpacked doc.xml от НРД); сохраняются как есть
// для дословной пересылки в техподдержку НРД. Может быть nil (mock-режим).
func (d *Deal) ReceiveServiceResponse(_ context.Context, resp *m2m.M2MTransferResponse, raw []byte) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	if resp == nil {
		return fmt.Errorf("m2mcore: ReceiveServiceResponse: resp=nil")
	}
	d.Response = resp
	if len(raw) > 0 {
		d.RawResponse = raw
	}
	d.UpdatedAt = time.Now().UTC()

	if resp.StatusCode != m2m.StatusError {
		// INFO/приём в обработку — фиксируем, но не двигаем FSM.
		d.recordEvent("service_ack", responsePayload(resp), "nsd")
		return nil
	}

	// ERROR — отказ сервиса МОСТ. Если сделка уже в терминальном состоянии,
	// повторный ответ игнорируем (идемпотентность поллера).
	if IsTerminal(d.State) {
		d.recordEvent("service_error_ignored_terminal", responsePayload(resp), "nsd")
		return nil
	}
	if err := d.shiftTo(StateRejected, "service_error:"+firstCode(resp)); err != nil {
		return err
	}
	d.recordEvent("service_error", responsePayload(resp), "nsd")
	return nil
}

// responsePayload сворачивает M2MTransferResponse в компактную запись для
// журнала событий (коды + тексты ответов сервиса).
func responsePayload(resp *m2m.M2MTransferResponse) map[string]any {
	codes := make([]map[string]string, 0, len(resp.Responses))
	for _, r := range resp.Responses {
		codes = append(codes, map[string]string{"code": r.Code, "text": r.Text})
	}
	return map[string]any{
		"status": string(resp.StatusCode),
		"guid":   string(resp.GUID),
		"codes":  codes,
	}
}

// firstCode возвращает первый код ответа (для reason перехода).
func firstCode(resp *m2m.M2MTransferResponse) string {
	if len(resp.Responses) > 0 {
		return resp.Responses[0].Code
	}
	return "unknown"
}

// Timeout переводит сделку в TimedOut (когда не дождались Decision).
func (d *Deal) Timeout(_ context.Context) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	if err := d.shiftTo(StateTimedOut, "no_decision_within_sla"); err != nil {
		return err
	}
	d.recordEvent("timed_out", nil, "system")
	return nil
}

// SendToManualApproval переводит на ручной разбор оператора.
func (d *Deal) SendToManualApproval(_ context.Context, reason string) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	if err := d.shiftTo(StateManualApproval, reason); err != nil {
		return err
	}
	d.recordEvent("manual_approval_requested", reason, "system")
	return nil
}

// ApproveManually вручную подтверждает сделку (с операторской подписью).
func (d *Deal) ApproveManually(_ context.Context, operator string) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	if err := d.shiftTo(StateConfirmed, "manual_approve"); err != nil {
		return err
	}
	d.recordEvent("manual_approve", nil, operator)
	return nil
}

// RejectManually вручную отказывает в сделке.
func (d *Deal) RejectManually(_ context.Context, operator, code, comment string) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	if err := d.shiftTo(StateRejected, "manual_reject:"+code); err != nil {
		return err
	}
	d.recordEvent("manual_reject", map[string]string{"code": code, "comment": comment}, operator)
	return nil
}

// CompleteSUB16 фиксирует получение SUB16 от НРД и переводит Confirmed
// -> AwaitingSUB16 -> Done. Может вызываться один раз.
func (d *Deal) CompleteSUB16(_ context.Context) error {
	d.mu.Lock()
	defer d.mu.Unlock()
	if d.State == StateConfirmed {
		if err := d.shiftTo(StateAwaitingSUB16, ""); err != nil {
			return err
		}
		d.recordEvent("awaiting_sub16", nil, "nsd")
	}
	if err := d.shiftTo(StateDone, ""); err != nil {
		return err
	}
	d.recordEvent("done", nil, "nsd")
	return nil
}
