# Sunocore Search API - User Guide

This guide explains how to use the improved search API in sunocore, which provides both low-level control and high-level convenience methods.

## Quick Start

### Simple Search

The easiest way to search for public songs:

```rust
use sunocore::models::search::SearchRequest;

// Just search for a string
let request = SearchRequest::simple_public_song("lo-fi beats")?;
```

### Building Complex Queries

Use the builder pattern for full control:

```rust
use sunocore::models::search::SearchQuery;

let query = SearchQuery::public_song("jazz")
    .rank_by("trending")
    .size(20)
    .build()?;
```

### Compound Searches (Multiple Query Types)

Search across multiple categories in one request:

```rust
let request = SearchRequest::builder()
    .public_song("jazz")
    .user("artist_name")
    .playlist("chill_vibes")
    .build()?;
```

## API Overview

### SearchRequest

The top-level request container for one or more search queries.

**Methods:**
- `simple_public_song(term)` - Quick search for public songs
- `builder()` - Create a compound request with multiple queries

**Examples:**

```rust
// Simple search
let req = SearchRequest::simple_public_song("ambient")?;

// Compound search
let req = SearchRequest::builder()
    .public_song("jazz")
    .similar_to("song-uuid")
    .user("producer")
    .build()?;
```

### SearchQuery

Individual search query within a request.

**Convenience Constructors:**
- `SearchQuery::public_song(term)` - Search public songs
- `SearchQuery::library_song(term)` - Search user's library
- `SearchQuery::similar_to(song_id)` - Find similar songs
- `SearchQuery::user(term)` - Search users
- `SearchQuery::playlist(term)` - Search playlists
- `SearchQuery::suno_shorts(term)` - Search Suno Shorts
- `SearchQuery::custom(search_type)` - Custom search type

**Builder Methods:**
- `.term(term)` - Set search term
- `.rank_by(method)` - Set ranking (e.g., "most_relevant", "trending", "most_recent")
- `.size(count)` - Set page size (1-100)
- `.from_index(offset)` - Set pagination offset
- `.is_instrumental(bool)` - Filter instrumental songs
- `.filters(filters)` - Apply complex filters
- `.pagination(pagination)` - Use pagination helper
- `.name(name)` - Override auto-generated name (advanced)
- And many more...

**Examples:**

```rust
// Search for ambient music
let query = SearchQuery::public_song("ambient")
    .rank_by("trending")
    .size(20)
    .build()?;

// Find similar songs
let query = SearchQuery::similar_to("550e8400-e29b-41d4-a716-446655440000")
    .size(10)
    .build()?;

// Search with filters
let filters = SearchFilters::builder()
    .full_song()
    .no_covers()
    .build();

let query = SearchQuery::public_song("jazz")
    .filters(filters)
    .is_instrumental(true)
    .build()?;
```

### SearchFilters

Complex filtering options for songs.

**Builder Methods:**
- `.full_song()` - Only full-length songs
- `.no_covers()` - Exclude covers
- `.is_cover(bool)` - Include/exclude covers
- `.is_extend(bool)` - Filter extended versions
- `.is_infill(bool)` - Filter infilled songs
- `.is_persona(bool)` - Filter persona-generated songs
- `.is_scene(bool)` - Filter image-to-song songs
- `.is_upsample(bool)` - Filter upsampled songs
- `.is_video_to_song(bool)` - Filter video-to-song songs
- `.is_gen_stem(bool)` - Filter generated stems

**Examples:**

```rust
// Full songs, no covers
let filters = SearchFilters::builder()
    .full_song()
    .no_covers()
    .build();

// Combination of filters
let filters = SearchFilters::builder()
    .full_song()
    .is_extend(false)
    .is_cover(false)
    .build();
```

### Pagination

Helper struct for managing pagination.

**Constructor Methods:**
- `Pagination::first_page(size)` - Create first page
- `Pagination::new(from_index, size)` - Create with explicit offset
- `Pagination::page(page_number, size)` - Create by page number (1-indexed)

**Navigation Methods:**
- `.next_page()` - Get next page
- `.prev_page()` - Get previous page

**Examples:**

```rust
// Get first page with 20 results
let page1 = Pagination::first_page(20)?;

// Navigate pages
let page2 = page1.next_page();
let page3 = page2.next_page();

// Go back
let page2_again = page3.prev_page();

// Or use page numbers
let page2 = Pagination::page(2, 20)?;
```

## Advanced Usage

### Validation

All queries are validated when built. Validation checks:
- Query name is not empty
- Required fields for specific search types are present (e.g., `song_id` for similar searches)
- Page size is between 1 and 100
- Pagination offsets are non-negative

**Error Handling:**

```rust
let query = SearchQuery::similar_to("song-uuid").build();

match query {
    Ok(q) => { /* use query */ },
    Err(ValidationError::MissingRequiredField { field, search_type }) => {
        eprintln!("Missing field: {} for {}", field, search_type);
    }
    Err(ValidationError::InvalidSize { size }) => {
        eprintln!("Invalid page size: {}", size);
    }
    Err(e) => eprintln!("Error: {}", e),
}
```

### Automatic Name Generation

Query names are automatically generated from `search_type` + `term`:
- `SearchType::PublicSong` + "jazz" → `"public_songjazz"`
- `SearchType::LibrarySong` + "ambient" → `"library_songambient"`
- `SearchType::User` + "artist" → `"userartist"`

To use a custom name:

```rust
let query = SearchQuery::public_song("test")
    .name("my_custom_search_name")
    .build()?;

// Name will be "my_custom_search_name" instead of "public_songtest"
```

### Service Integration

For HTTP requests, use the service layer:

```rust
use sunocore::services::search::CompoundSearchRequest;

// Simple search
let request = CompoundSearchRequest::simple_search(
    "https://api.example.com",
    "lo-fi beats",
    Some("api_key".to_string()),
)?;

// From builder
let request = CompoundSearchRequest::from_builder(
    "https://api.example.com",
    SearchRequest::builder()
        .public_song("jazz")
        .user("artist"),
    Some("api_key".to_string()),
)?;
```

## Backward Compatibility

All existing low-level APIs remain unchanged. You can still construct queries manually:

```rust
// Old way (still works)
let query = SearchQuery {
    name: "public_songjazz".to_string(),
    search_type: SearchType::PublicSong,
    term: Some("jazz".to_string()),
    rank_by: Some("most_relevant".to_string()),
    size: Some(20),
    // ... all other fields
};

// New ergonomic way
let query = SearchQuery::public_song("jazz")
    .rank_by("most_relevant")
    .size(20)
    .build()?;
```

## Examples

### Example 1: Search Public Songs with Filters

```rust
use sunocore::models::search::{SearchRequest, SearchQuery, SearchFilters};

let filters = SearchFilters::builder()
    .full_song()
    .no_covers()
    .build();

let query = SearchQuery::public_song("ambient")
    .filters(filters)
    .is_instrumental(true)
    .rank_by("most_recent")
    .size(50)
    .build()?;

let request = SearchRequest::builder()
    .add_query(query)
    .build()?;
```

### Example 2: Compound Search

```rust
let request = SearchRequest::builder()
    .public_song("lo-fi hip hop")
    .similar_to("550e8400-e29b-41d4-a716-446655440000")
    .user("artist_name")
    .playlist("chill")
    .build()?;

// This creates 4 separate search queries in one request
```

### Example 3: Paginated Search

```rust
let mut page = Pagination::first_page(20)?;

loop {
    let query = SearchQuery::public_song("jazz")
        .pagination(page)
        .build()?;

    let request = SearchRequest::builder()
        .add_query(query)
        .build()?;

    // Make API call...
    // If results.len() < 20, break
    // Otherwise: page = page.next_page()
}
```

## Validation Error Types

- `ValidationError::EmptyName` - Query name is empty
- `ValidationError::MissingRequiredField { field, search_type }` - Required field missing
- `ValidationError::InvalidSize { size }` - Page size out of range (1-100)
- `ValidationError::InvalidPagination { reason }` - Invalid pagination parameters
- `ValidationError::EmptySearchRequest` - Request has no queries

## Testing

Run tests with:

```bash
# Unit tests for search models
cargo test --lib models::search

# All library tests
cargo test --lib

# Integration tests (requires API access)
export SUNO_BASE_URL="http://127.0.0.1:8000"
export SUNO_API_KEY="your-api-key"
cargo test --test integration_discover_search -- --ignored
```

## Performance Notes

- All validation happens before making HTTP requests
- No allocations for None fields (uses `#[serde(skip_serializing_if = "Option::is_none")]`)
- Query names use simple string concatenation (O(n) where n = search_type + term length)
- Pagination helpers use integer arithmetic only

## Migration Guide

If you're currently using the low-level API:

**Before:**
```rust
let query = SearchQuery {
    name: "public_songjazz".to_string(),
    search_type: SearchType::PublicSong,
    term: Some("jazz".to_string()),
    from_index: Some(0),
    size: Some(20),
    rank_by: Some("most_relevant".to_string()),
    // ... 23 more None fields ...
};
```

**After:**
```rust
let query = SearchQuery::public_song("jazz")
    .rank_by("most_relevant")
    .size(20)
    .build()?;
```

The new API is fully backward compatible - the low-level way still works!
