package storage

import (
	"database/sql"
	"fmt"
	"strings"
)

// SetRead marks (read=true) or clears (read=false) the read flag for one story
// and one signed-in user (keyed by OIDC subject). Read and bookmark state share
// a row; clearing the last remaining flag removes the row.
func SetRead(sub string, storyID int64, read bool) error {
	var ts any
	if read {
		ts = nowUnix()
	}
	_, err := Get().Exec(`
		INSERT INTO user_story_state (user_sub, story_id, read_at, bookmarked_at)
		VALUES (?, ?, ?, NULL)
		ON CONFLICT(user_sub, story_id) DO UPDATE SET read_at = excluded.read_at`,
		sub, storyID, ts)
	if err != nil {
		return fmt.Errorf("set read: %w", err)
	}
	return pruneEmptyState(sub, storyID)
}

// SetBookmark adds (on=true) or removes (on=false) a bookmark for one story and
// one signed-in user. See SetRead for the shared-row semantics.
func SetBookmark(sub string, storyID int64, on bool) error {
	var ts any
	if on {
		ts = nowUnix()
	}
	_, err := Get().Exec(`
		INSERT INTO user_story_state (user_sub, story_id, read_at, bookmarked_at)
		VALUES (?, ?, NULL, ?)
		ON CONFLICT(user_sub, story_id) DO UPDATE SET bookmarked_at = excluded.bookmarked_at`,
		sub, storyID, ts)
	if err != nil {
		return fmt.Errorf("set bookmark: %w", err)
	}
	return pruneEmptyState(sub, storyID)
}

// pruneEmptyState drops a row once neither flag is set, keeping the table to
// only meaningful state.
func pruneEmptyState(sub string, storyID int64) error {
	_, err := Get().Exec(
		`DELETE FROM user_story_state
		 WHERE user_sub = ? AND story_id = ? AND read_at IS NULL AND bookmarked_at IS NULL`,
		sub, storyID)
	if err != nil {
		return fmt.Errorf("prune user state: %w", err)
	}
	return nil
}

// UserStoryState reports, for the given story ids, which are read and which are
// bookmarked by this user. Both maps contain only ids whose flag is set, so a
// missing key means false. An empty sub or id list returns empty maps.
func UserStoryState(sub string, ids []int64) (read, bookmarked map[int64]bool, err error) {
	read = make(map[int64]bool)
	bookmarked = make(map[int64]bool)
	if sub == "" || len(ids) == 0 {
		return read, bookmarked, nil
	}
	q := `SELECT story_id, read_at, bookmarked_at FROM user_story_state
	      WHERE user_sub = ? AND story_id IN (` + placeholders(len(ids)) + `)`
	args := make([]any, 0, len(ids)+1)
	args = append(args, sub)
	for _, id := range ids {
		args = append(args, id)
	}
	rows, err := Get().Query(q, args...)
	if err != nil {
		return nil, nil, fmt.Errorf("user story state: %w", err)
	}
	defer rows.Close()
	for rows.Next() {
		var id int64
		var r, b sql.NullInt64
		if err := rows.Scan(&id, &r, &b); err != nil {
			return nil, nil, err
		}
		if r.Valid {
			read[id] = true
		}
		if b.Valid {
			bookmarked[id] = true
		}
	}
	return read, bookmarked, rows.Err()
}

// ListBookmarks returns the user's bookmarked stories, most recently bookmarked
// first, restricted to still-classified stories.
func ListBookmarks(sub string, limit, offset int) ([]Story, error) {
	rows, err := Get().Query(
		`SELECT s.id, s.guid, s.headline, s.lede, s.image_url, s.article_url, s.source, s.platforms, s.channel, s.paywalled, s.seen_at,
		        EXISTS(SELECT 1 FROM post_log p WHERE p.guid = s.guid) AS posted
		 FROM user_story_state u
		 JOIN stories s ON s.id = u.story_id
		 WHERE u.user_sub = ?
		   AND u.bookmarked_at IS NOT NULL
		   AND s.classified = 1
		   AND s.channel NOT IN ('_discarded', '_duplicate')
		 ORDER BY u.bookmarked_at DESC, u.story_id DESC
		 LIMIT ? OFFSET ?`, sub, limit, offset)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []Story
	for rows.Next() {
		var s Story
		if err := rows.Scan(&s.ID, &s.GUID, &s.Headline, &s.Lede, &s.ImageURL, &s.ArticleURL, &s.Source, &s.Platforms, &s.Channel, &s.Paywalled, &s.SeenAt, &s.Posted); err != nil {
			return nil, err
		}
		out = append(out, s)
	}
	return out, rows.Err()
}

// CountBookmarks returns how many bookmarked stories the user would see on the
// bookmarks page. It applies the same classified filter as ListBookmarks so the
// "N saved" header can't exceed the number of rendered cards.
func CountBookmarks(sub string) (int, error) {
	var n int
	err := Get().QueryRow(
		`SELECT COUNT(*) FROM user_story_state u
		 JOIN stories s ON s.id = u.story_id
		 WHERE u.user_sub = ? AND u.bookmarked_at IS NOT NULL AND s.classified = 1
		   AND s.channel NOT IN ('_discarded', '_duplicate')`,
		sub).Scan(&n)
	return n, err
}

// placeholders returns "?, ?, …" with n slots for an IN clause.
func placeholders(n int) string {
	if n <= 0 {
		return ""
	}
	return strings.Repeat("?, ", n-1) + "?"
}
