package lkgateway

import (
	"context"
	"errors"
	"fmt"
	"log"
	"net/http"
	"sort"
	"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"
)

// Service — бизнес-логика lk-gateway: преобразует DTO в доменные сущности
// m2mcore, оркестрирует FSM сделки, эмитит callback'и в ЛК.
type Service struct {
	repo            m2mcore.Repository
	sender          m2mcore.NSDSender
	store           m2mcore.FansyStore
	recorder        *m2mcore.MemoryRecorder
	defaultSender   m2m.DeponentCode
	defaultReceiver m2m.DeponentCode
	callbackURL     string
	httpClient      *http.Client

	mu        sync.RWMutex
	claimToID map[string]string // claim public ID -> internal deal ID
}

// Config — параметры сервиса.
type Config struct {
	Repository      m2mcore.Repository
	Sender          m2mcore.NSDSender
	Store           m2mcore.FansyStore
	Recorder        *m2mcore.MemoryRecorder
	DefaultSender   m2m.DeponentCode
	DefaultReceiver m2m.DeponentCode
	CallbackURL     string
}

// NewService собирает сервис.
func NewService(cfg Config) *Service {
	if cfg.Recorder == nil {
		cfg.Recorder = m2mcore.NewMemoryRecorder()
	}
	return &Service{
		repo:            cfg.Repository,
		sender:          cfg.Sender,
		store:           cfg.Store,
		recorder:        cfg.Recorder,
		defaultSender:   cfg.DefaultSender,
		defaultReceiver: cfg.DefaultReceiver,
		callbackURL:     cfg.CallbackURL,
		httpClient:      &http.Client{Timeout: 5 * time.Second},
		claimToID:       make(map[string]string),
	}
}

// CreateClaim принимает DTO заявки, формирует M2MTransferRequest,
// создаёт сделку и отправляет в НРД.
func (s *Service) CreateClaim(ctx context.Context, in CreateClaimRequest) (CreateClaimResponse, error) {
	domainClaim, err := dtoToClaim(in)
	if err != nil {
		return CreateClaimResponse{}, fmt.Errorf("lkgateway: dtoToClaim: %w", err)
	}

	receiver := s.defaultReceiver
	if in.ReceiverCodeOverride != "" {
		receiver = m2m.DeponentCode(in.ReceiverCodeOverride)
	}
	req, err := m2mcore.EnrichRequest(ctx, s.store, domainClaim, m2mcore.SenderReceiver{
		SenderCode:   s.defaultSender,
		ReceiverCode: receiver,
	})
	if err != nil {
		return CreateClaimResponse{}, fmt.Errorf("lkgateway: EnrichRequest: %w", err)
	}

	deal, err := m2mcore.NewDeal(req.Header.GUID, in.Investor.ID, []byte(in.SignedDocument))
	if err != nil {
		return CreateClaimResponse{}, fmt.Errorf("lkgateway: NewDeal: %w", err)
	}
	saved, err := s.repo.Create(ctx, deal)
	if err != nil {
		return CreateClaimResponse{}, fmt.Errorf("lkgateway: repo.Create: %w", err)
	}

	if err := saved.Validate(ctx, req); err != nil {
		return CreateClaimResponse{}, fmt.Errorf("lkgateway: deal.Validate: %w", err)
	}

	resp, err := s.sender.Send(ctx, req)
	if err != nil {
		return CreateClaimResponse{}, fmt.Errorf("lkgateway: sender.Send: %w", err)
	}
	saved.Response = resp

	if err := saved.Submit(ctx); err != nil {
		return CreateClaimResponse{}, fmt.Errorf("lkgateway: deal.Submit: %w", err)
	}
	if err := s.repo.Update(ctx, saved); err != nil {
		return CreateClaimResponse{}, fmt.Errorf("lkgateway: repo.Update: %w", err)
	}
	s.recorder.IncDeal(saved.State)
	s.mu.Lock()
	s.claimToID[saved.ID] = saved.ID
	s.mu.Unlock()

	return CreateClaimResponse{
		ID:        saved.ID,
		Status:    string(saved.State),
		CreatedAt: saved.CreatedAt,
		Success:   true,
	}, nil
}

// GetClaim возвращает полную карточку заявки.
func (s *Service) GetClaim(ctx context.Context, id string) (ClaimView, error) {
	deal, err := s.repo.GetByID(ctx, id)
	if err != nil {
		return ClaimView{}, err
	}
	return dealToView(deal), nil
}

// ListClaims возвращает страницу заявок.
func (s *Service) ListClaims(ctx context.Context, filter m2mcore.Filter) (ClaimsPage, error) {
	if filter.Limit == 0 {
		filter.Limit = 50
	}
	deals, err := s.repo.List(ctx, filter)
	if err != nil {
		return ClaimsPage{}, err
	}
	sort.Slice(deals, func(i, j int) bool { return deals[i].CreatedAt.After(deals[j].CreatedAt) })
	items := make([]ClaimView, 0, len(deals))
	for _, d := range deals {
		items = append(items, dealToView(d))
	}
	return ClaimsPage{Items: items, Total: len(items), Limit: filter.Limit, Offset: filter.Offset}, nil
}

// ApplyDecision принимает Decision (из mock-NSDSender или реального адаптера),
// обновляет соответствующую сделку и шлёт callback в ЛК.
func (s *Service) ApplyDecision(ctx context.Context, decision *m2m.M2MTransferDecision) error {
	if decision == nil {
		return errors.New("lkgateway: ApplyDecision: decision=nil")
	}
	deal, err := s.repo.GetByGUID(ctx, decision.Header.GUID)
	if err != nil {
		return fmt.Errorf("lkgateway: GetByGUID: %w", err)
	}
	if err := deal.ReceiveDecision(ctx, decision); err != nil {
		return fmt.Errorf("lkgateway: ReceiveDecision: %w", err)
	}
	if err := s.repo.Update(ctx, deal); err != nil {
		return fmt.Errorf("lkgateway: repo.Update: %w", err)
	}
	s.recorder.IncDeal(deal.State)

	if s.callbackURL != "" {
		s.sendCallback(ctx, deal)
	}
	return nil
}

// ApplyServiceResponse применяет M2MTransferResponse (ответ сервиса МОСТ) к
// сделке: сохраняет ответ, при ERROR переводит сделку в Rejected и шлёт
// callback в ЛК. Сделку ищем по GUID ответа.
func (s *Service) ApplyServiceResponse(ctx context.Context, resp *m2m.M2MTransferResponse, raw []byte) error {
	if resp == nil {
		return errors.New("lkgateway: ApplyServiceResponse: resp=nil")
	}
	deal, err := s.findDealForResponse(ctx, resp)
	if err != nil {
		return fmt.Errorf("lkgateway: поиск сделки для ответа: %w", err)
	}
	prev := deal.State
	if err := deal.ReceiveServiceResponse(ctx, resp, raw); err != nil {
		return fmt.Errorf("lkgateway: ReceiveServiceResponse: %w", err)
	}
	if err := s.repo.Update(ctx, deal); err != nil {
		return fmt.Errorf("lkgateway: repo.Update: %w", err)
	}
	// Состояние сменилось (ERROR → Rejected) — учитываем в метриках и шлём
	// callback. На INFO состояние не меняется — callback не нужен.
	if deal.State != prev {
		s.recorder.IncDeal(deal.State)
		if s.callbackURL != "" {
			s.sendCallback(ctx, deal)
		}
	}
	return nil
}

// zeroGUID — нулевой UUID, который НРД присылает в сервисных ошибках
// (напр. M2M14), когда не идентифицирует исходный запрос.
const zeroGUID = "00000000-0000-0000-0000-000000000000"

// findDealForResponse сопоставляет ответ МОСТ со сделкой. Сначала по GUID;
// если GUID нулевой/пустой или сделка по нему не найдена (реальное поведение
// НРД при M2M14 — ответ без нашего GUID и без ReferenceID), применяем
// эвристику: берём самую раннюю ожидающую решение заявку без ответа. Для
// тестового сценария «одна заявка в полёте» это однозначно; при множестве
// заявок in-flight такие сервисные ошибки в принципе неразличимы на стороне
// НРД, и FIFO — наилучшее доступное приближение.
func (s *Service) findDealForResponse(ctx context.Context, resp *m2m.M2MTransferResponse) (*m2mcore.Deal, error) {
	guid := string(resp.GUID)
	if guid != "" && guid != zeroGUID {
		deal, err := s.repo.GetByGUID(ctx, resp.GUID)
		if err == nil {
			return deal, nil
		}
		if !errors.Is(err, m2mcore.ErrNotFound) {
			return nil, err
		}
	}
	// Fallback: ответ без идентифицируемого GUID.
	st := m2mcore.StateAwaitingDecision
	deals, err := s.repo.List(ctx, m2mcore.Filter{State: &st, Limit: 200})
	if err != nil {
		return nil, err
	}
	var cand *m2mcore.Deal
	for _, d := range deals {
		if d.Response != nil {
			continue // этой заявке ответ уже сопоставлен
		}
		if cand == nil || d.CreatedAt.Before(cand.CreatedAt) {
			cand = d
		}
	}
	if cand == nil {
		return nil, m2mcore.ErrNotFound
	}
	log.Printf("lkgateway: ответ МОСТ с GUID=%s сопоставлен по эвристике (FIFO) заявке id=%s status=%s",
		guid, cand.ID, resp.StatusCode)
	return cand, nil
}

// sendCallback отправляет PATCH в ЛК с обновлением статуса.
func (s *Service) sendCallback(ctx context.Context, deal *m2mcore.Deal) {
	cb := callbackForDeal(deal)
	if err := postJSON(ctx, s.httpClient, s.callbackURL+"/api/v1/back_office/claims/"+deal.ID, "PATCH", cb); err != nil {
		log.Printf("lkgateway: callback в ЛК упал: %v", err)
	}
}

// Recorder возвращает экспонируемый Recorder для admin-страницы.
func (s *Service) Recorder() *m2mcore.MemoryRecorder { return s.recorder }

// Repo возвращает Repository (для админских проверок).
func (s *Service) Repo() m2mcore.Repository { return s.repo }

// Внутренние преобразования и хелперы.

func dtoToClaim(in CreateClaimRequest) (m2mcore.ClaimInput, error) {
	out := m2mcore.ClaimInput{
		InvestorClientID:          in.Investor.ID,
		TransferringDepositoryINN: m2m.OrganizationINN(in.TransferringDepositoryINN),
		ReceivingDepositoryINN:    m2m.OrganizationINN(in.ReceivingDepositoryINN),
	}
	// Переопределение документа инвестора (тест с роботом: серия = сценарий).
	if d := in.InvestorDocumentOverride; d != nil {
		out.InvestorDocument = &m2mcore.ClientDocument{
			DocumentType: m2m.IdentityDocumentCode(d.DocumentType),
			Series:       d.Series,
			Number:       d.Number,
		}
	}
	// CostInfo
	if in.CostInfo.Yes != nil {
		out.CostInfo = m2m.CostInfo{Yes: &m2m.CostInfoYes{Code: m2m.DeponentCode(in.CostInfo.Yes.Code)}}
	} else {
		out.CostInfo = m2m.CostInfo{No: &m2m.CostInfoNo{}}
	}
	// IIA
	if in.IIAAgreement != nil {
		out.IIAAgreement = &m2m.IIAAgreementDetails{
			AgreementType:   m2m.IIAContractType(in.IIAAgreement.AgreementType),
			AgreementNumber: in.IIAAgreement.AgreementNumber,
			AgreementDate:   in.IIAAgreement.AgreementDate,
			BrokerINN:       m2m.OrganizationINN(in.IIAAgreement.BrokerINN),
		}
	}
	// Securities
	for _, sec := range in.Securities {
		ds, err := dtoSecurityDetails(sec.SecurityDetails)
		if err != nil {
			return m2mcore.ClaimInput{}, err
		}
		q, err := dtoQuantity(sec.Quantity)
		if err != nil {
			return m2mcore.ClaimInput{}, err
		}
		out.Securities = append(out.Securities, m2mcore.ClaimSecurityInput{
			SecurityCode: m2m.SecurityCode(sec.SecurityCode),
			Details:      ds,
			Quantity:     q,
		})
	}
	return out, nil
}

func dtoSecurityDetails(in SecurityDetails) (m2m.SecurityDetails, error) {
	if in.ISIN != "" {
		isin := m2m.ISIN(in.ISIN)
		return m2m.SecurityDetails{ISIN: &isin}, nil
	}
	if in.SecurityInfo != nil {
		si := &m2m.SecurityDescription{
			SecurityClassification: m2m.SecurityClassification(in.SecurityInfo.Classification),
			SecurityCategory:       m2m.SecurityCategory(in.SecurityInfo.Category),
			SecurityType:           in.SecurityInfo.SecurityType,
			SecuritySeries:         in.SecurityInfo.SecuritySeries,
		}
		if in.SecurityInfo.IdentificationDetails.RegNumber != "" {
			rn := in.SecurityInfo.IdentificationDetails.RegNumber
			si.IdentificationDetails = m2m.IdentificationDetails{RegNumber: &rn}
		}
		if in.SecurityInfo.IdentificationDetails.FundShares != nil {
			si.IdentificationDetails = m2m.IdentificationDetails{
				FundShares: &m2m.FundShares{
					RegNumber: in.SecurityInfo.IdentificationDetails.FundShares.RegNumber,
					Class:     in.SecurityInfo.IdentificationDetails.FundShares.Class,
				},
			}
		}
		return m2m.SecurityDetails{SecurityInfo: si}, nil
	}
	return m2m.SecurityDetails{}, errors.New("lkgateway: SecurityDetails — задайте isin или security_info")
}

func dtoQuantity(in Quantity) (m2m.Quantity, error) {
	if in.Whole > 0 {
		w := in.Whole
		return m2m.Quantity{Whole: &w}, nil
	}
	if in.Fractional != "" {
		f := m2m.Decimal16(in.Fractional)
		return m2m.Quantity{Fractional: &f}, nil
	}
	return m2m.Quantity{}, errors.New("lkgateway: Quantity — задайте whole или fractional")
}

func dealToView(d *m2mcore.Deal) ClaimView {
	out := ClaimView{
		ID:        d.ID,
		Status:    string(d.State),
		CreatedAt: d.CreatedAt,
		UpdatedAt: d.UpdatedAt,
		M2MGUID:   d.GUID,
	}
	for _, st := range d.Stages {
		out.Stages = append(out.Stages, StageView{
			State: string(st.State), EnteredAt: st.EnteredAt, LeftAt: st.LeftAt, Reason: st.Reason,
		})
	}
	if d.Request != nil {
		out.TransferringDepositoryINN = string(d.Request.Data.TransferringDepository.INN)
		out.ReceivingDepositoryINN = string(d.Request.Data.ReceivingDepository.INN)
		ii := d.Request.Data.InvestorInformation
		out.Investor = Investor{
			LastName: ii.LastName, FirstName: ii.FirstName, MiddleName: ii.MiddleName,
			Document: Document{
				DocumentType: string(ii.IdentityDocument.DocumentType),
				Number:       string(ii.IdentityDocument.DocumentNumber),
			},
		}
		if ii.IdentityDocument.DocumentSeries != nil {
			out.Investor.Document.Series = string(*ii.IdentityDocument.DocumentSeries)
		}
		if d.Request.Header.CostInfo.Yes != nil {
			out.CostInfo = CostInfo{Yes: &CostInfoYes{Code: string(d.Request.Header.CostInfo.Yes.Code)}}
		} else if d.Request.Header.CostInfo.No != nil {
			empty := struct{}{}
			out.CostInfo = CostInfo{No: &empty}
		}
		if d.Request.Header.IIAAgreementDetails != nil {
			out.IIAAgreement = &IIAAgreement{
				AgreementType:   string(d.Request.Header.IIAAgreementDetails.AgreementType),
				AgreementNumber: d.Request.Header.IIAAgreementDetails.AgreementNumber,
				AgreementDate:   d.Request.Header.IIAAgreementDetails.AgreementDate,
				BrokerINN:       string(d.Request.Header.IIAAgreementDetails.BrokerINN),
			}
		}
	}
	if d.Response != nil {
		out.M2MResponse = responseToView(d.Response)
		if len(d.RawResponse) > 0 {
			// Ответ НРД в windows-1251 — декодируем в UTF-8 для показа.
			out.M2MResponse.RawXML = string(nsdxml.DecodeWindows1251(d.RawResponse))
		}
	}
	if d.Decision != nil {
		out.M2MDecision = decisionToView(d.Decision)
	}
	if d.State != m2mcore.StateDraft {
		cb := callbackForDeal(d)
		out.LastCallback = &cb
	}
	return out
}

func responseToView(r *m2m.M2MTransferResponse) *NSDResponseView {
	v := &NSDResponseView{
		GUID:       string(r.GUID),
		StatusCode: string(r.StatusCode),
	}
	for _, e := range r.Responses {
		ent := NSDResponseEntry{Code: e.Code, Text: e.Text}
		if e.ReferenceID != nil {
			ent.ReferenceID = string(*e.ReferenceID)
		}
		v.Responses = append(v.Responses, ent)
	}
	return v
}

func decisionToView(d *m2m.M2MTransferDecision) *NSDDecisionView {
	v := &NSDDecisionView{GUID: string(d.Header.GUID)}
	for _, sec := range d.Data.Securities {
		entry := NSDDecisionSecurity{ReferenceID: string(sec.ReferenceID)}
		if sec.TransferDecision.Confirmation != nil {
			entry.Outcome = "confirmed"
		} else if sec.TransferDecision.Rejection != nil {
			entry.Outcome = "rejected"
			entry.RejectCodes = sec.TransferDecision.Rejection.Codes
		}
		v.Securities = append(v.Securities, entry)
	}
	return v
}

func callbackForDeal(d *m2mcore.Deal) StatusCallback {
	cb := StatusCallback{
		ClaimID:   d.ID,
		NewStatus: string(d.State),
		UpdatedAt: d.UpdatedAt,
	}
	if d.Decision != nil {
		cb.NSDResponse = nsdResponseFromDecision(d.Decision)
		if d.State == m2mcore.StateRejected {
			for _, sec := range d.Decision.Data.Securities {
				if sec.TransferDecision.Rejection != nil && len(sec.TransferDecision.Rejection.Codes) > 0 {
					cb.ReasonCode = sec.TransferDecision.Rejection.Codes[0]
					cb.ReasonText = "Отказ принимающей стороны (mock)"
					break
				}
			}
		}
	} else if d.Response != nil {
		cb.NSDResponse = responseToView(d.Response)
	}
	return cb
}

func nsdResponseFromDecision(d *m2m.M2MTransferDecision) *NSDResponseView {
	v := &NSDResponseView{GUID: string(d.Header.GUID), StatusCode: string(m2m.StatusInfo)}
	for _, sec := range d.Data.Securities {
		ref := string(sec.ReferenceID)
		ent := NSDResponseEntry{ReferenceID: ref}
		if sec.TransferDecision.Confirmation != nil {
			ent.Code = "01"
			ent.Text = "Подтверждение принимающей стороны."
		} else if sec.TransferDecision.Rejection != nil {
			ent.Code = "07"
			ent.Text = "Отказ принимающей стороны."
		}
		v.Responses = append(v.Responses, ent)
	}
	return v
}
