package devices

import (
	"context"
	"errors"
	"time"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/ultisuite/ulti-backend/internal/users"
)

// ErrUserNotFound is returned when the authenticated user has no provisioned row.
var ErrUserNotFound = errors.New("user not found")

// ErrDeviceNotFound is returned when a delete affects no rows.
var ErrDeviceNotFound = errors.New("device token not found")

// Device is a registered mobile device push token.
type Device struct {
	ID        string    `json:"id"`
	Platform  string    `json:"platform"`
	App       string    `json:"app"`
	DeviceID  string    `json:"device_id,omitempty"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

// Service persists device tokens keyed by the internal user id.
type Service struct {
	db *pgxpool.Pool
}

func NewService(db *pgxpool.Pool) *Service {
	return &Service{db: db}
}

// Register upserts a device token for the given external user id (OIDC sub),
// returning the row id. Re-registering an identical (user, app, push_token)
// refreshes platform/device_id/updated_at.
func (s *Service) Register(ctx context.Context, externalID, platform, app, pushToken, deviceID string) (string, error) {
	userID, err := s.resolveUser(ctx, externalID)
	if err != nil {
		return "", err
	}

	var devicePtr *string
	if deviceID != "" {
		devicePtr = &deviceID
	}

	var id string
	err = s.db.QueryRow(ctx, `
		INSERT INTO device_tokens (user_id, platform, app, push_token, device_id)
		VALUES ($1, $2, $3, $4, $5)
		ON CONFLICT (user_id, app, push_token)
		DO UPDATE SET platform = EXCLUDED.platform,
		              device_id = EXCLUDED.device_id,
		              updated_at = now()
		RETURNING id::text
	`, userID, platform, app, pushToken, devicePtr).Scan(&id)
	if err != nil {
		return "", err
	}
	return id, nil
}

// UnregisterByID removes a device token owned by the user.
func (s *Service) UnregisterByID(ctx context.Context, externalID, id string) error {
	userID, err := s.resolveUser(ctx, externalID)
	if err != nil {
		return err
	}
	tag, err := s.db.Exec(ctx, `
		DELETE FROM device_tokens WHERE id = $1 AND user_id = $2
	`, id, userID)
	if err != nil {
		return err
	}
	if tag.RowsAffected() == 0 {
		return ErrDeviceNotFound
	}
	return nil
}

// UnregisterByToken removes a device token by its push token for the user.
func (s *Service) UnregisterByToken(ctx context.Context, externalID, pushToken string) error {
	userID, err := s.resolveUser(ctx, externalID)
	if err != nil {
		return err
	}
	tag, err := s.db.Exec(ctx, `
		DELETE FROM device_tokens WHERE push_token = $1 AND user_id = $2
	`, pushToken, userID)
	if err != nil {
		return err
	}
	if tag.RowsAffected() == 0 {
		return ErrDeviceNotFound
	}
	return nil
}

// List returns the user's registered devices, most recently updated first.
func (s *Service) List(ctx context.Context, externalID string) ([]Device, error) {
	userID, err := s.resolveUser(ctx, externalID)
	if err != nil {
		return nil, err
	}
	rows, err := s.db.Query(ctx, `
		SELECT id::text, platform, app, coalesce(device_id, ''), created_at, updated_at
		FROM device_tokens
		WHERE user_id = $1
		ORDER BY updated_at DESC
	`, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	out := make([]Device, 0)
	for rows.Next() {
		var d Device
		if err := rows.Scan(&d.ID, &d.Platform, &d.App, &d.DeviceID, &d.CreatedAt, &d.UpdatedAt); err != nil {
			return nil, err
		}
		out = append(out, d)
	}
	return out, rows.Err()
}

func (s *Service) resolveUser(ctx context.Context, externalID string) (string, error) {
	userID, err := users.LookupUserID(ctx, s.db, externalID)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return "", ErrUserNotFound
		}
		return "", err
	}
	if userID == "" {
		return "", ErrUserNotFound
	}
	return userID, nil
}
