package store

import (
	"context"
	"fmt"
	"strings"
)

// StateGroup — группа состояний для фильтра списка загрузок. Единый источник
// истины «группа → состояния» для транспорта (чипы) и запроса (WHERE).
type StateGroup string

const (
	GroupAll     StateGroup = "all"     // все, кроме скрытых терминальных (deleted/cancelled), если не IncludeDeleted
	GroupReview  StateGroup = "review"  // ждут человека
	GroupActive  StateGroup = "active"  // в работе
	GroupDone    StateGroup = "done"    // готово
	GroupProblem StateGroup = "problem" // проблемы
)

// statesInGroup возвращает состояния группы фильтра. nil — группа не сужает
// выборку по состоянию (all/неизвестная): состояния перечисляются здесь и
// нигде больше, чтобы UI и запрос не разъезжались. Терминальные cancelled/
// reverted/deleted своей группы (чипа) не имеют. Под all скрытые терминальные
// (deleted и cancelled — шум в ленте) видны только при IncludeDeleted; reverted
// остаётся видимым по умолчанию. Точечно к ним не фильтруем (см. tasks 2.1).
func statesInGroup(g StateGroup) []State {
	switch g {
	case GroupReview:
		return []State{StateReview, StateDeferred}
	case GroupActive:
		return []State{StateCatched, StateDownloading, StateCompleted, StateRecognizing, StateLinking}
	case GroupDone:
		return []State{StateDone}
	case GroupProblem:
		return []State{StateStuck, StateFailed, StateTargetMissing, StateOrphaned}
	default:
		return nil
	}
}

// ListFilter — параметры страницы списка загрузок.
type ListFilter struct {
	Group          StateGroup // группа состояний ("" / all — без сужения)
	Query          string     // поиск по id/названию/infohash/контексту (подстрока)
	IncludeDeleted bool       // показывать скрытые терминальные deleted+cancelled (только под all)
	Limit          int        // размер страницы
	Offset         int        // смещение страницы
}

// listWhere строит условие WHERE и его аргументы по фильтру.
func listWhere(f ListFilter) (string, []any) {
	var conds []string
	var args []any

	if states := statesInGroup(f.Group); states != nil {
		ph := make([]string, len(states))
		for i, st := range states {
			ph[i] = "?"
			args = append(args, string(st))
		}
		conds = append(conds, "state IN ("+strings.Join(ph, ",")+")")
	} else if !f.IncludeDeleted {
		// Группа all: скрываем терминальные deleted и cancelled по умолчанию (шум
		// в ленте; cancelled плодит универсальный стоп-кран «Закрыть»). reverted
		// оставляем видимым. Оба показываются тумблером IncludeDeleted.
		conds = append(conds, "state NOT IN (?, ?)")
		args = append(args, string(StateDeleted), string(StateCancelled))
	}

	if q := strings.TrimSpace(f.Query); q != "" {
		// LIKE даёт регистронезависимость только для ASCII (встроенное
		// case-folding SQLite), для кириллицы поиск чувствителен к регистру —
		// ICU в чистом modernc-драйвере нет. Достаточно для инфохэша/латиницы;
		// кириллические названия ищем как введены.
		like := "%" + q + "%"
		conds = append(conds, "(download.id LIKE ? COLLATE NOCASE "+
			"OR source_ref LIKE ? COLLATE NOCASE "+
			"OR display_name LIKE ? COLLATE NOCASE "+
			"OR context LIKE ? COLLATE NOCASE "+
			"OR EXISTS (SELECT 1 FROM download_infohash dh "+
			"WHERE dh.download_id = download.id AND dh.infohash LIKE ? COLLATE NOCASE))")
		args = append(args, like, like, like, like, like)
	}

	if len(conds) == 0 {
		return "", args
	}
	return " WHERE " + strings.Join(conds, " AND "), args
}

// listSelect и listOrder — общая форма выборки списка загрузок: LEFT JOIN
// текущей попытки распознавания (фолбэк заголовка и значок типа) и порядок по
// времени добавления с tie-break по id. Держим одним куском, чтобы страница
// списка и страница группового удаления не разъехались порядком.
const listSelect = `SELECT download.*, r.title AS rec_title, r.media_type AS rec_media_type
FROM download
LEFT JOIN recognition r ON r.download_id = download.id AND r.is_current = 1`

const listOrder = `
ORDER BY COALESCE(download.source_added_at, download.created_at) DESC, download.id DESC`

// ListDeletableDownloads возвращает все загрузки, для которых домен допускает
// полное удаление, без постраничной выдачи: страница группового удаления
// показывает их разом, а разбиение по страницам лишило бы возможности выбрать
// пачку. Перечень состояний берётся из единой точки домена — своего списка
// запрос не держит.
func (s *Store) ListDeletableDownloads(ctx context.Context) ([]Download, error) {
	ph := make([]string, len(deletableStates))
	args := make([]any, len(deletableStates))
	for i, st := range deletableStates {
		ph[i] = "?"
		args[i] = string(st)
	}
	q := listSelect + " WHERE download.state IN (" + strings.Join(ph, ",") + ")" + listOrder

	var out []Download
	if err := s.DB.SelectContext(ctx, &out, q, args...); err != nil {
		return nil, fmt.Errorf("list deletable downloads: %w", err)
	}
	if err := s.attachInfohashes(ctx, out); err != nil {
		return nil, fmt.Errorf("list deletable downloads: %w", err)
	}
	return out, nil
}

// ListDownloadsPage возвращает страницу загрузок под фильтром и общее число
// строк, удовлетворяющих фильтру (для пагинации). Сортировка — по времени
// добавления в источник (source_added_at) с фолбеком на created_at и tie-break
// по id: без вторичного ключа при равных метках порядок неустойчив, и строки
// задваивались бы/пропадали между страницами. RecTitle и RecMediaType
// подтягиваются LEFT JOIN текущей попытки распознавания — для фолбека заголовка
// и значка типа карточки.
func (s *Store) ListDownloadsPage(ctx context.Context, f ListFilter) ([]Download, int, error) {
	where, args := listWhere(f)

	var total int
	if err := s.DB.GetContext(ctx, &total, `SELECT COUNT(*) FROM download`+where, args...); err != nil {
		return nil, 0, fmt.Errorf("list downloads page count: %w", err)
	}

	q := listSelect + where + listOrder + `
LIMIT ? OFFSET ?`
	pageArgs := append(append([]any{}, args...), f.Limit, f.Offset)

	var out []Download
	if err := s.DB.SelectContext(ctx, &out, q, pageArgs...); err != nil {
		return nil, 0, fmt.Errorf("list downloads page: %w", err)
	}
	if err := s.attachInfohashes(ctx, out); err != nil {
		return nil, 0, fmt.Errorf("list downloads page: %w", err)
	}
	return out, total, nil
}
