package services

// PinService manages paliad.user_pinned_projects — per-user project pins
// for the /projects page (chip "Angepinnt" + star marker).
//
// Design: docs/design-projects-page-2026-05-07.md §4.7.
//
// Visibility: pin/unpin gate on can_see_project — a user can't pin what
// they can't see. Reads of the pinned-set are RLS-scoped (user_id = auth.uid())
// AND code-checked for defense-in-depth.

import (
	"context"
	"fmt"

	"github.com/google/uuid"
	"github.com/jmoiron/sqlx"
)

// PinService manages paliad.user_pinned_projects.
type PinService struct {
	db       *sqlx.DB
	projects *ProjectService
}

// NewPinService wires the service.
func NewPinService(db *sqlx.DB, projects *ProjectService) *PinService {
	return &PinService{db: db, projects: projects}
}

// Pin records (userID, projectID) as a pin. Idempotent — re-pinning is a
// no-op (existing pinned_at preserved). Returns ErrNotVisible if the user
// cannot see the project.
func (s *PinService) Pin(ctx context.Context, userID, projectID uuid.UUID) error {
	visible, err := s.projects.CanSee(ctx, userID, projectID)
	if err != nil {
		return err
	}
	if !visible {
		return ErrNotVisible
	}
	_, err = s.db.ExecContext(ctx, `
		INSERT INTO paliad.user_pinned_projects (user_id, project_id)
		VALUES ($1, $2)
		ON CONFLICT (user_id, project_id) DO NOTHING
	`, userID, projectID)
	if err != nil {
		return fmt.Errorf("pin project: %w", err)
	}
	return nil
}

// Unpin removes the pin. Idempotent — unpinning a non-pinned project is a
// no-op. Does NOT gate on visibility (unpinning a project you've lost
// access to should still work — otherwise the pin row leaks forever).
func (s *PinService) Unpin(ctx context.Context, userID, projectID uuid.UUID) error {
	_, err := s.db.ExecContext(ctx, `
		DELETE FROM paliad.user_pinned_projects
		 WHERE user_id = $1 AND project_id = $2
	`, userID, projectID)
	if err != nil {
		return fmt.Errorf("unpin project: %w", err)
	}
	return nil
}

// IsPinned reports whether the user has pinned the project.
func (s *PinService) IsPinned(ctx context.Context, userID, projectID uuid.UUID) (bool, error) {
	var pinned bool
	err := s.db.GetContext(ctx, &pinned, `
		SELECT EXISTS (SELECT 1 FROM paliad.user_pinned_projects
		                WHERE user_id = $1 AND project_id = $2)
	`, userID, projectID)
	if err != nil {
		return false, fmt.Errorf("check pin: %w", err)
	}
	return pinned, nil
}

// PinnedSet returns the set of project_ids the user has pinned. Used by
// BuildTree to populate per-node `pinned: bool`.
func (s *PinService) PinnedSet(ctx context.Context, userID uuid.UUID) (map[uuid.UUID]struct{}, error) {
	var ids []uuid.UUID
	err := s.db.SelectContext(ctx, &ids, `
		SELECT project_id FROM paliad.user_pinned_projects
		 WHERE user_id = $1
		 ORDER BY pinned_at DESC
	`, userID)
	if err != nil {
		return nil, fmt.Errorf("list pinned: %w", err)
	}
	set := make(map[uuid.UUID]struct{}, len(ids))
	for _, id := range ids {
		set[id] = struct{}{}
	}
	return set, nil
}

// ListPinned returns project_ids in pinned-at-DESC order. Used by the
// (deferred) sidebar pin widget; not used by the tree response (which
// prefers the set form via PinnedSet for O(1) lookups during stitching).
func (s *PinService) ListPinned(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error) {
	var ids []uuid.UUID
	err := s.db.SelectContext(ctx, &ids, `
		SELECT project_id FROM paliad.user_pinned_projects
		 WHERE user_id = $1
		 ORDER BY pinned_at DESC
	`, userID)
	if err != nil {
		return nil, fmt.Errorf("list pinned ordered: %w", err)
	}
	return ids, nil
}

