package oobs

import (
	"context"
	"strings"
	"time"

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"

	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"go.uber.org/zap"
)

type svc struct {
	logger  mlogger.Logger
	metrics Metrics
	store   AuditStore
	now     func() time.Time
}

func newService(deps Dependencies) (Observer, error) {
	store := deps.Store
	if store == nil {
		store = newMemoryAuditStore()
	}

	logger := deps.Logger
	if logger == nil {
		logger = zap.NewNop()
	}
	logger = logger.Named("observer")

	metrics := deps.Metrics
	if metrics == nil {
		metrics = newNoopMetrics()
	}

	return &svc{
		logger:  logger,
		metrics: metrics,
		store:   store,
		now:     deps.Now,
	}, nil
}

func (s *svc) RecordPayment(_ context.Context, in RecordPaymentInput) (err error) {
	logger := s.logger
	paymentRef := ""
	if in.Payment != nil {
		paymentRef = strings.TrimSpace(in.Payment.PaymentRef)
	}
	logger.Debug("Starting Record payment",
		zap.String("payment_ref", paymentRef),
		zap.String("event", string(in.Event)),
	)
	defer func(start time.Time) {
		fields := []zap.Field{
			zap.Int64("duration_ms", time.Since(start).Milliseconds()),
			zap.String("payment_ref", paymentRef),
			zap.String("event", string(in.Event)),
		}
		if err != nil {
			logger.Warn("Failed to record payment", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed Record payment", fields...)
	}(time.Now())

	entry, payment, event, err := buildPaymentEntry(nowUTC(s.now), in)
	if err != nil {
		return err
	}
	if err := s.store.Append(entry); err != nil {
		return err
	}

	s.metrics.IncPaymentEvent(event, payment.State)
	s.logPayment(entry, payment.State, payment.Version)
	return nil
}

func (s *svc) RecordStep(_ context.Context, in RecordStepInput) (err error) {
	logger := s.logger
	logger.Debug("Starting Record step",
		zap.String("payment_ref", strings.TrimSpace(in.PaymentRef)),
		zap.String("step_ref", strings.TrimSpace(in.Step.StepRef)),
		zap.Uint32("attempt", in.Step.Attempt),
		zap.String("event", string(in.Event)),
	)
	defer func(start time.Time) {
		fields := []zap.Field{
			zap.Int64("duration_ms", time.Since(start).Milliseconds()),
			zap.String("payment_ref", strings.TrimSpace(in.PaymentRef)),
			zap.String("step_ref", strings.TrimSpace(in.Step.StepRef)),
			zap.Uint32("attempt", in.Step.Attempt),
			zap.String("event", string(in.Event)),
		}
		if err != nil {
			logger.Warn("Failed to record step", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed Record step", fields...)
	}(time.Now())

	entry, step, event, duration, err := buildStepEntry(nowUTC(s.now), in)
	if err != nil {
		return err
	}
	if err := s.store.Append(entry); err != nil {
		return err
	}

	s.metrics.IncStepEvent(event, step.StepCode, step.State)
	if duration > 0 {
		s.metrics.ObserveStepDuration(step.StepCode, step.State, duration)
	}
	s.logStep(entry, step.State, duration)
	return nil
}

func (s *svc) RecordExternal(_ context.Context, in RecordExternalInput) (err error) {
	logger := s.logger
	logger.Debug("Starting Record external",
		zap.String("payment_ref", strings.TrimSpace(in.PaymentRef)),
		zap.String("step_ref", strings.TrimSpace(in.StepRef)),
		zap.Uint32("attempt", in.Attempt),
		zap.String("source", string(in.Source)),
		zap.String("status", strings.TrimSpace(in.Status)),
	)
	defer func(start time.Time) {
		fields := []zap.Field{
			zap.Int64("duration_ms", time.Since(start).Milliseconds()),
			zap.String("payment_ref", strings.TrimSpace(in.PaymentRef)),
			zap.String("step_ref", strings.TrimSpace(in.StepRef)),
			zap.Uint32("attempt", in.Attempt),
			zap.String("source", string(in.Source)),
			zap.String("status", strings.TrimSpace(in.Status)),
		}
		if err != nil {
			logger.Warn("Failed to record external", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed Record external", fields...)
	}(time.Now())

	entry, source, status, err := buildExternalEntry(nowUTC(s.now), in)
	if err != nil {
		return err
	}
	if err := s.store.Append(entry); err != nil {
		return err
	}

	s.metrics.IncExternalEvent(source, status)
	s.logExternal(entry, source, status)
	return nil
}

func (s *svc) PaymentTimeline(_ context.Context, in PaymentTimelineInput) (out *TimelineOutput, err error) {
	logger := s.logger
	logger.Debug("Starting Payment timeline",
		zap.String("payment_ref", strings.TrimSpace(in.PaymentRef)),
		zap.Int32("limit", in.Limit),
		zap.Bool("desc", in.Desc),
	)
	defer func(start time.Time) {
		fields := []zap.Field{zap.Int64("duration_ms", time.Since(start).Milliseconds())}
		if out != nil {
			fields = append(fields, zap.Int("items_count", len(out.Items)))
		}
		if err != nil {
			logger.Warn("Failed to payment timeline", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed Payment timeline", fields...)
	}(time.Now())

	paymentRef := strings.TrimSpace(in.PaymentRef)
	if paymentRef == "" {
		return nil, merrors.InvalidArgument("payment_ref is required")
	}
	items, err := s.store.ListByPayment(paymentRef, in.Limit, in.Desc)
	if err != nil {
		return nil, err
	}
	out = &TimelineOutput{Items: items}
	return out, nil
}

func (s *svc) StepTimeline(_ context.Context, in StepTimelineInput) (out *TimelineOutput, err error) {
	logger := s.logger
	logger.Debug("Starting Step timeline",
		zap.String("payment_ref", strings.TrimSpace(in.PaymentRef)),
		zap.String("step_ref", strings.TrimSpace(in.StepRef)),
		zap.Uint32("attempt", in.Attempt),
		zap.Int32("limit", in.Limit),
		zap.Bool("desc", in.Desc),
	)
	defer func(start time.Time) {
		fields := []zap.Field{zap.Int64("duration_ms", time.Since(start).Milliseconds())}
		if out != nil {
			fields = append(fields, zap.Int("items_count", len(out.Items)))
		}
		if err != nil {
			logger.Warn("Failed to step timeline", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed Step timeline", fields...)
	}(time.Now())

	paymentRef := strings.TrimSpace(in.PaymentRef)
	if paymentRef == "" {
		return nil, merrors.InvalidArgument("payment_ref is required")
	}
	stepRef := strings.TrimSpace(in.StepRef)
	if stepRef == "" {
		return nil, merrors.InvalidArgument("step_ref is required")
	}
	if in.Attempt == 0 {
		return nil, merrors.InvalidArgument("attempt is required")
	}

	items, err := s.store.ListByStepAttempt(paymentRef, stepRef, in.Attempt, in.Limit, in.Desc)
	if err != nil {
		return nil, err
	}
	out = &TimelineOutput{Items: items}
	return out, nil
}

func buildPaymentEntry(now time.Time, in RecordPaymentInput) (TimelineEntry, *agg.Payment, PaymentEvent, error) {
	payment := in.Payment
	if payment == nil {
		return TimelineEntry{}, nil, "", merrors.InvalidArgument("payment is required")
	}
	event, ok := normalizePaymentEvent(in.Event)
	if !ok {
		return TimelineEntry{}, nil, "", merrors.InvalidArgument("event is invalid")
	}
	state, ok := normalizeAggregateState(payment.State)
	if !ok {
		return TimelineEntry{}, nil, "", merrors.InvalidArgument("payment.state is invalid")
	}
	paymentRef := strings.TrimSpace(payment.PaymentRef)
	if paymentRef == "" {
		return TimelineEntry{}, nil, "", merrors.InvalidArgument("payment.payment_ref is required")
	}

	entry := TimelineEntry{
		OccurredAt: now,
		Scope:      ScopePayment,
		PaymentRef: paymentRef,
		Event:      string(event),
		State:      string(state),
		Message:    strings.TrimSpace(in.Message),
		Fields:     trimStringMap(in.Fields),
	}
	return entry, payment, event, nil
}

func buildStepEntry(now time.Time, in RecordStepInput) (TimelineEntry, agg.StepExecution, StepEvent, time.Duration, error) {
	event, ok := normalizeStepEvent(in.Event)
	if !ok {
		return TimelineEntry{}, agg.StepExecution{}, "", 0, merrors.InvalidArgument("event is invalid")
	}
	paymentRef := strings.TrimSpace(in.PaymentRef)
	if paymentRef == "" {
		return TimelineEntry{}, agg.StepExecution{}, "", 0, merrors.InvalidArgument("payment_ref is required")
	}
	stepRef := strings.TrimSpace(in.Step.StepRef)
	if stepRef == "" {
		return TimelineEntry{}, agg.StepExecution{}, "", 0, merrors.InvalidArgument("step.step_ref is required")
	}
	stepState, ok := normalizeStepState(in.Step.State)
	if !ok {
		return TimelineEntry{}, agg.StepExecution{}, "", 0, merrors.InvalidArgument("step.state is invalid")
	}

	attempt := in.Step.Attempt
	if attempt == 0 {
		attempt = 1
	}

	entry := TimelineEntry{
		OccurredAt: now,
		Scope:      ScopeStep,
		PaymentRef: paymentRef,
		StepRef:    stepRef,
		StepCode:   strings.TrimSpace(in.Step.StepCode),
		Attempt:    attempt,
		Event:      string(event),
		State:      string(stepState),
		Message:    strings.TrimSpace(in.Message),
		Fields:     trimStringMap(in.Fields),
	}

	step := in.Step
	step.State = stepState
	step.StepRef = stepRef
	step.StepCode = strings.TrimSpace(step.StepCode)
	step.Attempt = attempt

	return entry, step, event, in.Duration, nil
}

func buildExternalEntry(now time.Time, in RecordExternalInput) (TimelineEntry, ExternalSource, string, error) {
	source, ok := normalizeExternalSource(in.Source)
	if !ok {
		return TimelineEntry{}, "", "", merrors.InvalidArgument("source is invalid")
	}
	paymentRef := strings.TrimSpace(in.PaymentRef)
	if paymentRef == "" {
		return TimelineEntry{}, "", "", merrors.InvalidArgument("payment_ref is required")
	}
	stepRef := strings.TrimSpace(in.StepRef)
	if stepRef == "" {
		return TimelineEntry{}, "", "", merrors.InvalidArgument("step_ref is required")
	}
	if in.Attempt == 0 {
		return TimelineEntry{}, "", "", merrors.InvalidArgument("attempt is required")
	}

	status := strings.ToLower(strings.TrimSpace(in.Status))
	if status == "" {
		status = "received"
	}
	fields := mergeMaps(trimStringMap(in.Fields), map[string]string{
		"source":  string(source),
		"status":  status,
		"refKind": strings.TrimSpace(in.RefKind),
		"ref":     strings.TrimSpace(in.Ref),
	})
	entry := TimelineEntry{
		OccurredAt: now,
		Scope:      ScopeStep,
		PaymentRef: paymentRef,
		StepRef:    stepRef,
		Attempt:    in.Attempt,
		Event:      "external." + string(source) + "." + status,
		State:      status,
		Message:    strings.TrimSpace(in.Message),
		Fields:     trimStringMap(fields),
	}
	return entry, source, status, nil
}

func (s *svc) logPayment(entry TimelineEntry, state agg.State, version uint64) {
	logger := s.logger.With(
		zap.String("payment_ref", entry.PaymentRef),
		zap.String("event", entry.Event),
		zap.String("state", string(state)),
		zap.Uint64("version", version),
		zap.String("scope", string(entry.Scope)),
	)
	if entry.Message != "" {
		logger = logger.With(zap.String("message", entry.Message))
	}
	if len(entry.Fields) > 0 {
		logger = logger.With(zap.Any("fields", entry.Fields))
	}
	switch state {
	case agg.StateFailed, agg.StateNeedsAttention:
		logger.Warn("Orchestration payment event")
	default:
		logger.Info("Orchestration payment event")
	}
}

func (s *svc) logStep(entry TimelineEntry, state agg.StepState, duration time.Duration) {
	logger := s.logger.With(
		zap.String("payment_ref", entry.PaymentRef),
		zap.String("step_ref", entry.StepRef),
		zap.String("step_code", entry.StepCode),
		zap.Uint32("attempt", entry.Attempt),
		zap.String("event", entry.Event),
		zap.String("state", string(state)),
		zap.String("scope", string(entry.Scope)),
	)
	if duration > 0 {
		logger = logger.With(zap.Int64("duration_ms", duration.Milliseconds()))
	}
	if entry.Message != "" {
		logger = logger.With(zap.String("message", entry.Message))
	}
	if len(entry.Fields) > 0 {
		logger = logger.With(zap.Any("fields", entry.Fields))
	}
	switch state {
	case agg.StepStateFailed, agg.StepStateNeedsAttention:
		logger.Warn("Orchestration step event")
	default:
		logger.Info("Orchestration step event")
	}
}

func (s *svc) logExternal(entry TimelineEntry, source ExternalSource, status string) {
	logger := s.logger.With(
		zap.String("payment_ref", entry.PaymentRef),
		zap.String("step_ref", entry.StepRef),
		zap.Uint32("attempt", entry.Attempt),
		zap.String("source", string(source)),
		zap.String("status", status),
		zap.String("event", entry.Event),
		zap.String("scope", string(entry.Scope)),
	)
	if entry.Message != "" {
		logger = logger.With(zap.String("message", entry.Message))
	}
	if len(entry.Fields) > 0 {
		logger = logger.With(zap.Any("fields", entry.Fields))
	}
	if strings.Contains(status, "failed") || strings.Contains(status, "cancel") {
		logger.Warn("Orchestration external event")
		return
	}
	logger.Info("Orchestration external event")
}
