package orders

import (
	"context"
	"crypto/rand"
	"database/sql"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/jackc/pgx/v5"

	"royal-pop-backend/internal/database"
)

var ErrOrderNotFound = errors.New("order not found")

const (
	StatusPending    = "pending"
	StatusPaid       = "paid"
	StatusFailed     = "failed"
	StatusProcessing = "processing"
	StatusCanceled   = "canceled"

	FulfillmentPending    = "pending"
	FulfillmentPaid       = "paid"
	FulfillmentProcessing = "processing"
	FulfillmentPacked     = "packed"
	FulfillmentShipped    = "shipped"
	FulfillmentDelivered  = "delivered"
	FulfillmentCancelled  = "cancelled"
)

type CustomerDetails struct {
	Email        string
	Phone        string
	FirstName    string
	LastName     string
	AddressLine1 string
	AddressLine2 string
	City         string
	Region       string
	PostalCode   string
	Country      string
	Notes        string
}

type LineItem struct {
	ItemID     string
	Style      string
	ColorwayID string
	FinishID   string
	Quantity   int64
	UnitAmount int64
}

type CreateOrderInput struct {
	OrderID               string
	Customer              CustomerDetails
	Items                 []LineItem
	Currency              string
	Amount                int64
	StripePriceID         string
	StripePaymentIntentID string
}

type Order struct {
	ID                    string
	Status                string
	FulfillmentStatus     string
	Email                 string
	Phone                 string
	FirstName             string
	LastName              string
	AddressLine1          string
	AddressLine2          string
	City                  string
	Region                string
	PostalCode            string
	Country               string
	Notes                 string
	Currency              string
	Amount                int64
	StripePriceID         string
	StripePaymentIntentID string
	ShippingCarrier       string
	TrackingNumber        string
	FulfillmentNotes      string
	ShippedAt             *time.Time
	WebhookStatus         string
	WebhookEventID        string
	WebhookEventType      string
	WebhookMessage        string
	Items                 []LineItem
	CreatedAt             time.Time
	UpdatedAt             time.Time
}

type WebhookUpdate struct {
	Status           string
	WebhookStatus    string
	WebhookEventID   string
	WebhookEventType string
	WebhookMessage   string
}

type ListFilter struct {
	Limit             int
	Search            string
	FulfillmentStatus string
}

type UpdateFulfillmentInput struct {
	OrderID           string
	FulfillmentStatus string
	ShippingCarrier   string
	TrackingNumber    string
	FulfillmentNotes  string
	ClearShippedAt    bool
	MarkShippedAtNow  bool
}

type Store struct {
	db *database.DB
}

func NewStore(db *database.DB) *Store {
	return &Store{db: db}
}

func (s *Store) CreatePendingOrder(ctx context.Context, input CreateOrderInput) (*Order, error) {
	if s == nil || s.db == nil || s.db.Pool == nil {
		return nil, fmt.Errorf("postgres store is not configured")
	}

	tx, err := s.db.Pool.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	_, err = tx.Exec(ctx, `
		INSERT INTO orders (
			id, status, email, phone, first_name, last_name,
			address_line_1, address_line_2, city, region, postal_code, country, notes,
			currency, amount, stripe_price_id, stripe_payment_intent_id,
			webhook_status, webhook_message
		) VALUES (
			$1, $2, $3, $4, $5, $6,
			$7, $8, $9, $10, $11, $12, $13,
			$14, $15, $16, $17,
			$18, $19
		)
	`,
		input.OrderID,
		StatusPending,
		input.Customer.Email,
		input.Customer.Phone,
		input.Customer.FirstName,
		input.Customer.LastName,
		input.Customer.AddressLine1,
		input.Customer.AddressLine2,
		input.Customer.City,
		input.Customer.Region,
		input.Customer.PostalCode,
		input.Customer.Country,
		input.Customer.Notes,
		input.Currency,
		input.Amount,
		input.StripePriceID,
		input.StripePaymentIntentID,
		"awaiting_webhook",
		"Waiting for Stripe webhook verification.",
	)
	if err != nil {
		return nil, err
	}

	for _, item := range input.Items {
		quantity := item.Quantity
		if quantity <= 0 {
			quantity = 1
		}

		_, err = tx.Exec(ctx, `
			INSERT INTO order_items (order_id, item_id, style, colorway_id, finish_id, quantity, unit_amount)
			VALUES ($1, $2, $3, $4, $5, $6, $7)
		`, input.OrderID, item.ItemID, item.Style, item.ColorwayID, item.FinishID, quantity, item.UnitAmount)
		if err != nil {
			return nil, err
		}
	}

	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}

	return s.GetByPaymentIntentID(ctx, input.StripePaymentIntentID)
}

func (s *Store) GetByPaymentIntentID(ctx context.Context, paymentIntentID string) (*Order, error) {
	if s == nil || s.db == nil || s.db.Pool == nil {
		return nil, fmt.Errorf("postgres store is not configured")
	}

	row := s.db.Pool.QueryRow(ctx, `
		SELECT id, status, fulfillment_status, email, phone, first_name, last_name,
			address_line_1, address_line_2, city, region, postal_code, country, notes,
			currency, amount, stripe_price_id, stripe_payment_intent_id,
			shipping_carrier, tracking_number, fulfillment_notes, shipped_at,
			webhook_status, webhook_event_id, webhook_event_type, webhook_message,
			created_at, updated_at
		FROM orders
		WHERE stripe_payment_intent_id = $1
	`, paymentIntentID)

	var order Order
	var shippedAt sql.NullTime
	if err := row.Scan(
		&order.ID,
		&order.Status,
		&order.FulfillmentStatus,
		&order.Email,
		&order.Phone,
		&order.FirstName,
		&order.LastName,
		&order.AddressLine1,
		&order.AddressLine2,
		&order.City,
		&order.Region,
		&order.PostalCode,
		&order.Country,
		&order.Notes,
		&order.Currency,
		&order.Amount,
		&order.StripePriceID,
		&order.StripePaymentIntentID,
		&order.ShippingCarrier,
		&order.TrackingNumber,
		&order.FulfillmentNotes,
		&shippedAt,
		&order.WebhookStatus,
		&order.WebhookEventID,
		&order.WebhookEventType,
		&order.WebhookMessage,
		&order.CreatedAt,
		&order.UpdatedAt,
	); err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, ErrOrderNotFound
		}
		return nil, err
	}
	if shippedAt.Valid {
		order.ShippedAt = &shippedAt.Time
	}

	items, err := s.listItemsByOrderID(ctx, order.ID)
	if err != nil {
		return nil, err
	}
	order.Items = items

	return &order, nil
}

func (s *Store) ListRecent(ctx context.Context, limit int) ([]Order, error) {
	if s == nil || s.db == nil || s.db.Pool == nil {
		return nil, fmt.Errorf("postgres store is not configured")
	}

	if limit <= 0 {
		limit = 25
	}
	if limit > 100 {
		limit = 100
	}

	rows, err := s.db.Pool.Query(ctx, `
		SELECT id, status, fulfillment_status, email, phone, first_name, last_name,
			address_line_1, address_line_2, city, region, postal_code, country, notes,
			currency, amount, stripe_price_id, stripe_payment_intent_id,
			shipping_carrier, tracking_number, fulfillment_notes, shipped_at,
			webhook_status, webhook_event_id, webhook_event_type, webhook_message,
			created_at, updated_at
		FROM orders
		ORDER BY created_at DESC
		LIMIT $1
	`, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	ordersList := make([]Order, 0, limit)
	for rows.Next() {
		var order Order
		var shippedAt sql.NullTime
		if err := rows.Scan(
			&order.ID,
			&order.Status,
			&order.FulfillmentStatus,
			&order.Email,
			&order.Phone,
			&order.FirstName,
			&order.LastName,
			&order.AddressLine1,
			&order.AddressLine2,
			&order.City,
			&order.Region,
			&order.PostalCode,
			&order.Country,
			&order.Notes,
			&order.Currency,
			&order.Amount,
			&order.StripePriceID,
			&order.StripePaymentIntentID,
			&order.ShippingCarrier,
			&order.TrackingNumber,
			&order.FulfillmentNotes,
			&shippedAt,
			&order.WebhookStatus,
			&order.WebhookEventID,
			&order.WebhookEventType,
			&order.WebhookMessage,
			&order.CreatedAt,
			&order.UpdatedAt,
		); err != nil {
			return nil, err
		}
		if shippedAt.Valid {
			order.ShippedAt = &shippedAt.Time
		}

		items, err := s.listItemsByOrderID(ctx, order.ID)
		if err != nil {
			return nil, err
		}
		order.Items = items
		ordersList = append(ordersList, order)
	}

	if err := rows.Err(); err != nil {
		return nil, err
	}

	return ordersList, nil
}

func (s *Store) ListForDashboard(ctx context.Context, filter ListFilter) ([]Order, error) {
	if s == nil || s.db == nil || s.db.Pool == nil {
		return nil, fmt.Errorf("postgres store is not configured")
	}

	limit := filter.Limit
	if limit <= 0 {
		limit = 50
	}
	if limit > 200 {
		limit = 200
	}

	search := strings.TrimSpace(filter.Search)
	fulfillmentStatus := strings.TrimSpace(strings.ToLower(filter.FulfillmentStatus))

	rows, err := s.db.Pool.Query(ctx, `
		SELECT id, status, fulfillment_status, email, phone, first_name, last_name,
			address_line_1, address_line_2, city, region, postal_code, country, notes,
			currency, amount, stripe_price_id, stripe_payment_intent_id,
			shipping_carrier, tracking_number, fulfillment_notes, shipped_at,
			webhook_status, webhook_event_id, webhook_event_type, webhook_message,
			created_at, updated_at
		FROM orders
		WHERE ($1 = '' OR fulfillment_status = $1)
		  AND (
			$2 = ''
			OR id ILIKE '%' || $2 || '%'
			OR first_name ILIKE '%' || $2 || '%'
			OR last_name ILIKE '%' || $2 || '%'
			OR email ILIKE '%' || $2 || '%'
			OR tracking_number ILIKE '%' || $2 || '%'
		  )
		ORDER BY created_at DESC
		LIMIT $3
	`, fulfillmentStatus, search, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	ordersList := make([]Order, 0, limit)
	for rows.Next() {
		var order Order
		var shippedAt sql.NullTime
		if err := rows.Scan(
			&order.ID,
			&order.Status,
			&order.FulfillmentStatus,
			&order.Email,
			&order.Phone,
			&order.FirstName,
			&order.LastName,
			&order.AddressLine1,
			&order.AddressLine2,
			&order.City,
			&order.Region,
			&order.PostalCode,
			&order.Country,
			&order.Notes,
			&order.Currency,
			&order.Amount,
			&order.StripePriceID,
			&order.StripePaymentIntentID,
			&order.ShippingCarrier,
			&order.TrackingNumber,
			&order.FulfillmentNotes,
			&shippedAt,
			&order.WebhookStatus,
			&order.WebhookEventID,
			&order.WebhookEventType,
			&order.WebhookMessage,
			&order.CreatedAt,
			&order.UpdatedAt,
		); err != nil {
			return nil, err
		}
		if shippedAt.Valid {
			order.ShippedAt = &shippedAt.Time
		}

		items, err := s.listItemsByOrderID(ctx, order.ID)
		if err != nil {
			return nil, err
		}
		order.Items = items
		ordersList = append(ordersList, order)
	}

	if err := rows.Err(); err != nil {
		return nil, err
	}

	return ordersList, nil
}

func (s *Store) GetByID(ctx context.Context, orderID string) (*Order, error) {
	if s == nil || s.db == nil || s.db.Pool == nil {
		return nil, fmt.Errorf("postgres store is not configured")
	}

	row := s.db.Pool.QueryRow(ctx, `
		SELECT id, status, fulfillment_status, email, phone, first_name, last_name,
			address_line_1, address_line_2, city, region, postal_code, country, notes,
			currency, amount, stripe_price_id, stripe_payment_intent_id,
			shipping_carrier, tracking_number, fulfillment_notes, shipped_at,
			webhook_status, webhook_event_id, webhook_event_type, webhook_message,
			created_at, updated_at
		FROM orders
		WHERE id = $1
	`, strings.TrimSpace(orderID))

	var order Order
	var shippedAt sql.NullTime
	if err := row.Scan(
		&order.ID,
		&order.Status,
		&order.FulfillmentStatus,
		&order.Email,
		&order.Phone,
		&order.FirstName,
		&order.LastName,
		&order.AddressLine1,
		&order.AddressLine2,
		&order.City,
		&order.Region,
		&order.PostalCode,
		&order.Country,
		&order.Notes,
		&order.Currency,
		&order.Amount,
		&order.StripePriceID,
		&order.StripePaymentIntentID,
		&order.ShippingCarrier,
		&order.TrackingNumber,
		&order.FulfillmentNotes,
		&shippedAt,
		&order.WebhookStatus,
		&order.WebhookEventID,
		&order.WebhookEventType,
		&order.WebhookMessage,
		&order.CreatedAt,
		&order.UpdatedAt,
	); err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil, ErrOrderNotFound
		}
		return nil, err
	}
	if shippedAt.Valid {
		order.ShippedAt = &shippedAt.Time
	}

	items, err := s.listItemsByOrderID(ctx, order.ID)
	if err != nil {
		return nil, err
	}
	order.Items = items

	return &order, nil
}

func (s *Store) UpdateFulfillment(ctx context.Context, input UpdateFulfillmentInput) (*Order, error) {
	if s == nil || s.db == nil || s.db.Pool == nil {
		return nil, fmt.Errorf("postgres store is not configured")
	}

	status := normalizeFulfillmentStatus(input.FulfillmentStatus)
	if status == "" {
		return nil, fmt.Errorf("invalid fulfillment status")
	}

	trimmedOrderID := strings.TrimSpace(input.OrderID)
	shippingCarrier := strings.TrimSpace(input.ShippingCarrier)
	trackingNumber := strings.TrimSpace(input.TrackingNumber)
	fulfillmentNotes := strings.TrimSpace(input.FulfillmentNotes)

	var shippedAt any
	if input.MarkShippedAtNow {
		shippedAt = time.Now().UTC()
	}

	commandTag, err := s.db.Pool.Exec(ctx, `
		UPDATE orders
		SET fulfillment_status = $2,
			shipping_carrier = $3,
			tracking_number = $4,
			fulfillment_notes = $5,
			shipped_at = CASE
				WHEN $6::timestamptz IS NOT NULL THEN $6::timestamptz
				WHEN $7 THEN NULL
				ELSE shipped_at
			END,
			updated_at = NOW()
		WHERE id = $1
	`, trimmedOrderID, status, shippingCarrier, trackingNumber, fulfillmentNotes, shippedAt, input.ClearShippedAt)
	if err != nil {
		return nil, err
	}
	if commandTag.RowsAffected() == 0 {
		return nil, ErrOrderNotFound
	}

	return s.GetByID(ctx, trimmedOrderID)
}

func (s *Store) UpdateFromWebhook(ctx context.Context, paymentIntentID string, update WebhookUpdate) error {
	if s == nil || s.db == nil || s.db.Pool == nil {
		return fmt.Errorf("postgres store is not configured")
	}

	fulfillmentStatus := ""
	switch update.Status {
	case StatusPaid:
		fulfillmentStatus = FulfillmentPaid
	case StatusFailed, StatusCanceled:
		fulfillmentStatus = FulfillmentCancelled
	}

	commandTag, err := s.db.Pool.Exec(ctx, `
		UPDATE orders
		SET status = $2,
			fulfillment_status = CASE
				WHEN $7 <> '' AND (fulfillment_status = '' OR fulfillment_status = $8 OR fulfillment_status = $9)
				THEN $7
				ELSE fulfillment_status
			END,
			webhook_status = $3,
			webhook_event_id = $4,
			webhook_event_type = $5,
			webhook_message = $6,
			updated_at = NOW()
		WHERE stripe_payment_intent_id = $1
	`, paymentIntentID, update.Status, update.WebhookStatus, update.WebhookEventID, update.WebhookEventType, update.WebhookMessage, fulfillmentStatus, FulfillmentPending, FulfillmentPaid)
	if err != nil {
		return err
	}

	if commandTag.RowsAffected() == 0 {
		return ErrOrderNotFound
	}

	return nil
}

func NewOrderID() string {
	raw := make([]byte, 8)
	_, _ = rand.Read(raw)
	return "rpo_" + hex.EncodeToString(raw)
}

func (s *Store) listItemsByOrderID(ctx context.Context, orderID string) ([]LineItem, error) {
	rows, err := s.db.Pool.Query(ctx, `
		SELECT item_id, style, colorway_id, finish_id, quantity, unit_amount
		FROM order_items
		WHERE order_id = $1
		ORDER BY id ASC
	`, orderID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	items := make([]LineItem, 0)
	for rows.Next() {
		var item LineItem
		if err := rows.Scan(
			&item.ItemID,
			&item.Style,
			&item.ColorwayID,
			&item.FinishID,
			&item.Quantity,
			&item.UnitAmount,
		); err != nil {
			return nil, err
		}
		items = append(items, item)
	}

	if err := rows.Err(); err != nil {
		return nil, err
	}

	return items, nil
}

func normalizeFulfillmentStatus(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case FulfillmentPending:
		return FulfillmentPending
	case FulfillmentPaid:
		return FulfillmentPaid
	case FulfillmentProcessing:
		return FulfillmentProcessing
	case FulfillmentPacked:
		return FulfillmentPacked
	case FulfillmentShipped:
		return FulfillmentShipped
	case FulfillmentDelivered:
		return FulfillmentDelivered
	case FulfillmentCancelled, StatusCanceled:
		return FulfillmentCancelled
	default:
		return ""
	}
}
