package commerce

import (
	"context"
	"encoding/json"
	"errors"
	"strings"
	"time"

	"gorm.io/gorm"
)

type NotificationQuery struct {
	Category      string
	Level         string
	UnreadOnly    bool
	ImportantOnly bool
	ArchivedOnly  bool
	HandledState  string
	Limit         int
	Offset        int
}

type NotificationBatchRequest struct {
	IDs []int64 `json:"ids"`
}

type NotificationArchiveRequest struct {
	IDs      []int64 `json:"ids"`
	Archived bool    `json:"archived"`
}

type NotificationHandledRequest struct {
	IDs     []int64 `json:"ids"`
	Handled bool    `json:"handled"`
}

type NotificationPreferenceUpdateRequest struct {
	Preferences []NotificationPreferenceInput `json:"preferences"`
}

type NotificationPreferenceInput struct {
	Category     string `json:"category"`
	InAppEnabled bool   `json:"in_app_enabled"`
	EmailEnabled bool   `json:"email_enabled"`
	SMSEnabled   bool   `json:"sms_enabled"`
}

var notificationCategories = []string{"system", "order", "instance", "finance", "ticket", "promotion", "membership", "announcement"}

func (s *Service) ListNotifications(ctx context.Context, userID int64, query NotificationQuery) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	db := s.db.WithContext(ctx).Model(&Notification{}).Where("user_id = ?", userID)
	if query.Category != "" {
		db = db.Where("category = ?", query.Category)
	}
	if query.Level != "" {
		db = db.Where("level = ?", query.Level)
	}
	if query.ImportantOnly {
		db = db.Where("level IN ?", []string{"warning", "error", "urgent"})
	}
	if query.UnreadOnly {
		db = db.Where("is_read = ?", false)
	}
	if query.ArchivedOnly {
		db = db.Where("archived_at IS NOT NULL")
	} else {
		db = db.Where("archived_at IS NULL")
	}
	switch query.HandledState {
	case "handled":
		db = db.Where("handled_at IS NOT NULL")
	case "unhandled":
		db = db.Where("handled_at IS NULL")
	}

	var total int64
	if err := db.Count(&total).Error; err != nil {
		return nil, err
	}
	var items []Notification
	if err := db.Order("id DESC").Limit(query.Limit).Offset(query.Offset).Find(&items).Error; err != nil {
		return nil, err
	}
	out := make([]map[string]any, 0, len(items))
	for _, item := range items {
		out = append(out, notificationMap(item))
	}
	return map[string]any{"items": out, "total": total}, nil
}

func (s *Service) NotificationUnreadCount(ctx context.Context, userID int64) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	rows := []struct {
		Category string
		Count    int64
	}{}
	if err := s.db.WithContext(ctx).Model(&Notification{}).
		Select("category, COUNT(*) as count").
		Where("user_id = ? AND is_read = ? AND archived_at IS NULL", userID, false).
		Group("category").
		Scan(&rows).Error; err != nil {
		return nil, err
	}
	byCategory := map[string]int64{}
	notificationTotal := int64(0)
	for _, row := range rows {
		byCategory[row.Category] = row.Count
		notificationTotal += row.Count
	}
	now := time.Now().UTC()
	var announcementTotal int64
	err := s.db.WithContext(ctx).Table("announcements AS a").
		Where("a.is_published = ? AND (a.published_at IS NULL OR a.published_at <= ?) AND (a.expires_at IS NULL OR a.expires_at > ?)", true, now, now).
		Where("NOT EXISTS (SELECT 1 FROM announcement_reads ar WHERE ar.announcement_id = a.id AND ar.user_id = ?)", userID).
		Count(&announcementTotal).Error
	if err != nil {
		return nil, err
	}
	total := notificationTotal + announcementTotal
	return map[string]any{
		"notifications": notificationTotal,
		"announcements": announcementTotal,
		"total":         total,
		"by_category":   byCategory,
	}, nil
}

func (s *Service) MarkNotificationRead(ctx context.Context, userID int64, id int64) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	now := time.Now().UTC()
	result := s.db.WithContext(ctx).Model(&Notification{}).Where("id = ? AND user_id = ?", id, userID).Updates(map[string]any{"is_read": true, "read_at": now})
	return map[string]any{"ok": result.RowsAffected > 0}, result.Error
}

func (s *Service) MarkAllNotificationsRead(ctx context.Context, userID int64) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	now := time.Now().UTC()
	result := s.db.WithContext(ctx).Model(&Notification{}).Where("user_id = ? AND is_read = ?", userID, false).Updates(map[string]any{"is_read": true, "read_at": now})
	return map[string]any{"count": result.RowsAffected}, result.Error
}

func (s *Service) MarkManyNotificationsRead(ctx context.Context, userID int64, ids []int64) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	if len(ids) == 0 {
		return map[string]any{"count": 0}, nil
	}
	now := time.Now().UTC()
	result := s.db.WithContext(ctx).Model(&Notification{}).Where("user_id = ? AND id IN ?", userID, ids).Updates(map[string]any{"is_read": true, "read_at": now})
	return map[string]any{"count": result.RowsAffected}, result.Error
}

func (s *Service) DeleteNotification(ctx context.Context, userID int64, id int64) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	result := s.db.WithContext(ctx).Where("id = ? AND user_id = ?", id, userID).Delete(&Notification{})
	return map[string]any{"ok": result.RowsAffected > 0}, result.Error
}

func (s *Service) DeleteManyNotifications(ctx context.Context, userID int64, ids []int64) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	if len(ids) == 0 {
		return map[string]any{"count": 0}, nil
	}
	result := s.db.WithContext(ctx).Where("user_id = ? AND id IN ?", userID, ids).Delete(&Notification{})
	return map[string]any{"count": result.RowsAffected}, result.Error
}

func (s *Service) SetManyNotificationsArchived(ctx context.Context, userID int64, ids []int64, archived bool) (map[string]any, error) {
	return s.setManyNotificationTime(ctx, userID, ids, "archived_at", archived)
}

func (s *Service) SetManyNotificationsHandled(ctx context.Context, userID int64, ids []int64, handled bool) (map[string]any, error) {
	return s.setManyNotificationTime(ctx, userID, ids, "handled_at", handled)
}

func (s *Service) setManyNotificationTime(ctx context.Context, userID int64, ids []int64, column string, enabled bool) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	if len(ids) == 0 {
		return map[string]any{"count": 0}, nil
	}
	var value any
	if enabled {
		now := time.Now().UTC()
		value = &now
	}
	result := s.db.WithContext(ctx).Model(&Notification{}).Where("user_id = ? AND id IN ?", userID, ids).Update(column, value)
	return map[string]any{"count": result.RowsAffected}, result.Error
}

func (s *Service) NotificationPreferences(ctx context.Context, userID int64) ([]map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	var prefs []NotificationPreference
	if err := s.db.WithContext(ctx).Where("user_id = ?", userID).Find(&prefs).Error; err != nil {
		return nil, err
	}
	byCategory := map[string]NotificationPreference{}
	for _, pref := range prefs {
		byCategory[pref.Category] = pref
	}
	out := make([]map[string]any, 0, len(notificationCategories))
	for _, category := range notificationCategories {
		pref, ok := byCategory[category]
		item := map[string]any{"category": category, "in_app_enabled": true, "email_enabled": false, "sms_enabled": false}
		if ok {
			item["in_app_enabled"] = pref.InAppEnabled
			item["email_enabled"] = pref.EmailEnabled
			item["sms_enabled"] = pref.SMSEnabled
		}
		out = append(out, item)
	}
	return out, nil
}

func (s *Service) UpdateNotificationPreferences(ctx context.Context, userID int64, inputs []NotificationPreferenceInput) ([]map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		for _, input := range inputs {
			category := strings.TrimSpace(input.Category)
			if category == "" {
				continue
			}
			var pref NotificationPreference
			err := tx.Where("user_id = ? AND category = ?", userID, category).First(&pref).Error
			if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
				return err
			}
			if errors.Is(err, gorm.ErrRecordNotFound) {
				pref = NotificationPreference{UserID: userID, Category: category}
			}
			pref.InAppEnabled = input.InAppEnabled
			pref.EmailEnabled = input.EmailEnabled
			pref.SMSEnabled = input.SMSEnabled
			if errors.Is(err, gorm.ErrRecordNotFound) {
				if err := tx.Create(&pref).Error; err != nil {
					return err
				}
			} else if err := tx.Save(&pref).Error; err != nil {
				return err
			}
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return s.NotificationPreferences(ctx, userID)
}

func (s *Service) ListAnnouncements(ctx context.Context, userID int64, limit int, offset int) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	now := time.Now().UTC()
	db := s.db.WithContext(ctx).Table("announcements").
		Where("is_published = ? AND (published_at IS NULL OR published_at <= ?) AND (expires_at IS NULL OR expires_at > ?)", true, now, now)
	var total int64
	if err := db.Count(&total).Error; err != nil {
		return nil, err
	}
	var items []announcementRow
	if err := db.Order("is_pinned DESC, published_at DESC, id DESC").Limit(limit).Offset(offset).Find(&items).Error; err != nil {
		return nil, err
	}
	ids := make([]int64, 0, len(items))
	for _, item := range items {
		ids = append(ids, item.ID)
	}
	readSet := map[int64]struct{}{}
	if len(ids) > 0 {
		var reads []AnnouncementRead
		if err := s.db.WithContext(ctx).Where("user_id = ? AND announcement_id IN ?", userID, ids).Find(&reads).Error; err != nil {
			return nil, err
		}
		for _, read := range reads {
			readSet[read.AnnouncementID] = struct{}{}
		}
	}
	out := make([]map[string]any, 0, len(items))
	for _, item := range items {
		row := announcementRowMap(item)
		_, read := readSet[item.ID]
		row["is_read"] = read
		out = append(out, row)
	}
	return map[string]any{"items": out, "total": total}, nil
}

func (s *Service) MarkAnnouncementRead(ctx context.Context, userID int64, id int64) (map[string]any, error) {
	if err := s.requireDB(); err != nil {
		return nil, err
	}
	var existing AnnouncementRead
	err := s.db.WithContext(ctx).Where("user_id = ? AND announcement_id = ?", userID, id).First(&existing).Error
	if err == nil {
		return map[string]any{"ok": true}, nil
	}
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}
	read := AnnouncementRead{UserID: userID, AnnouncementID: id, ReadAt: time.Now().UTC()}
	if err := s.db.WithContext(ctx).Create(&read).Error; err != nil {
		return nil, err
	}
	return map[string]any{"ok": true}, nil
}

func notificationMap(item Notification) map[string]any {
	actionURL := item.ActionURL
	if actionURL == nil {
		actionURL = item.Link
	}
	return map[string]any{
		"id":           item.ID,
		"category":     item.Category,
		"level":        item.Level,
		"sender_id":    item.SenderID,
		"sender_name":  firstNonEmptyPtr(item.SenderName, notificationSourceName(item.Category)),
		"sender_type":  item.SenderType,
		"source_label": notificationSourceName(item.Category),
		"title":        item.Title,
		"content":      item.Content,
		"link":         item.Link,
		"action_label": item.ActionLabel,
		"action_url":   actionURL,
		"action": func() any {
			if actionURL == nil {
				return nil
			}
			label := "View"
			if item.ActionLabel != nil && *item.ActionLabel != "" {
				label = *item.ActionLabel
			}
			return map[string]any{"label": label, "url": actionURL}
		}(),
		"is_read":     item.IsRead,
		"read_at":     item.ReadAt,
		"handled_at":  item.HandledAt,
		"archived_at": item.ArchivedAt,
		"created_at":  item.CreatedAt,
	}
}

func announcementMap(item Announcement) map[string]any {
	actionURL := item.ActionURL
	if actionURL == nil {
		actionURL = item.Link
	}
	return map[string]any{
		"id":              item.ID,
		"title":           item.Title,
		"content":         item.Content,
		"category":        firstNonEmpty(item.Category, "announcement"),
		"level":           item.Level,
		"link":            item.Link,
		"action_label":    item.ActionLabel,
		"action_url":      actionURL,
		"target_type":     firstNonEmpty(item.TargetType, item.TargetAudience, "all"),
		"target_user_ids": jsonAnyValue(item.TargetUserIDs),
		"is_published":    item.IsPublished,
		"is_pinned":       item.IsPinned,
		"published_at":    item.PublishedAt,
		"expires_at":      item.ExpiresAt,
		"created_by":      firstNonNilInt64(item.CreatedBy, item.CreatedByID),
		"target_audience": item.TargetAudience,
		"created_at":      item.CreatedAt,
	}
}

type announcementRow struct {
	ID             int64      `gorm:"column:id"`
	Title          string     `gorm:"column:title"`
	Content        string     `gorm:"column:content"`
	Category       string     `gorm:"column:category"`
	Level          string     `gorm:"column:level"`
	Link           *string    `gorm:"column:link"`
	ActionLabel    *string    `gorm:"column:action_label"`
	ActionURL      *string    `gorm:"column:action_url"`
	TargetType     string     `gorm:"column:target_type"`
	TargetUserIDs  *string    `gorm:"column:target_user_ids"`
	IsPinned       bool       `gorm:"column:is_pinned"`
	IsPublished    bool       `gorm:"column:is_published"`
	PublishedAt    *time.Time `gorm:"column:published_at"`
	ExpiresAt      *time.Time `gorm:"column:expires_at"`
	CreatedBy      *int64     `gorm:"column:created_by"`
	CreatedByID    *int64     `gorm:"column:created_by_id"`
	TargetAudience string     `gorm:"column:target_audience"`
	CreatedAt      time.Time  `gorm:"column:created_at"`
}

func announcementRowMap(item announcementRow) map[string]any {
	actionURL := item.ActionURL
	if actionURL == nil {
		actionURL = item.Link
	}
	return map[string]any{
		"id":              item.ID,
		"title":           item.Title,
		"content":         item.Content,
		"category":        firstNonEmpty(item.Category, "announcement"),
		"level":           firstNonEmpty(item.Level, "info"),
		"link":            item.Link,
		"action_label":    item.ActionLabel,
		"action_url":      actionURL,
		"target_type":     firstNonEmpty(item.TargetType, item.TargetAudience, "all"),
		"target_user_ids": announcementTargetUserIDs(item.TargetUserIDs),
		"is_published":    item.IsPublished,
		"is_pinned":       item.IsPinned,
		"published_at":    item.PublishedAt,
		"expires_at":      item.ExpiresAt,
		"created_by":      firstNonNilInt64(item.CreatedBy, item.CreatedByID),
		"target_audience": firstNonEmpty(item.TargetAudience, item.TargetType, "all"),
		"created_at":      item.CreatedAt,
	}
}

func announcementTargetUserIDs(raw *string) any {
	if raw == nil || strings.TrimSpace(*raw) == "" || strings.TrimSpace(*raw) == "null" {
		return nil
	}
	var out any
	if err := json.Unmarshal([]byte(*raw), &out); err != nil {
		return nil
	}
	return out
}

func notificationSourceName(category string) string {
	switch category {
	case "order":
		return "Orders"
	case "instance":
		return "Instances"
	case "finance":
		return "Finance"
	case "ticket":
		return "Tickets"
	case "promotion":
		return "Promotion"
	case "membership":
		return "Membership"
	case "announcement":
		return "Announcements"
	default:
		return "System"
	}
}

func firstNonEmptyPtr(value *string, fallback string) string {
	if value != nil && strings.TrimSpace(*value) != "" {
		return *value
	}
	return fallback
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return value
		}
	}
	return ""
}

func firstNonNilInt64(values ...*int64) *int64 {
	for _, value := range values {
		if value != nil {
			return value
		}
	}
	return nil
}
