package services

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strconv"
	"time"

	"github.com/rs/zerolog/log"
	"gorm.io/gorm"

	"github.com/treytartt/honeydue-api/internal/apperrors"
	"github.com/treytartt/honeydue-api/internal/models"
	"github.com/treytartt/honeydue-api/internal/push"
	"github.com/treytartt/honeydue-api/internal/repositories"
)

// Notification-related errors
var (
	// Deprecated: Use apperrors.NotFound("error.notification_not_found") instead
	ErrNotificationNotFound = errors.New("notification not found")
	// Deprecated: Use apperrors.NotFound("error.device_not_found") instead
	ErrDeviceNotFound       = errors.New("device not found")
	// Deprecated: Use apperrors.BadRequest("error.invalid_platform") instead
	ErrInvalidPlatform      = errors.New("invalid platform, must be 'ios' or 'android'")
)

// NotificationService handles notification business logic
type NotificationService struct {
	notificationRepo *repositories.NotificationRepository
	pushClient       *push.Client
}

// NewNotificationService creates a new notification service
func NewNotificationService(notificationRepo *repositories.NotificationRepository, pushClient *push.Client) *NotificationService {
	return &NotificationService{
		notificationRepo: notificationRepo,
		pushClient:       pushClient,
	}
}

// === Notifications ===

// GetNotifications gets notifications for a user
func (s *NotificationService) GetNotifications(userID uint, limit, offset int) ([]NotificationResponse, error) {
	notifications, err := s.notificationRepo.FindByUser(userID, limit, offset)
	if err != nil {
		return nil, apperrors.Internal(err)
	}

	result := make([]NotificationResponse, len(notifications))
	for i, n := range notifications {
		result[i] = NewNotificationResponse(&n)
	}
	return result, nil
}

// GetUnreadCount gets the count of unread notifications
func (s *NotificationService) GetUnreadCount(userID uint) (int64, error) {
	count, err := s.notificationRepo.CountUnread(userID)
	if err != nil {
		return 0, apperrors.Internal(err)
	}
	return count, nil
}

// MarkAsRead marks a notification as read
func (s *NotificationService) MarkAsRead(notificationID, userID uint) error {
	notification, err := s.notificationRepo.FindByID(notificationID)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return apperrors.NotFound("error.notification_not_found")
		}
		return apperrors.Internal(err)
	}

	if notification.UserID != userID {
		return apperrors.NotFound("error.notification_not_found")
	}

	if err := s.notificationRepo.MarkAsRead(notificationID); err != nil {
		return apperrors.Internal(err)
	}
	return nil
}

// MarkAllAsRead marks all notifications as read
func (s *NotificationService) MarkAllAsRead(userID uint) error {
	if err := s.notificationRepo.MarkAllAsRead(userID); err != nil {
		return apperrors.Internal(err)
	}
	return nil
}

// CreateAndSendNotification creates a notification and sends it via push
func (s *NotificationService) CreateAndSendNotification(ctx context.Context, userID uint, notificationType models.NotificationType, title, body string, data map[string]interface{}) error {
	// Check user preferences
	prefs, err := s.notificationRepo.GetOrCreatePreferences(userID)
	if err != nil {
		return apperrors.Internal(err)
	}

	// Check if notification type is enabled
	if !s.isNotificationEnabled(prefs, notificationType) {
		return nil // Skip silently
	}

	// Create notification record
	dataJSON, _ := json.Marshal(data)
	notification := &models.Notification{
		UserID:           userID,
		NotificationType: notificationType,
		Title:            title,
		Body:             body,
		Data:             string(dataJSON),
	}

	if err := s.notificationRepo.Create(notification); err != nil {
		return apperrors.Internal(err)
	}

	// Get device tokens
	iosTokens, androidTokens, err := s.notificationRepo.GetActiveTokensForUser(userID)
	if err != nil {
		return apperrors.Internal(err)
	}

	// Convert data for push
	pushData := make(map[string]string)
	for k, v := range data {
		switch val := v.(type) {
		case string:
			pushData[k] = val
		default:
			jsonVal, _ := json.Marshal(val)
			pushData[k] = string(jsonVal)
		}
	}
	pushData["notification_id"] = strconv.FormatUint(uint64(notification.ID), 10)

	// Send push notification
	if s.pushClient != nil {
		err = s.pushClient.SendToAll(ctx, iosTokens, androidTokens, title, body, pushData)
		if err != nil {
			s.notificationRepo.SetError(notification.ID, err.Error())
			return apperrors.Internal(err)
		}
	}

	if err := s.notificationRepo.MarkAsSent(notification.ID); err != nil {
		return apperrors.Internal(err)
	}
	return nil
}

// isNotificationEnabled checks if a notification type is enabled for user
func (s *NotificationService) isNotificationEnabled(prefs *models.NotificationPreference, notificationType models.NotificationType) bool {
	switch notificationType {
	case models.NotificationTaskDueSoon:
		return prefs.TaskDueSoon
	case models.NotificationTaskOverdue:
		return prefs.TaskOverdue
	case models.NotificationTaskCompleted:
		return prefs.TaskCompleted
	case models.NotificationTaskAssigned:
		return prefs.TaskAssigned
	case models.NotificationResidenceShared:
		return prefs.ResidenceShared
	case models.NotificationWarrantyExpiring:
		return prefs.WarrantyExpiring
	default:
		return true
	}
}

// === Notification Preferences ===

// GetPreferences gets notification preferences for a user
func (s *NotificationService) GetPreferences(userID uint) (*NotificationPreferencesResponse, error) {
	prefs, err := s.notificationRepo.GetOrCreatePreferences(userID)
	if err != nil {
		return nil, apperrors.Internal(err)
	}
	return NewNotificationPreferencesResponse(prefs), nil
}

// validateHourField checks that an optional hour value is in the valid range 0-23.
func validateHourField(val *int, fieldName string) error {
	if val != nil && (*val < 0 || *val > 23) {
		return apperrors.BadRequest("error.invalid_hour").
			WithMessage(fmt.Sprintf("%s must be between 0 and 23", fieldName))
	}
	return nil
}

// UpdatePreferences updates notification preferences
func (s *NotificationService) UpdatePreferences(userID uint, req *UpdatePreferencesRequest) (*NotificationPreferencesResponse, error) {
	// B-12: Validate hour fields are in range 0-23
	hourFields := []struct {
		value *int
		name  string
	}{
		{req.TaskDueSoonHour, "task_due_soon_hour"},
		{req.TaskOverdueHour, "task_overdue_hour"},
		{req.WarrantyExpiringHour, "warranty_expiring_hour"},
		{req.DailyDigestHour, "daily_digest_hour"},
	}
	for _, hf := range hourFields {
		if err := validateHourField(hf.value, hf.name); err != nil {
			return nil, err
		}
	}

	prefs, err := s.notificationRepo.GetOrCreatePreferences(userID)
	if err != nil {
		return nil, apperrors.Internal(err)
	}

	if req.TaskDueSoon != nil {
		prefs.TaskDueSoon = *req.TaskDueSoon
	}
	if req.TaskOverdue != nil {
		prefs.TaskOverdue = *req.TaskOverdue
	}
	if req.TaskCompleted != nil {
		prefs.TaskCompleted = *req.TaskCompleted
	}
	if req.TaskAssigned != nil {
		prefs.TaskAssigned = *req.TaskAssigned
	}
	if req.ResidenceShared != nil {
		prefs.ResidenceShared = *req.ResidenceShared
	}
	if req.WarrantyExpiring != nil {
		prefs.WarrantyExpiring = *req.WarrantyExpiring
	}
	if req.DailyDigest != nil {
		prefs.DailyDigest = *req.DailyDigest
	}
	if req.EmailTaskCompleted != nil {
		prefs.EmailTaskCompleted = *req.EmailTaskCompleted
	}

	// Update notification times (can be set to nil to use system default)
	// Note: We update if the field is present in the request (including null values)
	if req.TaskDueSoonHour != nil {
		prefs.TaskDueSoonHour = req.TaskDueSoonHour
	}
	if req.TaskOverdueHour != nil {
		prefs.TaskOverdueHour = req.TaskOverdueHour
	}
	if req.WarrantyExpiringHour != nil {
		prefs.WarrantyExpiringHour = req.WarrantyExpiringHour
	}
	if req.DailyDigestHour != nil {
		prefs.DailyDigestHour = req.DailyDigestHour
	}

	if err := s.notificationRepo.UpdatePreferences(prefs); err != nil {
		return nil, apperrors.Internal(err)
	}

	return NewNotificationPreferencesResponse(prefs), nil
}

// UpdateUserTimezone updates the user's timezone for background job calculations.
// This is called automatically when the user makes API calls (e.g., fetching tasks).
// The timezone should be an IANA timezone name (e.g., "America/Los_Angeles").
func (s *NotificationService) UpdateUserTimezone(userID uint, timezone string) {
	// Validate timezone is a valid IANA name
	if _, err := time.LoadLocation(timezone); err != nil {
		return // Invalid timezone, skip silently
	}

	// Get or create preferences and update timezone
	prefs, err := s.notificationRepo.GetOrCreatePreferences(userID)
	if err != nil {
		return // Skip silently on error
	}

	// Only update if timezone changed (avoid unnecessary DB writes)
	if prefs.Timezone == nil || *prefs.Timezone != timezone {
		prefs.Timezone = &timezone
		if err := s.notificationRepo.UpdatePreferences(prefs); err != nil {
			log.Error().Err(err).Uint("user_id", userID).Str("timezone", timezone).
				Msg("Failed to update user timezone in notification preferences")
		}
	}
}

// === Device Registration ===

// RegisterDevice registers a device for push notifications
func (s *NotificationService) RegisterDevice(userID uint, req *RegisterDeviceRequest) (*DeviceResponse, error) {
	switch req.Platform {
	case push.PlatformIOS:
		return s.registerAPNSDevice(userID, req)
	case push.PlatformAndroid:
		return s.registerGCMDevice(userID, req)
	default:
		return nil, apperrors.BadRequest("error.invalid_platform")
	}
}

func (s *NotificationService) registerAPNSDevice(userID uint, req *RegisterDeviceRequest) (*DeviceResponse, error) {
	// Check if device exists
	existing, err := s.notificationRepo.FindAPNSDeviceByToken(req.RegistrationID)
	if err == nil {
		// Update existing device
		existing.UserID = &userID
		existing.Active = true
		existing.Name = req.Name
		existing.DeviceID = req.DeviceID
		if err := s.notificationRepo.UpdateAPNSDevice(existing); err != nil {
			return nil, apperrors.Internal(err)
		}
		return NewAPNSDeviceResponse(existing), nil
	}

	// Create new device
	device := &models.APNSDevice{
		UserID:         &userID,
		Name:           req.Name,
		DeviceID:       req.DeviceID,
		RegistrationID: req.RegistrationID,
		Active:         true,
	}
	if err := s.notificationRepo.CreateAPNSDevice(device); err != nil {
		return nil, apperrors.Internal(err)
	}
	return NewAPNSDeviceResponse(device), nil
}

func (s *NotificationService) registerGCMDevice(userID uint, req *RegisterDeviceRequest) (*DeviceResponse, error) {
	// Check if device exists
	existing, err := s.notificationRepo.FindGCMDeviceByToken(req.RegistrationID)
	if err == nil {
		// Update existing device
		existing.UserID = &userID
		existing.Active = true
		existing.Name = req.Name
		existing.DeviceID = req.DeviceID
		if err := s.notificationRepo.UpdateGCMDevice(existing); err != nil {
			return nil, apperrors.Internal(err)
		}
		return NewGCMDeviceResponse(existing), nil
	}

	// Create new device
	device := &models.GCMDevice{
		UserID:           &userID,
		Name:             req.Name,
		DeviceID:         req.DeviceID,
		RegistrationID:   req.RegistrationID,
		CloudMessageType: "FCM",
		Active:           true,
	}
	if err := s.notificationRepo.CreateGCMDevice(device); err != nil {
		return nil, apperrors.Internal(err)
	}
	return NewGCMDeviceResponse(device), nil
}

// ListDevices lists all devices for a user
func (s *NotificationService) ListDevices(userID uint) ([]DeviceResponse, error) {
	iosDevices, err := s.notificationRepo.FindAPNSDevicesByUser(userID)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, apperrors.Internal(err)
	}

	androidDevices, err := s.notificationRepo.FindGCMDevicesByUser(userID)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, apperrors.Internal(err)
	}

	result := make([]DeviceResponse, 0, len(iosDevices)+len(androidDevices))
	for _, d := range iosDevices {
		result = append(result, *NewAPNSDeviceResponse(&d))
	}
	for _, d := range androidDevices {
		result = append(result, *NewGCMDeviceResponse(&d))
	}
	return result, nil
}

// DeleteDevice deactivates a device after verifying it belongs to the requesting user.
// Without ownership verification, an attacker could deactivate push notifications for other users.
func (s *NotificationService) DeleteDevice(deviceID uint, platform string, userID uint) error {
	switch platform {
	case push.PlatformIOS:
		device, err := s.notificationRepo.FindAPNSDeviceByID(deviceID)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return apperrors.NotFound("error.device_not_found")
			}
			return apperrors.Internal(err)
		}
		// Verify the device belongs to the requesting user
		if device.UserID == nil || *device.UserID != userID {
			return apperrors.Forbidden("error.device_access_denied")
		}
		if err := s.notificationRepo.DeactivateAPNSDevice(deviceID); err != nil {
			return apperrors.Internal(err)
		}
	case push.PlatformAndroid:
		device, err := s.notificationRepo.FindGCMDeviceByID(deviceID)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return apperrors.NotFound("error.device_not_found")
			}
			return apperrors.Internal(err)
		}
		// Verify the device belongs to the requesting user
		if device.UserID == nil || *device.UserID != userID {
			return apperrors.Forbidden("error.device_access_denied")
		}
		if err := s.notificationRepo.DeactivateGCMDevice(deviceID); err != nil {
			return apperrors.Internal(err)
		}
	default:
		return apperrors.BadRequest("error.invalid_platform")
	}
	return nil
}

// UnregisterDevice deactivates a device by its registration token
func (s *NotificationService) UnregisterDevice(registrationID, platform string, userID uint) error {
	switch platform {
	case push.PlatformIOS:
		device, err := s.notificationRepo.FindAPNSDeviceByToken(registrationID)
		if err != nil {
			return apperrors.NotFound("error.device_not_found")
		}
		// Verify ownership
		if device.UserID == nil || *device.UserID != userID {
			return apperrors.NotFound("error.device_not_found")
		}
		if err := s.notificationRepo.DeactivateAPNSDevice(device.ID); err != nil {
			return apperrors.Internal(err)
		}
	case push.PlatformAndroid:
		device, err := s.notificationRepo.FindGCMDeviceByToken(registrationID)
		if err != nil {
			return apperrors.NotFound("error.device_not_found")
		}
		// Verify ownership
		if device.UserID == nil || *device.UserID != userID {
			return apperrors.NotFound("error.device_not_found")
		}
		if err := s.notificationRepo.DeactivateGCMDevice(device.ID); err != nil {
			return apperrors.Internal(err)
		}
	default:
		return apperrors.BadRequest("error.invalid_platform")
	}
	return nil
}

// === Response/Request Types ===

// TODO(hardening): Move to internal/dto/responses/notification.go
// NotificationResponse represents a notification in API response
type NotificationResponse struct {
	ID               uint                     `json:"id"`
	UserID           uint                     `json:"user_id"`
	NotificationType models.NotificationType  `json:"notification_type"`
	Title            string                   `json:"title"`
	Body             string                   `json:"body"`
	Data             map[string]interface{}   `json:"data"`
	Read             bool                     `json:"read"`
	ReadAt           *string                  `json:"read_at"`
	Sent             bool                     `json:"sent"`
	SentAt           *string                  `json:"sent_at"`
	CreatedAt        string                   `json:"created_at"`
}

// NewNotificationResponse creates a NotificationResponse
func NewNotificationResponse(n *models.Notification) NotificationResponse {
	resp := NotificationResponse{
		ID:               n.ID,
		UserID:           n.UserID,
		NotificationType: n.NotificationType,
		Title:            n.Title,
		Body:             n.Body,
		Read:             n.Read,
		Sent:             n.Sent,
		CreatedAt:        n.CreatedAt.Format("2006-01-02T15:04:05Z"),
	}

	if n.Data != "" {
		json.Unmarshal([]byte(n.Data), &resp.Data)
	}
	if n.ReadAt != nil {
		t := n.ReadAt.Format("2006-01-02T15:04:05Z")
		resp.ReadAt = &t
	}
	if n.SentAt != nil {
		t := n.SentAt.Format("2006-01-02T15:04:05Z")
		resp.SentAt = &t
	}

	return resp
}

// TODO(hardening): Move to internal/dto/responses/notification.go
// NotificationPreferencesResponse represents notification preferences
type NotificationPreferencesResponse struct {
	TaskDueSoon      bool `json:"task_due_soon"`
	TaskOverdue      bool `json:"task_overdue"`
	TaskCompleted    bool `json:"task_completed"`
	TaskAssigned     bool `json:"task_assigned"`
	ResidenceShared  bool `json:"residence_shared"`
	WarrantyExpiring bool `json:"warranty_expiring"`
	DailyDigest      bool `json:"daily_digest"`

	// Email preferences
	EmailTaskCompleted bool `json:"email_task_completed"`

	// Custom notification times (UTC hour 0-23, null means use system default)
	TaskDueSoonHour      *int `json:"task_due_soon_hour"`
	TaskOverdueHour      *int `json:"task_overdue_hour"`
	WarrantyExpiringHour *int `json:"warranty_expiring_hour"`
	DailyDigestHour      *int `json:"daily_digest_hour"`
}

// NewNotificationPreferencesResponse creates a NotificationPreferencesResponse
func NewNotificationPreferencesResponse(p *models.NotificationPreference) *NotificationPreferencesResponse {
	return &NotificationPreferencesResponse{
		TaskDueSoon:          p.TaskDueSoon,
		TaskOverdue:          p.TaskOverdue,
		TaskCompleted:        p.TaskCompleted,
		TaskAssigned:         p.TaskAssigned,
		ResidenceShared:      p.ResidenceShared,
		WarrantyExpiring:     p.WarrantyExpiring,
		DailyDigest:          p.DailyDigest,
		EmailTaskCompleted:   p.EmailTaskCompleted,
		TaskDueSoonHour:      p.TaskDueSoonHour,
		TaskOverdueHour:      p.TaskOverdueHour,
		WarrantyExpiringHour: p.WarrantyExpiringHour,
		DailyDigestHour:      p.DailyDigestHour,
	}
}

// TODO(hardening): Move to internal/dto/requests/notification.go
// UpdatePreferencesRequest represents preferences update request
type UpdatePreferencesRequest struct {
	TaskDueSoon      *bool `json:"task_due_soon"`
	TaskOverdue      *bool `json:"task_overdue"`
	TaskCompleted    *bool `json:"task_completed"`
	TaskAssigned     *bool `json:"task_assigned"`
	ResidenceShared  *bool `json:"residence_shared"`
	WarrantyExpiring *bool `json:"warranty_expiring"`
	DailyDigest      *bool `json:"daily_digest"`

	// Email preferences
	EmailTaskCompleted *bool `json:"email_task_completed"`

	// Custom notification times (UTC hour 0-23)
	// Use a special wrapper to differentiate between "not sent" and "set to null"
	TaskDueSoonHour      *int `json:"task_due_soon_hour"`
	TaskOverdueHour      *int `json:"task_overdue_hour"`
	WarrantyExpiringHour *int `json:"warranty_expiring_hour"`
	DailyDigestHour      *int `json:"daily_digest_hour"`
}

// TODO(hardening): Move to internal/dto/responses/notification.go
// DeviceResponse represents a device in API response
type DeviceResponse struct {
	ID             uint   `json:"id"`
	Name           string `json:"name"`
	DeviceID       string `json:"device_id"`
	RegistrationID string `json:"registration_id"`
	Platform       string `json:"platform"`
	Active         bool   `json:"active"`
	DateCreated    string `json:"date_created"`
}

// NewAPNSDeviceResponse creates a DeviceResponse from APNS device
func NewAPNSDeviceResponse(d *models.APNSDevice) *DeviceResponse {
	return &DeviceResponse{
		ID:             d.ID,
		Name:           d.Name,
		DeviceID:       d.DeviceID,
		RegistrationID: d.RegistrationID,
		Platform:       push.PlatformIOS,
		Active:         d.Active,
		DateCreated:    d.DateCreated.Format("2006-01-02T15:04:05Z"),
	}
}

// NewGCMDeviceResponse creates a DeviceResponse from GCM device
func NewGCMDeviceResponse(d *models.GCMDevice) *DeviceResponse {
	return &DeviceResponse{
		ID:             d.ID,
		Name:           d.Name,
		DeviceID:       d.DeviceID,
		RegistrationID: d.RegistrationID,
		Platform:       push.PlatformAndroid,
		Active:         d.Active,
		DateCreated:    d.DateCreated.Format("2006-01-02T15:04:05Z"),
	}
}

// TODO(hardening): Move to internal/dto/requests/notification.go
// RegisterDeviceRequest represents device registration request
type RegisterDeviceRequest struct {
	Name           string `json:"name"`
	DeviceID       string `json:"device_id" validate:"required"`
	RegistrationID string `json:"registration_id" validate:"required"`
	Platform       string `json:"platform" validate:"required,oneof=ios android"`
}

// === Task Notifications with Actions ===

// CreateAndSendTaskNotification creates and sends a task notification with actionable buttons
// The backend always sends full notification data - the client decides how to display
// based on its locally cached subscription status
func (s *NotificationService) CreateAndSendTaskNotification(
	ctx context.Context,
	userID uint,
	notificationType models.NotificationType,
	task *models.Task,
) error {
	// Check user notification preferences
	prefs, err := s.notificationRepo.GetOrCreatePreferences(userID)
	if err != nil {
		return apperrors.Internal(err)
	}
	if !s.isNotificationEnabled(prefs, notificationType) {
		return nil // Skip silently
	}

	// Build notification content - always send full data
	title := GetTaskNotificationTitle(notificationType)
	body := task.Title

	// Get button types and iOS category based on task state
	buttonTypes := GetButtonTypesForTask(task, 30) // 30 days threshold
	iosCategoryID := GetIOSCategoryForTask(task)

	// Build data payload - always includes full task info
	// Client decides what to display based on local subscription status
	data := map[string]interface{}{
		"task_id":      task.ID,
		"task_name":    task.Title,
		"residence_id": task.ResidenceID,
		"type":         string(notificationType),
		"button_types": buttonTypes,
		"ios_category": iosCategoryID,
	}

	// Create notification record
	dataJSON, _ := json.Marshal(data)
	notification := &models.Notification{
		UserID:           userID,
		NotificationType: notificationType,
		Title:            title,
		Body:             body,
		Data:             string(dataJSON),
		TaskID:           &task.ID,
	}

	if err := s.notificationRepo.Create(notification); err != nil {
		return apperrors.Internal(err)
	}

	// Get device tokens
	iosTokens, androidTokens, err := s.notificationRepo.GetActiveTokensForUser(userID)
	if err != nil {
		return apperrors.Internal(err)
	}

	// Convert data for push payload
	pushData := make(map[string]string)
	for k, v := range data {
		switch val := v.(type) {
		case string:
			pushData[k] = val
		case uint:
			pushData[k] = strconv.FormatUint(uint64(val), 10)
		default:
			jsonVal, _ := json.Marshal(val)
			pushData[k] = string(jsonVal)
		}
	}
	pushData["notification_id"] = strconv.FormatUint(uint64(notification.ID), 10)

	// Send push notification with actionable support
	if s.pushClient != nil {
		err = s.pushClient.SendActionableNotification(ctx, iosTokens, androidTokens, title, body, pushData, iosCategoryID)
		if err != nil {
			s.notificationRepo.SetError(notification.ID, err.Error())
			return apperrors.Internal(err)
		}
	}

	if err := s.notificationRepo.MarkAsSent(notification.ID); err != nil {
		return apperrors.Internal(err)
	}
	return nil
}
