package store

import (
	"context"
	"fmt"

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

// allowedActions is the closed set of action verbs persisted in summary_actions.
// It mirrors the CHECK constraint in migration 002; validating in Go returns a
// clear error before the round-trip rather than a raw constraint violation.
var allowedActions = map[string]bool{
	"watched": true,
	"skipped": true,
	"saved":   true,
}

// oppositeAction maps each mutually-exclusive action to the one it clears:
// watching a video clears a prior skip and vice versa (ui-spec.md §4). "saved"
// is independent and has no opposite, so it is absent here.
var oppositeAction = map[string]string{
	"watched": "skipped",
	"skipped": "watched",
}

// validateAction rejects any action outside the allowed set.
func validateAction(action string) error {
	if !allowedActions[action] {
		return fmt.Errorf("store: invalid action %q (want watched|skipped|saved)", action)
	}
	return nil
}

// SetAction records that the user took action on a video. It is idempotent:
// re-setting an already-active action refreshes acted_at, never duplicates.
// Mutual exclusion is enforced atomically in one tx — setting "watched" clears
// "skipped" and vice versa; "saved" coexists with either.
func (s *Store) SetAction(ctx context.Context, userID, videoID, action string) error {
	if err := validateAction(action); err != nil {
		return err
	}

	return s.withUser(ctx, userID, func(tx pgx.Tx) error {
		if opposite, ok := oppositeAction[action]; ok {
			if _, err := tx.Exec(ctx,
				`DELETE FROM summary_actions
				  WHERE user_id = $1 AND video_id = $2 AND action = $3`,
				userID, videoID, opposite); err != nil {
				return fmt.Errorf("store: clear opposite action: %w", err)
			}
		}

		if _, err := tx.Exec(ctx,
			`INSERT INTO summary_actions (user_id, video_id, action)
			 VALUES ($1, $2, $3)
			 ON CONFLICT (user_id, video_id, action) DO UPDATE SET acted_at = NOW()`,
			userID, videoID, action); err != nil {
			return fmt.Errorf("store: set action: %w", err)
		}
		return nil
	})
}

// ClearAction removes an action for (user, video). Clearing an action that is
// not set is a no-op (no error) — toggling off an inactive button is harmless.
func (s *Store) ClearAction(ctx context.Context, userID, videoID, action string) error {
	if err := validateAction(action); err != nil {
		return err
	}
	return s.withUser(ctx, userID, func(tx pgx.Tx) error {
		if _, err := tx.Exec(ctx,
			`DELETE FROM summary_actions
			  WHERE user_id = $1 AND video_id = $2 AND action = $3`,
			userID, videoID, action); err != nil {
			return fmt.Errorf("store: clear action: %w", err)
		}
		return nil
	})
}

// ActionsFor returns the active actions per video for the given user, keyed by
// video_id, for the list view. Videos with no actions are simply absent from the
// map. Scoped by user_id: one user never sees another's actions.
func (s *Store) ActionsFor(ctx context.Context, userID string, videoIDs []string) (map[string][]string, error) {
	out := make(map[string][]string)
	if len(videoIDs) == 0 {
		return out, nil
	}
	if err := s.withUser(ctx, userID, func(tx pgx.Tx) error {
		rows, err := tx.Query(ctx,
			`SELECT video_id, action FROM summary_actions
			  WHERE user_id = $1 AND video_id = ANY($2)
			  ORDER BY video_id, action`,
			userID, videoIDs)
		if err != nil {
			return fmt.Errorf("store: actions for: %w", err)
		}
		defer rows.Close()

		for rows.Next() {
			var videoID, action string
			if err := rows.Scan(&videoID, &action); err != nil {
				return fmt.Errorf("store: scan action: %w", err)
			}
			out[videoID] = append(out[videoID], action)
		}
		if err := rows.Err(); err != nil {
			return fmt.Errorf("store: iterate actions: %w", err)
		}
		return nil
	}); err != nil {
		return nil, err
	}
	return out, nil
}

// attachActions enriches read rows with their current actions in a single query.
// Additive: rows with no actions keep a nil Actions slice. Used by the read
// views (reads.go) to surface action state without a cross-type SQL join
// (summaries.video_id is UUID, summary_actions.video_id is TEXT).
func (s *Store) attachActions(ctx context.Context, userID string, rows []SummaryRow) error {
	if len(rows) == 0 {
		return nil
	}
	ids := make([]string, len(rows))
	for i := range rows {
		ids[i] = rows[i].VideoID
	}
	actions, err := s.ActionsFor(ctx, userID, ids)
	if err != nil {
		return err
	}
	for i := range rows {
		rows[i].Actions = actions[rows[i].VideoID]
	}
	return nil
}
