// Workflow example: cross-service orchestration via AI agents.
//
// This example runs three services (Inventory, Orders, Notifications) and
// demonstrates how an AI agent can orchestrate a multi-step workflow:
//
//  1. Check inventory for a product
//  2. Place an order if in stock
//  3. Send a confirmation notification
//
// The agent figures out the right sequence of calls on its own — no
// workflow engine, no glue code, just natural language.
//
// Run:
//
//	go run .
//
// MCP tools: http://localhost:3001/mcp/tools
package main

import (
	"context"
	"fmt"
	"log"
	"strings"
	"sync"
	"time"

	"go-micro.dev/v6"
	"go-micro.dev/v6/gateway/mcp"
)

// ---------------------------------------------------------------------------
// Inventory service
// ---------------------------------------------------------------------------

type Product struct {
	SKU      string  `json:"sku" description:"Stock keeping unit identifier"`
	Name     string  `json:"name" description:"Product name"`
	Price    float64 `json:"price" description:"Unit price in USD"`
	InStock  int     `json:"in_stock" description:"Number of units available"`
	Category string  `json:"category" description:"Product category"`
}

type CheckStockRequest struct {
	SKU string `json:"sku" description:"Product SKU to check"`
}

type CheckStockResponse struct {
	Product *Product `json:"product" description:"Product details with current stock level"`
}

type SearchProductsRequest struct {
	Query    string `json:"query" description:"Search term to match against product name or category"`
	Category string `json:"category,omitempty" description:"Filter by category: electronics, clothing, books (optional)"`
}

type SearchProductsResponse struct {
	Products []*Product `json:"products" description:"Products matching the search criteria"`
}

type ReserveStockRequest struct {
	SKU      string `json:"sku" description:"Product SKU to reserve"`
	Quantity int    `json:"quantity" description:"Number of units to reserve"`
}

type ReserveStockResponse struct {
	Reserved  bool   `json:"reserved" description:"True if stock was successfully reserved"`
	Remaining int    `json:"remaining" description:"Units remaining after reservation"`
	Message   string `json:"message" description:"Human-readable result message"`
}

type InventoryService struct {
	mu       sync.RWMutex
	products map[string]*Product
}

// CheckStock returns the current stock level for a product.
// Use this before placing an order to verify availability.
//
// @example {"sku": "LAPTOP-001"}
func (s *InventoryService) CheckStock(ctx context.Context, req *CheckStockRequest, rsp *CheckStockResponse) error {
	s.mu.RLock()
	defer s.mu.RUnlock()
	p, ok := s.products[req.SKU]
	if !ok {
		return fmt.Errorf("product %s not found", req.SKU)
	}
	rsp.Product = p
	return nil
}

// Search finds products by name or category. Use this to help
// users find what they're looking for before checking stock.
//
// @example {"query": "laptop"}
func (s *InventoryService) Search(ctx context.Context, req *SearchProductsRequest, rsp *SearchProductsResponse) error {
	s.mu.RLock()
	defer s.mu.RUnlock()
	q := strings.ToLower(req.Query)
	for _, p := range s.products {
		if req.Category != "" && !strings.EqualFold(p.Category, req.Category) {
			continue
		}
		if q == "" || strings.Contains(strings.ToLower(p.Name), q) || strings.Contains(strings.ToLower(p.Category), q) {
			rsp.Products = append(rsp.Products, p)
		}
	}
	return nil
}

// ReserveStock decrements inventory for a product. Call this after
// confirming stock is available. Returns an error if insufficient stock.
//
// @example {"sku": "LAPTOP-001", "quantity": 1}
func (s *InventoryService) ReserveStock(ctx context.Context, req *ReserveStockRequest, rsp *ReserveStockResponse) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	p, ok := s.products[req.SKU]
	if !ok {
		return fmt.Errorf("product %s not found", req.SKU)
	}
	if p.InStock < req.Quantity {
		rsp.Reserved = false
		rsp.Remaining = p.InStock
		rsp.Message = fmt.Sprintf("insufficient stock: requested %d but only %d available", req.Quantity, p.InStock)
		return nil
	}
	p.InStock -= req.Quantity
	rsp.Reserved = true
	rsp.Remaining = p.InStock
	rsp.Message = fmt.Sprintf("reserved %d units of %s", req.Quantity, p.Name)
	return nil
}

// ---------------------------------------------------------------------------
// Orders service
// ---------------------------------------------------------------------------

type Order struct {
	ID        string    `json:"id" description:"Unique order identifier"`
	Customer  string    `json:"customer" description:"Customer name or email"`
	SKU       string    `json:"sku" description:"Product SKU ordered"`
	Quantity  int       `json:"quantity" description:"Number of units"`
	Total     float64   `json:"total" description:"Total order amount in USD"`
	Status    string    `json:"status" description:"Order status: pending, confirmed, shipped, delivered"`
	CreatedAt time.Time `json:"created_at" description:"When the order was placed"`
}

type PlaceOrderRequest struct {
	Customer string `json:"customer" description:"Customer name or email (required)"`
	SKU      string `json:"sku" description:"Product SKU to order (required)"`
	Quantity int    `json:"quantity" description:"Number of units (required, must be positive)"`
}

type PlaceOrderResponse struct {
	Order *Order `json:"order" description:"The newly created order"`
}

type GetOrderRequest struct {
	ID string `json:"id" description:"Order ID to look up"`
}

type GetOrderResponse struct {
	Order *Order `json:"order" description:"The requested order"`
}

type ListOrdersRequest struct {
	Customer string `json:"customer,omitempty" description:"Filter by customer (optional)"`
	Status   string `json:"status,omitempty" description:"Filter by status (optional)"`
}

type ListOrdersResponse struct {
	Orders []*Order `json:"orders" description:"Matching orders"`
}

type OrderService struct {
	mu     sync.RWMutex
	orders map[string]*Order
	nextID int
	// In a real app this would be a client to the inventory service
	inventory *InventoryService
}

// PlaceOrder creates a new order. Stock must be reserved first via
// InventoryService.ReserveStock — this service does not check inventory.
//
// @example {"customer": "alice@example.com", "sku": "LAPTOP-001", "quantity": 1}
func (s *OrderService) PlaceOrder(ctx context.Context, req *PlaceOrderRequest, rsp *PlaceOrderResponse) error {
	if req.Customer == "" {
		return fmt.Errorf("customer is required")
	}
	if req.SKU == "" {
		return fmt.Errorf("sku is required")
	}
	if req.Quantity <= 0 {
		return fmt.Errorf("quantity must be positive")
	}

	// Look up price
	s.inventory.mu.RLock()
	p, ok := s.inventory.products[req.SKU]
	s.inventory.mu.RUnlock()
	if !ok {
		return fmt.Errorf("product %s not found", req.SKU)
	}

	s.mu.Lock()
	defer s.mu.Unlock()
	s.nextID++
	order := &Order{
		ID:        fmt.Sprintf("ORD-%04d", s.nextID),
		Customer:  req.Customer,
		SKU:       req.SKU,
		Quantity:  req.Quantity,
		Total:     p.Price * float64(req.Quantity),
		Status:    "confirmed",
		CreatedAt: time.Now(),
	}
	s.orders[order.ID] = order
	rsp.Order = order
	return nil
}

// GetOrder retrieves an order by ID.
//
// @example {"id": "ORD-0001"}
func (s *OrderService) GetOrder(ctx context.Context, req *GetOrderRequest, rsp *GetOrderResponse) error {
	s.mu.RLock()
	defer s.mu.RUnlock()
	o, ok := s.orders[req.ID]
	if !ok {
		return fmt.Errorf("order %s not found", req.ID)
	}
	rsp.Order = o
	return nil
}

// ListOrders returns orders, optionally filtered by customer or status.
//
// @example {"customer": "alice@example.com"}
func (s *OrderService) ListOrders(ctx context.Context, req *ListOrdersRequest, rsp *ListOrdersResponse) error {
	s.mu.RLock()
	defer s.mu.RUnlock()
	for _, o := range s.orders {
		if req.Customer != "" && o.Customer != req.Customer {
			continue
		}
		if req.Status != "" && o.Status != req.Status {
			continue
		}
		rsp.Orders = append(rsp.Orders, o)
	}
	return nil
}

// ---------------------------------------------------------------------------
// Notifications service
// ---------------------------------------------------------------------------

type Notification struct {
	ID        string    `json:"id" description:"Notification identifier"`
	Recipient string    `json:"recipient" description:"Who received the notification"`
	Subject   string    `json:"subject" description:"Notification subject line"`
	Body      string    `json:"body" description:"Notification body text"`
	Channel   string    `json:"channel" description:"Delivery channel: email, sms, or slack"`
	SentAt    time.Time `json:"sent_at" description:"When the notification was sent"`
}

type SendNotificationRequest struct {
	Recipient string `json:"recipient" description:"Email address, phone number, or Slack handle"`
	Subject   string `json:"subject" description:"Subject line (required)"`
	Body      string `json:"body" description:"Message body (required)"`
	Channel   string `json:"channel,omitempty" description:"Channel: email (default), sms, or slack"`
}

type SendNotificationResponse struct {
	Notification *Notification `json:"notification" description:"The sent notification with delivery details"`
}

type ListNotificationsRequest struct {
	Recipient string `json:"recipient,omitempty" description:"Filter by recipient (optional)"`
}

type ListNotificationsResponse struct {
	Notifications []*Notification `json:"notifications" description:"Sent notifications"`
}

type NotificationService struct {
	mu            sync.RWMutex
	notifications []*Notification
	nextID        int
}

// Send delivers a notification to a recipient via the specified channel.
// Use this to confirm orders, alert users, or send updates.
// Defaults to email if no channel is specified.
//
// @example {"recipient": "alice@example.com", "subject": "Order Confirmed", "body": "Your order ORD-0001 has been confirmed.", "channel": "email"}
func (s *NotificationService) Send(ctx context.Context, req *SendNotificationRequest, rsp *SendNotificationResponse) error {
	if req.Recipient == "" {
		return fmt.Errorf("recipient is required")
	}
	if req.Subject == "" {
		return fmt.Errorf("subject is required")
	}
	if req.Body == "" {
		return fmt.Errorf("body is required")
	}
	channel := req.Channel
	if channel == "" {
		channel = "email"
	}

	s.mu.Lock()
	defer s.mu.Unlock()
	s.nextID++
	n := &Notification{
		ID:        fmt.Sprintf("notif-%d", s.nextID),
		Recipient: req.Recipient,
		Subject:   req.Subject,
		Body:      req.Body,
		Channel:   channel,
		SentAt:    time.Now(),
	}
	s.notifications = append(s.notifications, n)
	rsp.Notification = n
	return nil
}

// List returns sent notifications, optionally filtered by recipient.
//
// @example {"recipient": "alice@example.com"}
func (s *NotificationService) List(ctx context.Context, req *ListNotificationsRequest, rsp *ListNotificationsResponse) error {
	s.mu.RLock()
	defer s.mu.RUnlock()
	for _, n := range s.notifications {
		if req.Recipient != "" && n.Recipient != req.Recipient {
			continue
		}
		rsp.Notifications = append(rsp.Notifications, n)
	}
	return nil
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

func main() {
	service := micro.NewService("shop",
		micro.Address(":9090"),
		mcp.WithMCP(":3001"),
	)
	service.Init()

	inventory := &InventoryService{products: map[string]*Product{
		"LAPTOP-001": {SKU: "LAPTOP-001", Name: "ThinkPad X1 Carbon", Price: 1299.99, InStock: 15, Category: "electronics"},
		"LAPTOP-002": {SKU: "LAPTOP-002", Name: "MacBook Air M3", Price: 1099.00, InStock: 8, Category: "electronics"},
		"PHONE-001":  {SKU: "PHONE-001", Name: "Pixel 8 Pro", Price: 899.00, InStock: 23, Category: "electronics"},
		"BOOK-001":   {SKU: "BOOK-001", Name: "Designing Data-Intensive Applications", Price: 45.99, InStock: 50, Category: "books"},
		"BOOK-002":   {SKU: "BOOK-002", Name: "The Go Programming Language", Price: 39.99, InStock: 0, Category: "books"},
		"SHIRT-001":  {SKU: "SHIRT-001", Name: "Go Gopher T-Shirt", Price: 24.99, InStock: 100, Category: "clothing"},
	}}

	orders := &OrderService{
		orders:    make(map[string]*Order),
		inventory: inventory,
	}

	notifications := &NotificationService{}

	service.Handle(inventory)
	service.Handle(orders)
	service.Handle(notifications)

	fmt.Println()
	fmt.Println("  Shop Workflow Demo")
	fmt.Println()
	fmt.Println("  MCP Tools:  http://localhost:3001/mcp/tools")
	fmt.Println()
	fmt.Println("  Try asking an agent:")
	fmt.Println()
	fmt.Println("    \"What laptops do you have in stock?\"")
	fmt.Println("    \"Order a ThinkPad for alice@example.com and send her a confirmation\"")
	fmt.Println("    \"Check if 'The Go Programming Language' is available\"")
	fmt.Println("    \"Show me all orders for alice@example.com\"")
	fmt.Println("    \"Order 3 Go Gopher t-shirts for bob@example.com, reserve the stock, and notify him\"")
	fmt.Println()

	if err := service.Run(); err != nil {
		log.Fatal(err)
	}
}
