use bytes::Bytes;
use chrono::Local;
use futures_util::future::BoxFuture;
use http::{Request, Response, header};
use http_body_util::Empty;
use http_body_util::{BodyExt, Full};
use rmcp::handler::server::router::tool::ToolRouter;
use rmcp::handler::server::tool::Parameters;
use rmcp::model::{
    CallToolResult, Content, Implementation, ProtocolVersion, ServerCapabilities, ServerInfo,
};
use rmcp::schemars::{self, JsonSchema};
use rmcp::{ErrorData as McpError, ServerHandler, tool, tool_handler, tool_router};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::path::PathBuf;
use std::time::Instant;
use std::time::Duration;
use sunocore::models::discover::DiscoverRequest;
use sunocore::models::feed::FeedRequest;
use sunocore::models::search::{
    SearchQuery, SearchQueryBuilder, SearchResponse, SearchResultItem, SearchType,
};
use sunocore::services::aligned_lyrics::{GetAlignedLyricsV3, GetAlignedLyricsV3Request};
use sunocore::services::cdn_mp3::{GetCdnMp3, GetCdnMp3Request};
use sunocore::services::clips_listing::{
    GetClipPrompts, GetClipPromptsRequest, GetDirectChildrenByUser, GetDirectChildrenByUserRequest,
    GetSimilarClips, GetSimilarClipsRequest, GetSongsById, GetSongsByIdsRequest, GetTrashedClips,
    GetTrashedClipsRequest,
};
use sunocore::services::discover::{
    GetDiscoverSection, GetDiscoverSectionRequest, GetHomePageSections, GetHomePageSectionsRequest,
    GetShortcuts, GetShortcutsRequest,
};
use sunocore::services::downbeats::{GetDownbeats, GetDownbeatsRequest};
use sunocore::services::feed::{GetFeed, GetFeedRequest};
use sunocore::services::instruments::{GetInstruments, GetInstrumentsRequest};
use sunocore::services::key::{GetKey, GetKeyRequest};
use sunocore::services::midi::{GetMidi, GetMidiRequest};
use sunocore::services::playlist::{
    GetLikedPlaylists, GetLikedPlaylistsRequest, GetPlaylist, GetPlaylistRequest,
    GetPlaylistsWithClipStatus, GetPlaylistsWithClipStatusRequest, GetUserPlaylists,
    GetUserPlaylistsRequest,
};
use sunocore::services::profiles_listing::{
    GetBlockedUsers, GetBlockedUsersRequest, GetFollowers, GetFollowersRequest, GetFollowing,
    GetFollowingRequest, GetListenHistory, GetListenHistoryRequest, GetPinnedClips,
    GetPinnedClipsRequest, GetRecentClips, GetRecentClipsRequest, GetRemixesInspired,
    GetRemixesInspiredRequest, GetTopClips, GetTopClipsRequest, GetUserProfile,
    GetUserProfileRequest,
};
use sunocore::services::search::SearchClient;
use sunocore::services::studio::{GetStudioProject, GetStudioProjectRequest};
use sunocore::services::waveform::{GetWaveformAggregates, GetWaveformAggregatesRequest};
use sunocore::services::generation::{
    SubmitGeneration, SubmitGenerationRequest, BatchPoll, BatchPollRequest,
    ContinueGeneration, ContinueGenerationRequest, InfillGeneration, InfillGenerationRequest,
};
use sunocore::models::generation::{GenerateParams, ContinueGenerationParams, InfillGenerationParams, ClipStatus};
use sunocore::validation::env;
use tower::Service;
use uuid::Uuid;

/// Service exposing Suno API search and discovery as MCP tools.
#[derive(Clone)]
pub struct EnvironmentService {
    tool_router: ToolRouter<EnvironmentService>,
    http_client: reqwest::Client,
}

/// Tower service adapter around `reqwest::Client` for Full<Bytes> bodies
#[derive(Clone)]
struct ReqwestService {
    client: reqwest::Client,
}

impl ReqwestService {
    fn new(client: reqwest::Client) -> Self {
        Self { client }
    }
}

impl Service<Request<Full<Bytes>>> for ReqwestService {
    type Response = Response<Full<Bytes>>;
    type Error = reqwest::Error;
    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;

    fn poll_ready(
        &mut self,
        _cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Result<(), Self::Error>> {
        std::task::Poll::Ready(Ok(()))
    }

    fn call(&mut self, req: Request<Full<Bytes>>) -> Self::Future {
        let client = self.client.clone();
        let method = req.method().clone();
        let uri = req.uri().to_string();
        let headers = req.headers().clone();
        let body = req.into_body();

        Box::pin(async move {
            let body_bytes = body
                .collect()
                .await
                .expect("Full<Bytes> bodies are infallible")
                .to_bytes();

            let mut builder = client.request(method, uri);
            for (name, value) in headers.iter() {
                builder = builder.header(name, value);
            }
            let response = builder.body(body_bytes.to_vec()).send().await?;

            let status = response.status();
            let content_type = response.headers().get(header::CONTENT_TYPE).cloned();
            let bytes = response.bytes().await?;

            let mut http_response = Response::new(Full::new(Bytes::from(bytes)));
            *http_response.status_mut() = status;
            if let Some(ct) = content_type {
                http_response.headers_mut().insert(header::CONTENT_TYPE, ct);
            }

            Ok(http_response)
        })
    }
}

// Tower service adapter for Empty<Bytes> bodies (GET requests)
impl Service<Request<Empty<Bytes>>> for ReqwestService {
    type Response = Response<Full<Bytes>>;
    type Error = reqwest::Error;
    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;

    fn poll_ready(
        &mut self,
        _cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Result<(), Self::Error>> {
        std::task::Poll::Ready(Ok(()))
    }

    fn call(&mut self, req: Request<Empty<Bytes>>) -> Self::Future {
        let client = self.client.clone();
        let method = req.method().clone();
        let uri = req.uri().to_string();
        let headers = req.headers().clone();

        Box::pin(async move {
            let mut builder = client.request(method, uri);
            for (name, value) in headers.iter() {
                builder = builder.header(name, value);
            }
            let response = builder.send().await?;

            let status = response.status();
            let content_type = response.headers().get(header::CONTENT_TYPE).cloned();
            let bytes = response.bytes().await?;

            let mut http_response = Response::new(Full::new(Bytes::from(bytes)));
            *http_response.status_mut() = status;
            if let Some(ct) = content_type {
                http_response.headers_mut().insert(header::CONTENT_TYPE, ct);
            }

            Ok(http_response)
        })
    }
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct SearchSongsInput {
    term: String,
    #[serde(default)]
    limit: Option<u32>,
    #[serde(default)]
    offset: Option<u32>,
    #[serde(default)]
    rank_by: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct SearchUsersInput {
    term: String,
    #[serde(default)]
    limit: Option<u32>,
    #[serde(default)]
    offset: Option<u32>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct SearchPlaylistsInput {
    term: String,
    #[serde(default)]
    limit: Option<u32>,
    #[serde(default)]
    offset: Option<u32>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct SimilarSongsInput {
    song_id: String,
    #[serde(default)]
    limit: Option<u32>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct SearchLyricsInput {
    lyrics: String,
    #[serde(default)]
    limit: Option<u32>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct SearchGenreInput {
    genre: String,
    #[serde(default)]
    limit: Option<u32>,
    #[serde(default)]
    offset: Option<u32>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct SearchHashtagInput {
    hashtag: String,
    #[serde(default)]
    limit: Option<u32>,
    #[serde(default)]
    offset: Option<u32>,
}

// ============================================================================
// PHASE 1: CONTENT/MEDIA ACCESS INPUT STRUCTS
// ============================================================================

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetSimilarClipsInput {
    clip_id: String,
    #[serde(default)]
    count: Option<i32>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetSongsByIdsInput {
    clip_ids: Vec<String>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetCdnMp3Input {
    clip_id: String,
}

// ============================================================================
// PHASE 2: USER PROFILES & SOCIAL GRAPH INPUT STRUCTS
// ============================================================================

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetUserProfileInput {
    handle: String,
    #[serde(default)]
    page: Option<i32>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetFollowersInput {
    handle: String,
    #[serde(default)]
    page: Option<i32>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetFollowingInput {
    handle: String,
    #[serde(default)]
    page: Option<i32>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetTopClipsInput {
    #[serde(default)]
    size: Option<i32>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetPinnedClipsInput {}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetRecentClipsInput {
    handle: String,
}

// ============================================================================
// PHASE 3: USER CONTENT MANAGEMENT INPUT STRUCTS
// ============================================================================

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetUserPlaylistsInput {
    #[serde(default)]
    page: Option<i32>,
    #[serde(default)]
    query: Option<String>,
    #[serde(default)]
    show_trashed: Option<bool>,
    #[serde(default)]
    show_liked: Option<bool>,
    #[serde(default)]
    show_sharelist: Option<bool>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetTrashedClipsInput {
    #[serde(default)]
    page: Option<i32>,
    #[serde(default)]
    cursor: Option<String>,
    #[serde(default)]
    page_size: Option<i32>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetClipPromptsInput {
    #[serde(default)]
    page: Option<i32>,
    #[serde(default)]
    per_page: Option<i32>,
    #[serde(default)]
    filter_prompt_type: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetListenHistoryInput {
    handle: String,
    #[serde(default)]
    cursor: Option<String>,
    #[serde(default)]
    limit: Option<i32>,
}

// ============================================================================
// PHASE 4: MUSIC ANALYSIS INPUT STRUCTS
// ============================================================================

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetAlignedLyricsV3Input {
    clip_id: String,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetDownbeatsInput {
    clip_id: String,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetInstrumentsInput {
    clip_id: String,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetKeyInput {
    clip_id: String,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetMidiInput {
    clip_id: String,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetWaveformAggregatesInput {
    clip_id: String,
}

// ============================================================================
// PHASE 5: DISCOVERY & FEED INPUT STRUCTS
// ============================================================================

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetFeedInput {}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetHomePageSectionsInput {}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetDiscoverSectionInput {
    section_name: String,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetShortcutsInput {
    shortcut_type: String,
    #[serde(default)]
    context_song_id: Option<String>,
}

// ============================================================================
// PHASE 6: PLAYLIST & PROJECT MANAGEMENT INPUT STRUCTS
// ============================================================================

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetLikedPlaylistsInput {
    #[serde(default)]
    page: Option<i32>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetPlaylistsWithClipStatusInput {
    clip_id: String,
    #[serde(default)]
    page: Option<i32>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetPlaylistInput {
    playlist_id: String,
    #[serde(default)]
    page: Option<i32>,
    #[serde(default)]
    cursor: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetStudioProjectInput {
    project_id: String,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetDirectChildrenByUserInput {
    clip_id: String,
    #[serde(default)]
    page: Option<i32>,
    #[serde(default)]
    page_size: Option<i32>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetBlockedUsersInput {
    #[serde(default)]
    page: Option<i32>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct GetRemixesInspiredInput {
    handle: String,
    #[serde(default)]
    page: Option<i32>,
    #[serde(default)]
    page_size: Option<i32>,
}

// ========================================================================
// GENERATION INPUT STRUCTS
// ========================================================================

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct SubmitGenerationInput {
    prompt: String,
    #[serde(default)]
    title: Option<String>,
    #[serde(default)]
    tags: Option<Vec<String>>,
    #[serde(default)]
    make_instrumental: Option<bool>,
    #[serde(default)]
    model: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct SubmitGenerationAndDownloadInput {
    prompt: String,
    #[serde(default)]
    title: Option<String>,
    #[serde(default)]
    tags: Option<Vec<String>>,
    #[serde(default)]
    make_instrumental: Option<bool>,
    #[serde(default)]
    model: Option<String>,
    #[serde(default)]
    output_dir: Option<String>,
    #[serde(default)]
    max_wait_seconds: Option<u64>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct PollGenerationInput {
    clip_ids: Vec<String>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct ContinueGenerationInput {
    continue_clip_id: String,
    prompt: String,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct InfillGenerationInput {
    clip_id: String,
    start_s: f32,
    end_s: f32,
    prompt: String,
}

/// Save search results to cache folder
fn save_search_results(search_type: &str, term: &str, raw_json: &str) -> Result<(), McpError> {
    // Use environment variable if set, otherwise use a default location
    let cache_dir = if let Ok(cache_path) = std::env::var("SUNO_CACHE_DIR") {
        PathBuf::from(cache_path)
    } else {
        // Try to find the oxide directory
        let current_exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("."));
        let oxide_root = current_exe
            .ancestors()
            .find(|p| p.ends_with("oxide"))
            .map(|p| p.to_path_buf())
            .unwrap_or_else(|| PathBuf::from("."));
        oxide_root.join("cache")
    };

    // Create cache directory if it doesn't exist
    std::fs::create_dir_all(&cache_dir).map_err(|err| {
        McpError::internal_error(format!("Failed to create cache dir: {}", err), None)
    })?;

    // Create filename: search_type-term-timestamp.json
    let timestamp = Local::now().format("%Y%m%d_%H%M%S");
    let safe_term = term.replace(' ', "_").replace('/', "_").to_lowercase();
    let filename = format!("{}_{}-{}.json", search_type, safe_term, timestamp);
    let filepath = cache_dir.join(&filename);

    // Save as pretty JSON
    let parsed: Value = serde_json::from_str(raw_json)
        .map_err(|err| McpError::internal_error(format!("Failed to parse JSON: {}", err), None))?;

    let pretty_json = serde_json::to_string_pretty(&parsed).map_err(|err| {
        McpError::internal_error(format!("Failed to stringify JSON: {}", err), None)
    })?;

    std::fs::write(&filepath, pretty_json)
        .map_err(|err| McpError::internal_error(format!("Failed to write cache: {}", err), None))?;

    tracing::info!("Saved search results to: {}", filepath.display());
    Ok(())
}

/// Summarize search results to reduce token usage
fn summarize_search_results(raw_json: &str, max_results: usize) -> Result<String, McpError> {
    let response: Value = serde_json::from_str(raw_json).map_err(|err| {
        McpError::internal_error(format!("Failed to parse response: {}", err), None)
    })?;

    // Extract results from the nested structure
    let result = response
        .get("result")
        .and_then(|r| r.as_object())
        .ok_or_else(|| McpError::internal_error("No results in response".to_string(), None))?;

    let mut songs = Vec::new();

    for (_, query_results) in result.iter() {
        if let Some(items) = query_results.get("result").and_then(|r| r.as_array()) {
            for item in items.iter().take(max_results - songs.len()) {
                if let Some(song) = item.get("song").or_else(|| Some(item)) {
                    let id = song.get("id").and_then(|i| i.as_str()).unwrap_or("unknown");
                    let title = song
                        .get("title")
                        .and_then(|t| t.as_str())
                        .unwrap_or("Unknown");
                    let handle = song
                        .get("handle")
                        .and_then(|h| h.as_str())
                        .unwrap_or("Unknown");
                    let upvotes = song
                        .get("upvote_count")
                        .and_then(|u| u.as_i64())
                        .unwrap_or(0);
                    let plays = song.get("play_count").and_then(|p| p.as_i64()).unwrap_or(0);
                    let model_name = song
                        .get("model_name")
                        .and_then(|m| m.as_str())
                        .unwrap_or("unknown");
                    let genre = song
                        .get("genre")
                        .and_then(|g| g.as_str())
                        .or_else(|| song.get("goofy_description").and_then(|g| g.as_str()))
                        .unwrap_or("");

                    songs.push(json!({
                        "id": id,
                        "title": title,
                        "artist": handle,
                        "upvotes": upvotes,
                        "plays": plays,
                        "model": model_name,
                        "genre": genre
                    }));
                }

                if songs.len() >= max_results {
                    break;
                }
            }
        }
    }

    let summary = json!({
        "total_songs": songs.len(),
        "songs": songs
    });

    Ok(serde_json::to_string_pretty(&summary)
        .map_err(|err| McpError::internal_error(err.to_string(), None))?)
}

fn internal_error(context: &str, err: impl std::fmt::Display) -> McpError {
    McpError::internal_error(format!("{context}: {err}"), None)
}

fn cache_and_summarize_response(
    cache_key: &str,
    term: &str,
    response: &SearchResponse,
    summary_limit: usize,
) -> Result<String, McpError> {
    let raw_json = serde_json::to_string(response)
        .map_err(|err| internal_error("Failed to encode search response", err))?;

    if let Err(err) = save_search_results(cache_key, term, &raw_json) {
        tracing::warn!("Failed to save search results to cache: {}", err);
    }

    summarize_search_results(&raw_json, summary_limit)
}

fn extract_user_from_response(response: &SearchResponse, handle: &str) -> Option<(Uuid, String)> {
    let profiles: Vec<_> = response
        .result
        .values()
        .flat_map(|bucket| bucket.result.iter())
        .filter_map(|item| match item {
            SearchResultItem::Profile(profile) => Some(profile),
            _ => None,
        })
        .collect();

    if profiles.is_empty() {
        return None;
    }

    profiles
        .iter()
        .find(|profile| profile.handle.eq_ignore_ascii_case(handle))
        .map(|profile| (profile.user_id, profile.handle.clone()))
        .or_else(|| {
            profiles
                .first()
                .map(|profile| (profile.user_id, profile.handle.clone()))
        })
}

#[tool_router]
impl EnvironmentService {
    fn build_search_client(&self) -> Result<SearchClient<ReqwestService>, McpError> {
        let base_url = env::suno_base_url().map_err(|err| internal_error("SUNO_BASE_URL", err))?;
        let api_key = env::suno_api_key().map_err(|err| internal_error("SUNO_API_KEY", err))?;

        let transport = ReqwestService::new(self.http_client.clone());
        let client = SearchClient::new(base_url, transport)
            .map_err(|err| internal_error("Search client init", err))?;

        Ok(client.with_api_key(api_key))
    }

    fn get_base_url_and_api_key(&self) -> Result<(String, Option<String>), McpError> {
        let base_url = env::suno_base_url().map_err(|err| internal_error("SUNO_BASE_URL", err))?;
        let api_key = env::suno_api_key().map_err(|err| internal_error("SUNO_API_KEY", err))?;
        Ok((base_url, api_key))
    }

    async fn run_query_with_summary(
        &self,
        cache_key: &str,
        term: &str,
        builder: SearchQueryBuilder,
        summary_limit: usize,
    ) -> Result<String, McpError> {
        let client = self.build_search_client()?;
        let response = client
            .execute_query_builder(builder)
            .await
            .map_err(|err| internal_error("Search request failed", err))?;
        cache_and_summarize_response(cache_key, term, &response, summary_limit)
    }

    /// Construct a new environment service with registered tool router.
    pub fn new() -> Self {
        Self {
            tool_router: Self::tool_router(),
            http_client: reqwest::Client::new(),
        }
    }

    #[tool(description = "Summarize the configured Suno environment variables")]
    async fn summarize_environment(&self) -> Result<CallToolResult, McpError> {
        let base_url =
            env::suno_base_url().map_err(|err| McpError::internal_error(err.to_string(), None))?;
        let api_key =
            env::suno_api_key().map_err(|err| McpError::internal_error(err.to_string(), None))?;
        let workspace_id =
            env::workspace_id().map_err(|err| McpError::internal_error(err.to_string(), None))?;
        let cdn_base_url =
            env::cdn_base_url().map_err(|err| McpError::internal_error(err.to_string(), None))?;

        let summary = serde_json::json!({
            "base_url": base_url,
            "workspace_id": workspace_id,
            "cdn_base_url": cdn_base_url,
            "api_key": api_key,
        });

        let summary_text = serde_json::to_string_pretty(&summary)
            .map_err(|err| McpError::internal_error(err.to_string(), None))?;

        Ok(CallToolResult::success(vec![Content::text(summary_text)]))
    }

    #[tool(description = "Search for songs by title, artist, or keywords")]
    async fn search_songs(
        &self,
        Parameters(input): Parameters<SearchSongsInput>,
    ) -> Result<CallToolResult, McpError> {
        let SearchSongsInput {
            term,
            limit,
            offset,
            rank_by,
        } = input;

        let term = term.trim();
        if term.is_empty() {
            return Err(McpError::invalid_params("term must not be empty", None));
        }

        let limit = limit.unwrap_or(20).min(100) as i32;
        let offset = offset.unwrap_or(0) as i32;

        let builder = {
            let builder = SearchQuery::public_song(term.to_owned())
                .size(limit)
                .from_index(offset);
            match rank_by {
                Some(rank) => builder.rank_by(rank),
                None => builder,
            }
        };

        let summary = self
            .run_query_with_summary("songs", term, builder, 20)
            .await?;
        Ok(CallToolResult::success(vec![Content::text(summary)]))
    }

    #[tool(description = "Search for users by username or name")]
    async fn search_users(
        &self,
        Parameters(input): Parameters<SearchUsersInput>,
    ) -> Result<CallToolResult, McpError> {
        let SearchUsersInput {
            term,
            limit,
            offset,
        } = input;

        let term = term.trim();
        if term.is_empty() {
            return Err(McpError::invalid_params("term must not be empty", None));
        }

        let limit = limit.unwrap_or(10).min(50) as i32;
        let offset = offset.unwrap_or(0) as i32;

        let builder = SearchQuery::user(term.to_owned())
            .size(limit)
            .from_index(offset);

        let summary = self
            .run_query_with_summary("users", term, builder, 10)
            .await?;
        Ok(CallToolResult::success(vec![Content::text(summary)]))
    }

    #[tool(description = "Search for playlists by name")]
    async fn search_playlists(
        &self,
        Parameters(input): Parameters<SearchPlaylistsInput>,
    ) -> Result<CallToolResult, McpError> {
        let SearchPlaylistsInput {
            term,
            limit,
            offset,
        } = input;

        let term = term.trim();
        if term.is_empty() {
            return Err(McpError::invalid_params("term must not be empty", None));
        }

        let limit = limit.unwrap_or(10).min(50) as i32;
        let offset = offset.unwrap_or(0) as i32;

        let builder = SearchQuery::playlist(term.to_owned())
            .size(limit)
            .from_index(offset);

        let summary = self
            .run_query_with_summary("playlists", term, builder, 10)
            .await?;
        Ok(CallToolResult::success(vec![Content::text(summary)]))
    }

    #[tool(description = "Find songs similar to a given song by ID")]
    async fn similar_songs(
        &self,
        Parameters(input): Parameters<SimilarSongsInput>,
    ) -> Result<CallToolResult, McpError> {
        let SimilarSongsInput { song_id, limit } = input;

        let song_id = song_id.trim();
        if song_id.is_empty() {
            return Err(McpError::invalid_params("song_id must not be empty", None));
        }

        let limit = limit.unwrap_or(20).min(100) as i32;

        let builder = SearchQuery::similar_to(song_id.to_owned()).size(limit);

        let summary = self
            .run_query_with_summary("similar", song_id, builder, 20)
            .await?;
        Ok(CallToolResult::success(vec![Content::text(summary)]))
    }

    #[tool(description = "Search for songs by a specific creator/artist handle")]
    async fn search_by_creator(
        &self,
        Parameters(input): Parameters<SearchSongsInput>,
    ) -> Result<CallToolResult, McpError> {
        let SearchSongsInput {
            term,
            limit,
            offset,
            ..
        } = input;

        let creator_handle = term.trim();
        if creator_handle.is_empty() {
            return Err(McpError::invalid_params(
                "creator handle must not be empty",
                None,
            ));
        }

        let limit = limit.unwrap_or(20).min(100) as i32;
        let offset = offset.unwrap_or(0) as i32;

        let client = self.build_search_client()?;

        let user_response = client
            .execute_query_builder(
                SearchQuery::user(creator_handle.to_owned())
                    .size(1)
                    .from_index(0),
            )
            .await
            .map_err(|err| internal_error("User lookup failed", err))?;

        let (user_id, _resolved_handle) =
            extract_user_from_response(&user_response, creator_handle).ok_or_else(|| {
                McpError::internal_error(
                    format!("No user found with handle '{creator_handle}'"),
                    None,
                )
            })?;

        let songs_response = client
            .execute_query_builder(
                SearchQuery::custom(SearchType::TopUserSongs)
                    .term(user_id.to_string())
                    .size(limit)
                    .from_index(offset),
            )
            .await
            .map_err(|err| internal_error("Creator songs search failed", err))?;

        let summary = cache_and_summarize_response("creator", creator_handle, &songs_response, 20)?;
        Ok(CallToolResult::success(vec![Content::text(summary)]))
    }

    #[tool(description = "Search for songs by genre or tag")]
    async fn search_by_genre(
        &self,
        Parameters(input): Parameters<SearchGenreInput>,
    ) -> Result<CallToolResult, McpError> {
        let SearchGenreInput {
            genre,
            limit,
            offset,
        } = input;

        let genre = genre.trim();
        if genre.is_empty() {
            return Err(McpError::invalid_params("genre must not be empty", None));
        }

        let limit = limit.unwrap_or(20).min(100) as i32;
        let offset = offset.unwrap_or(0) as i32;

        let builder = SearchQuery::tag_song(genre.to_owned())
            .rank_by("most_relevant")
            .size(limit)
            .from_index(offset);

        let summary = self
            .run_query_with_summary("genre", genre, builder, 20)
            .await?;
        Ok(CallToolResult::success(vec![Content::text(summary)]))
    }

    #[tool(description = "Search for songs by lyrics content")]
    async fn search_by_lyrics(
        &self,
        Parameters(input): Parameters<SearchLyricsInput>,
    ) -> Result<CallToolResult, McpError> {
        let SearchLyricsInput { lyrics, limit } = input;

        let lyrics = lyrics.trim();
        if lyrics.is_empty() {
            return Err(McpError::invalid_params("lyrics must not be empty", None));
        }

        let limit = limit.unwrap_or(10).min(50) as i32;

        let builder = SearchQuery::lyrics(lyrics.to_owned()).size(limit);

        let summary = self
            .run_query_with_summary("lyrics", lyrics, builder, 10)
            .await?;
        Ok(CallToolResult::success(vec![Content::text(summary)]))
    }

    #[tool(description = "Search for songs by hashtag")]
    async fn search_by_hashtag(
        &self,
        Parameters(input): Parameters<SearchHashtagInput>,
    ) -> Result<CallToolResult, McpError> {
        let SearchHashtagInput {
            hashtag,
            limit,
            offset,
        } = input;

        let hashtag = hashtag.trim();
        if hashtag.is_empty() {
            return Err(McpError::invalid_params("hashtag must not be empty", None));
        }

        let limit = limit.unwrap_or(20).min(100) as i32;
        let offset = offset.unwrap_or(0) as i32;

        let builder = SearchQuery::hashtag_song(hashtag.to_owned())
            .size(limit)
            .from_index(offset);

        let summary = self
            .run_query_with_summary("hashtag", hashtag, builder, 20)
            .await?;
        Ok(CallToolResult::success(vec![Content::text(summary)]))
    }

    // ========================================================================
    // PHASE 1: CONTENT/MEDIA ACCESS TOOLS
    // ========================================================================

    #[tool(description = "Find clips similar to a given clip ID")]
    async fn get_similar_clips(
        &self,
        Parameters(input): Parameters<GetSimilarClipsInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let clip_id = Uuid::parse_str(input.clip_id.trim())
            .map_err(|_| McpError::invalid_params("Invalid clip_id UUID format", None))?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetSimilarClips::new(transport);

        let request = GetSimilarClipsRequest {
            base_url,
            clip_id,
            count: input.count,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetSimilarClips request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get multiple songs by their IDs")]
    async fn get_songs_by_ids(
        &self,
        Parameters(input): Parameters<GetSongsByIdsInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;

        let clip_ids: Result<Vec<Uuid>, _> = input
            .clip_ids
            .iter()
            .map(|id| Uuid::parse_str(id.trim()))
            .collect();

        let clip_ids = clip_ids
            .map_err(|_| McpError::invalid_params("Invalid clip_id UUID format in list", None))?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetSongsById::new(transport);

        let request = GetSongsByIdsRequest {
            base_url,
            clip_ids,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetSongsById request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Download and save MP3 file for a clip from CDN")]
    async fn get_cdn_mp3(
        &self,
        Parameters(input): Parameters<GetCdnMp3Input>,
    ) -> Result<CallToolResult, McpError> {
        let cdn_base_url =
            env::cdn_base_url().map_err(|err| internal_error("CDN_BASE_URL", err))?;

        let clip_id = Uuid::parse_str(input.clip_id.trim())
            .map_err(|_| McpError::invalid_params("Invalid clip_id UUID format", None))?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetCdnMp3::new(transport);

        let request = GetCdnMp3Request {
            base_url: cdn_base_url,
            clip_id,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetCdnMp3 request failed", err))?;

        // Determine output directory
        let output_dir = std::env::var("SUNO_OUTPUT_DIR")
            .unwrap_or_else(|_| "./downloads".to_string());

        std::fs::create_dir_all(&output_dir)
            .map_err(|err| internal_error("Failed to create output directory", err))?;

        // Save MP3 file
        let filename = format!("{}.mp3", input.clip_id);
        let file_path = PathBuf::from(&output_dir).join(&filename);

        std::fs::write(&file_path, &response.data)
            .map_err(|err| internal_error("Failed to write MP3 file", err))?;

        let result = json!({
            "clip_id": input.clip_id,
            "local_path": file_path.to_string_lossy(),
            "size_bytes": response.data.len(),
            "status": "downloaded"
        });

        Ok(CallToolResult::success(vec![Content::text(
            serde_json::to_string_pretty(&result)
                .map_err(|err| internal_error("Failed to serialize response", err))?
        )]))
    }

    // ========================================================================
    // PHASE 2: USER PROFILES & SOCIAL GRAPH TOOLS
    // ========================================================================

    #[tool(description = "Get user profile by handle")]
    async fn get_user_profile(
        &self,
        Parameters(input): Parameters<GetUserProfileInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let handle = input.handle.trim();
        if handle.is_empty() {
            return Err(McpError::invalid_params("handle must not be empty", None));
        }

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetUserProfile::new(transport);

        let request = GetUserProfileRequest {
            base_url,
            handle: handle.to_string(),
            page: input.page,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetUserProfile request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get followers of a user")]
    async fn get_followers(
        &self,
        Parameters(input): Parameters<GetFollowersInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let handle = input.handle.trim();
        if handle.is_empty() {
            return Err(McpError::invalid_params("handle must not be empty", None));
        }

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetFollowers::new(transport);

        let request = GetFollowersRequest {
            base_url,
            handle: handle.to_string(),
            page: input.page,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetFollowers request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get users that a user is following")]
    async fn get_following(
        &self,
        Parameters(input): Parameters<GetFollowingInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let handle = input.handle.trim();
        if handle.is_empty() {
            return Err(McpError::invalid_params("handle must not be empty", None));
        }

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetFollowing::new(transport);

        let request = GetFollowingRequest {
            base_url,
            handle: handle.to_string(),
            page: input.page,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetFollowing request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get top clips globally")]
    async fn get_top_clips(
        &self,
        Parameters(input): Parameters<GetTopClipsInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetTopClips::new(transport);

        let request = GetTopClipsRequest {
            base_url,
            size: input.size,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetTopClips request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get pinned clips")]
    async fn get_pinned_clips(
        &self,
        Parameters(_input): Parameters<GetPinnedClipsInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetPinnedClips::new(transport);

        let request = GetPinnedClipsRequest { base_url, api_key };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetPinnedClips request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get recent clips from a user")]
    async fn get_recent_clips(
        &self,
        Parameters(input): Parameters<GetRecentClipsInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let handle = input.handle.trim();
        if handle.is_empty() {
            return Err(McpError::invalid_params("handle must not be empty", None));
        }

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetRecentClips::new(transport);

        let request = GetRecentClipsRequest {
            base_url,
            handle: handle.to_string(),
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetRecentClips request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    // ========================================================================
    // PHASE 3: USER CONTENT MANAGEMENT TOOLS
    // ========================================================================

    #[tool(description = "Get user's playlists")]
    async fn get_user_playlists(
        &self,
        Parameters(input): Parameters<GetUserPlaylistsInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetUserPlaylists::new(transport);

        let request = GetUserPlaylistsRequest {
            base_url,
            page: input.page,
            query: input.query,
            show_trashed: input.show_trashed,
            show_liked: input.show_liked,
            show_sharelist: input.show_sharelist,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetUserPlaylists request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get trashed clips")]
    async fn get_trashed_clips(
        &self,
        Parameters(input): Parameters<GetTrashedClipsInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetTrashedClips::new(transport);

        let request = GetTrashedClipsRequest {
            base_url,
            page: input.page,
            cursor: input.cursor,
            page_size: input.page_size,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetTrashedClips request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get clip prompts")]
    async fn get_clip_prompts(
        &self,
        Parameters(input): Parameters<GetClipPromptsInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetClipPrompts::new(transport);

        let request = GetClipPromptsRequest {
            base_url,
            page: input.page,
            per_page: input.per_page,
            filter_prompt_type: input.filter_prompt_type,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetClipPrompts request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get listen history for a user")]
    async fn get_listen_history(
        &self,
        Parameters(input): Parameters<GetListenHistoryInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let handle = input.handle.trim();
        if handle.is_empty() {
            return Err(McpError::invalid_params("handle must not be empty", None));
        }

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetListenHistory::new(transport);

        let request = GetListenHistoryRequest {
            base_url,
            handle: handle.to_string(),
            cursor: input.cursor,
            limit: input.limit,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetListenHistory request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    // ========================================================================
    // PHASE 4: MUSIC ANALYSIS TOOLS
    // ========================================================================

    #[tool(description = "Get aligned lyrics for a clip (v3)")]
    async fn get_aligned_lyrics_v3(
        &self,
        Parameters(input): Parameters<GetAlignedLyricsV3Input>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let clip_id = Uuid::parse_str(input.clip_id.trim())
            .map_err(|_| McpError::invalid_params("Invalid clip_id UUID format", None))?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetAlignedLyricsV3::new(transport);

        let request = GetAlignedLyricsV3Request {
            base_url,
            clip_id,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetAlignedLyricsV3 request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get downbeats for a clip")]
    async fn get_downbeats(
        &self,
        Parameters(input): Parameters<GetDownbeatsInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let clip_id = Uuid::parse_str(input.clip_id.trim())
            .map_err(|_| McpError::invalid_params("Invalid clip_id UUID format", None))?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetDownbeats::new(transport);

        let request = GetDownbeatsRequest {
            base_url,
            clip_id,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetDownbeats request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get instruments in a clip")]
    async fn get_instruments(
        &self,
        Parameters(input): Parameters<GetInstrumentsInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let clip_id = Uuid::parse_str(input.clip_id.trim())
            .map_err(|_| McpError::invalid_params("Invalid clip_id UUID format", None))?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetInstruments::new(transport);

        let request = GetInstrumentsRequest {
            base_url,
            clip_id,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetInstruments request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get musical key of a clip")]
    async fn get_key(
        &self,
        Parameters(input): Parameters<GetKeyInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let clip_id = Uuid::parse_str(input.clip_id.trim())
            .map_err(|_| McpError::invalid_params("Invalid clip_id UUID format", None))?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetKey::new(transport);

        let request = GetKeyRequest {
            base_url,
            clip_id,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetKey request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get MIDI data for a clip")]
    async fn get_midi(
        &self,
        Parameters(input): Parameters<GetMidiInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let clip_id = Uuid::parse_str(input.clip_id.trim())
            .map_err(|_| McpError::invalid_params("Invalid clip_id UUID format", None))?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetMidi::new(transport);

        let request = GetMidiRequest {
            base_url,
            clip_id,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetMidi request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get waveform aggregates for a clip")]
    async fn get_waveform_aggregates(
        &self,
        Parameters(input): Parameters<GetWaveformAggregatesInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let clip_id = Uuid::parse_str(input.clip_id.trim())
            .map_err(|_| McpError::invalid_params("Invalid clip_id UUID format", None))?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetWaveformAggregates::new(transport);

        let request = GetWaveformAggregatesRequest {
            base_url,
            clip_id,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetWaveformAggregates request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    // ========================================================================
    // PHASE 5: DISCOVERY & FEED TOOLS
    // ========================================================================

    #[tool(description = "Get home feed")]
    async fn get_feed(
        &self,
        Parameters(_input): Parameters<GetFeedInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let workspace_id =
            env::workspace_id().map_err(|err| internal_error("WORKSPACE_ID", err))?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetFeed::new(transport);

        let feed_request = FeedRequest {
            cursor: None,
            limit: 50,
            filters: sunocore::models::feed::FeedFilters {
                disliked: "excluded".to_string(),
                trashed: "excluded".to_string(),
                from_studio_project: sunocore::models::feed::PresenceFilter {
                    presence: "excluded".to_string(),
                },
                stem: sunocore::models::feed::PresenceFilter {
                    presence: "excluded".to_string(),
                },
                workspace: sunocore::models::feed::WorkspaceFilter {
                    presence: "included".to_string(),
                    workspace_id,
                },
            },
        };

        let request = GetFeedRequest {
            base_url,
            feed_request,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetFeed request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get home page discovery sections")]
    async fn get_home_page_sections(
        &self,
        Parameters(_input): Parameters<GetHomePageSectionsInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetHomePageSections::new(transport);

        let discover_request = DiscoverRequest {
            product: None,
            language: "en".to_string(),
            start_index: 0,
            page_size: 10,
            section_name: None,
            section_content: None,
            secondary_section_content: None,
            page: 1,
            platform: "web".to_string(),
            disable_shuffle: None,
            section_size: None,
        };

        let request = GetHomePageSectionsRequest {
            base_url,
            discover_request,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetHomePageSections request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get a specific discover section")]
    async fn get_discover_section(
        &self,
        Parameters(input): Parameters<GetDiscoverSectionInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let section_name = input.section_name.trim();
        if section_name.is_empty() {
            return Err(McpError::invalid_params(
                "section_name must not be empty",
                None,
            ));
        }

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetDiscoverSection::new(transport);

        let request = GetDiscoverSectionRequest {
            base_url,
            section_name: section_name.to_string(),
            discover_request: None,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetDiscoverSection request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get discovery shortcuts")]
    async fn get_shortcuts(
        &self,
        Parameters(input): Parameters<GetShortcutsInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let shortcut_type = input.shortcut_type.trim();
        if shortcut_type.is_empty() {
            return Err(McpError::invalid_params(
                "shortcut_type must not be empty",
                None,
            ));
        }

        let context_song_id = if let Some(id) = input.context_song_id {
            let uuid = Uuid::parse_str(id.trim()).map_err(|_| {
                McpError::invalid_params("Invalid context_song_id UUID format", None)
            })?;
            Some(uuid.to_string())
        } else {
            None
        };

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetShortcuts::new(transport);

        let request = GetShortcutsRequest {
            base_url,
            shortcut_type: shortcut_type.to_string(),
            context_song_id,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetShortcuts request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    // ========================================================================
    // PHASE 6: PLAYLIST & PROJECT MANAGEMENT TOOLS
    // ========================================================================

    #[tool(description = "Get liked playlists")]
    async fn get_liked_playlists(
        &self,
        Parameters(input): Parameters<GetLikedPlaylistsInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetLikedPlaylists::new(transport);

        let request = GetLikedPlaylistsRequest {
            base_url,
            page: input.page,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetLikedPlaylists request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get playlists with clip status")]
    async fn get_playlists_with_clip_status(
        &self,
        Parameters(input): Parameters<GetPlaylistsWithClipStatusInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let clip_id = input.clip_id.trim().to_string();

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetPlaylistsWithClipStatus::new(transport);

        let request = GetPlaylistsWithClipStatusRequest {
            base_url,
            clip_id,
            page: input.page,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetPlaylistsWithClipStatus request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get a specific playlist")]
    async fn get_playlist(
        &self,
        Parameters(input): Parameters<GetPlaylistInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let playlist_id = input.playlist_id.trim().to_string();

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetPlaylist::new(transport);

        let request = GetPlaylistRequest {
            base_url,
            playlist_id,
            page: input.page,
            cursor: input.cursor,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetPlaylist request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get studio project details")]
    async fn get_studio_project(
        &self,
        Parameters(input): Parameters<GetStudioProjectInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let project_id = Uuid::parse_str(input.project_id.trim())
            .map_err(|_| McpError::invalid_params("Invalid project_id UUID format", None))?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetStudioProject::new(transport);

        let request = GetStudioProjectRequest {
            base_url,
            project_id,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetStudioProject request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get direct children of a clip by user")]
    async fn get_direct_children_by_user(
        &self,
        Parameters(input): Parameters<GetDirectChildrenByUserInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let clip_id = Uuid::parse_str(input.clip_id.trim())
            .map_err(|_| McpError::invalid_params("Invalid clip_id UUID format", None))?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetDirectChildrenByUser::new(transport);

        let request = GetDirectChildrenByUserRequest {
            base_url,
            clip_id,
            page: input.page,
            page_size: input.page_size,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetDirectChildrenByUser request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get blocked users")]
    async fn get_blocked_users(
        &self,
        Parameters(input): Parameters<GetBlockedUsersInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetBlockedUsers::new(transport);

        let request = GetBlockedUsersRequest {
            base_url,
            page: input.page,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetBlockedUsers request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Get remixes inspired by user")]
    async fn get_remixes_inspired(
        &self,
        Parameters(input): Parameters<GetRemixesInspiredInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let handle = input.handle.trim();
        if handle.is_empty() {
            return Err(McpError::invalid_params("handle must not be empty", None));
        }

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = GetRemixesInspired::new(transport);

        let request = GetRemixesInspiredRequest {
            base_url,
            handle: handle.to_string(),
            page: input.page,
            page_size: input.page_size,
            api_key,
        };

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("GetRemixesInspired request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    // ========================================================================
    // GENERATION TOOLS
    // ========================================================================

    #[tool(description = "Submit a song generation request. Returns clip IDs that can be polled for completion.")]
    async fn submit_generation(
        &self,
        Parameters(input): Parameters<SubmitGenerationInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let api_key = api_key.ok_or_else(|| McpError::internal_error("SUNO_API_KEY required for generation", None))?;

        let mut params = GenerateParams::new(input.prompt);

        if let Some(title) = input.title {
            params = params.with_title(title);
        }
        if let Some(tags) = input.tags {
            // Convert array of tags to comma-separated string for API
            let tags_str = tags.join(", ");
            params = params.with_tags(tags_str);
        }
        if let Some(instrumental) = input.make_instrumental {
            params = params.with_instrumental(instrumental);
        }
        if let Some(model) = input.model {
            params = params.with_model(model);
        }

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = SubmitGeneration::new(transport);

        let request = SubmitGenerationRequest::new(base_url, params, api_key);

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("SubmitGeneration request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Submit a song generation request, wait for completion, and automatically download the MP3 file")]
    async fn submit_generation_and_download(
        &self,
        Parameters(input): Parameters<SubmitGenerationAndDownloadInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let api_key = api_key.ok_or_else(|| McpError::internal_error("SUNO_API_KEY required for generation", None))?;

        tracing::info!("Submitting generation request...");

        // 1. Submit generation
        let mut params = GenerateParams::new(input.prompt);

        if let Some(title) = input.title {
            params = params.with_title(title);
        }
        if let Some(tags) = input.tags {
            let tags_str = tags.join(", ");
            params = params.with_tags(tags_str);
        }
        if let Some(instrumental) = input.make_instrumental {
            params = params.with_instrumental(instrumental);
        }
        if let Some(model) = input.model {
            params = params.with_model(model);
        }

        let transport = ReqwestService::new(self.http_client.clone());
        let mut submit_service = SubmitGeneration::new(transport.clone());

        let submit_request = SubmitGenerationRequest::new(base_url.clone(), params, api_key.clone());

        let generation_response = submit_service
            .call(submit_request)
            .await
            .map_err(|err| internal_error("SubmitGeneration request failed", err))?;

        let clip_ids: Vec<String> = generation_response.clips.iter()
            .map(|c| c.id.clone())
            .collect();

        tracing::info!("Generation submitted. Clip IDs: {:?}", clip_ids);

        // 2. Poll until completion with exponential backoff
        let max_wait = input.max_wait_seconds.unwrap_or(300);
        let start_time = Instant::now();
        let mut backoff_ms = 1000u64;
        let max_backoff_ms = 30000u64;
        let backoff_multiplier = 1.5;

        let mut poll_service = BatchPoll::new(transport.clone());
        let mut completed_downloads = vec![];
        let mut pending_clips = clip_ids.clone();

        loop {
            // Check timeout
            if start_time.elapsed().as_secs() >= max_wait {
                return Err(McpError::internal_error(
                    format!("Generation timed out after {} seconds", max_wait),
                    None
                ));
            }

            // Poll current status
            let poll_request = BatchPollRequest::new(
                base_url.clone(),
                pending_clips.clone(),
                api_key.clone()
            );

            let statuses = poll_service
                .call(poll_request)
                .await
                .map_err(|err| internal_error("Polling failed", err))?;

            tracing::info!("Poll attempt - {} clips: {:?}",
                          pending_clips.len(),
                          statuses.iter().map(|s| &s.status).collect::<Vec<_>>());

            // Check for completed clips with CDN URLs
            let mut newly_completed = vec![];
            for status in statuses {
                if status.status == ClipStatus::Complete && status.audio_url.is_some() {
                    newly_completed.push(status);
                } else if status.status == ClipStatus::Error {
                    return Err(McpError::internal_error(
                        format!("Generation failed for clip {}: {}",
                                status.clip_id,
                                status.error.unwrap_or_else(|| "Unknown error".into())),
                        None
                    ));
                }
            }

            // 3. Download completed clips
            for status in newly_completed {
                tracing::info!("Clip {} complete. Downloading MP3...", status.clip_id);

                let cdn_base_url = env::cdn_base_url()
                    .map_err(|err| internal_error("CDN_BASE_URL", err))?;

                let clip_uuid = Uuid::parse_str(&status.clip_id)
                    .map_err(|_| McpError::invalid_params("Invalid clip_id format", None))?;

                let mut download_service = GetCdnMp3::new(transport.clone());
                let download_request = GetCdnMp3Request {
                    base_url: cdn_base_url,
                    clip_id: clip_uuid,
                };

                let mp3_response = download_service
                    .call(download_request)
                    .await
                    .map_err(|err| internal_error("MP3 download failed", err))?;

                // 4. Save to disk
                let env_output_dir = std::env::var("SUNO_OUTPUT_DIR");
                let output_dir = input.output_dir.as_deref()
                    .or_else(|| env_output_dir.as_deref().ok())
                    .unwrap_or("./downloads");

                std::fs::create_dir_all(output_dir)
                    .map_err(|err| internal_error("Failed to create output directory", err))?;

                let filename = format!("{}.mp3", status.clip_id);
                let file_path = PathBuf::from(output_dir).join(&filename);

                std::fs::write(&file_path, &mp3_response.data)
                    .map_err(|err| internal_error("Failed to write MP3 file", err))?;

                tracing::info!("Downloaded: {}", file_path.display());

                completed_downloads.push(json!({
                    "clip_id": status.clip_id,
                    "audio_url": status.audio_url,
                    "local_path": file_path.to_string_lossy(),
                    "size_bytes": mp3_response.data.len(),
                    "status": "downloaded"
                }));

                // Remove from pending
                pending_clips.retain(|id| id != &status.clip_id);
            }

            // Check if all complete
            if pending_clips.is_empty() {
                break;
            }

            // Exponential backoff
            let sleep_ms = backoff_ms;
            tracing::info!("Waiting {}ms before next poll ({} clips remaining)...",
                          sleep_ms,
                          pending_clips.len());
            tokio::time::sleep(Duration::from_millis(sleep_ms)).await;

            backoff_ms = ((backoff_ms as f64 * backoff_multiplier) as u64).min(max_backoff_ms);
        }

        // 5. Return results
        let result = json!({
            "status": "success",
            "total_clips": clip_ids.len(),
            "downloads": completed_downloads,
            "elapsed_seconds": start_time.elapsed().as_secs()
        });

        Ok(CallToolResult::success(vec![Content::text(
            serde_json::to_string_pretty(&result)
                .map_err(|err| internal_error("Failed to serialize response", err))?
        )]))
    }

    #[tool(description = "Poll generation status for one or more clip IDs. Use this after submit_generation to check if clips are ready.")]
    async fn poll_generation_status(
        &self,
        Parameters(input): Parameters<PollGenerationInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let api_key = api_key.ok_or_else(|| McpError::internal_error("SUNO_API_KEY required", None))?;

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = BatchPoll::new(transport);

        let request = BatchPollRequest::new(base_url, input.clip_ids, api_key);

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("Poll request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Continue generation from an existing clip")]
    async fn continue_generation(
        &self,
        Parameters(input): Parameters<ContinueGenerationInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let api_key = api_key.ok_or_else(|| McpError::internal_error("SUNO_API_KEY required", None))?;

        let params = ContinueGenerationParams::new(input.continue_clip_id, input.prompt);

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = ContinueGeneration::new(transport);

        let request = ContinueGenerationRequest::new(base_url, params, api_key);

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("ContinueGeneration request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

    #[tool(description = "Infill a section of an existing clip")]
    async fn infill_generation(
        &self,
        Parameters(input): Parameters<InfillGenerationInput>,
    ) -> Result<CallToolResult, McpError> {
        let (base_url, api_key) = self.get_base_url_and_api_key()?;
        let api_key = api_key.ok_or_else(|| McpError::internal_error("SUNO_API_KEY required", None))?;

        let params = InfillGenerationParams::new(
            input.clip_id,
            input.start_s,
            input.end_s,
            input.prompt,
        );

        let transport = ReqwestService::new(self.http_client.clone());
        let mut service = InfillGeneration::new(transport);

        let request = InfillGenerationRequest::new(base_url, params, api_key);

        let response = service
            .call(request)
            .await
            .map_err(|err| internal_error("InfillGeneration request failed", err))?;

        let result = serde_json::to_string_pretty(&response)
            .map_err(|err| internal_error("Failed to serialize response", err))?;
        Ok(CallToolResult::success(vec![Content::text(result)]))
    }

}

#[tool_handler]
impl ServerHandler for EnvironmentService {
    fn get_info(&self) -> ServerInfo {
        ServerInfo {
            protocol_version: ProtocolVersion::V_2024_11_05,
            capabilities: ServerCapabilities::builder().enable_tools().build(),
            server_info: Implementation::from_build_env(),
            instructions: Some(
                "Suno MCP Server - Complete API coverage with 36 tools spanning search, profiles, content, analysis, discovery, and playlists.\n\
                \n\
                SEARCH TOOLS (9):\n\
                1. search_songs - Search public songs by title/keywords\n\
                2. search_users - Search for creators/artists by handle or name\n\
                3. search_playlists - Search for playlists\n\
                4. search_by_creator - Find all songs by a specific creator/artist\n\
                5. search_by_genre - Search songs by genre or tag\n\
                6. search_by_lyrics - Find songs matching specific lyrics\n\
                7. search_by_hashtag - Search songs by hashtag\n\
                8. similar_songs - Find songs similar to a specific song by ID\n\
                9. summarize_environment - Get configuration info\n\
                \n\
                CONTENT/MEDIA ACCESS (3):\n\
                10. get_similar_clips - Find clips similar to a given clip ID\n\
                11. get_songs_by_ids - Get multiple songs by their IDs\n\
                12. get_cdn_mp3 - Get MP3 file for a clip from CDN\n\
                \n\
                USER PROFILES & SOCIAL (7):\n\
                13. get_user_profile - Get user profile by handle\n\
                14. get_followers - Get followers of a user\n\
                15. get_following - Get users that a user is following\n\
                16. get_top_clips - Get top clips globally\n\
                17. get_pinned_clips - Get pinned clips\n\
                18. get_recent_clips - Get recent clips from a user\n\
                19. get_blocked_users - Get blocked users\n\
                \n\
                USER CONTENT MANAGEMENT (4):\n\
                20. get_user_playlists - Get user's playlists\n\
                21. get_trashed_clips - Get trashed clips\n\
                22. get_clip_prompts - Get clip prompts\n\
                23. get_listen_history - Get listen history for a user\n\
                \n\
                MUSIC ANALYSIS (6):\n\
                24. get_aligned_lyrics_v3 - Get aligned lyrics for a clip\n\
                25. get_downbeats - Get downbeats for a clip\n\
                26. get_instruments - Get instruments in a clip\n\
                27. get_key - Get musical key of a clip\n\
                28. get_midi - Get MIDI data for a clip\n\
                29. get_waveform_aggregates - Get waveform aggregates for a clip\n\
                \n\
                DISCOVERY & FEED (4):\n\
                30. get_feed - Get home feed\n\
                31. get_home_page_sections - Get home page discovery sections\n\
                32. get_discover_section - Get a specific discover section\n\
                33. get_shortcuts - Get discovery shortcuts\n\
                \n\
                PLAYLIST & PROJECT MANAGEMENT (6):\n\
                34. get_liked_playlists - Get liked playlists\n\
                35. get_playlists_with_clip_status - Get playlists with clip status\n\
                36. get_playlist - Get a specific playlist\n\
                37. get_studio_project - Get studio project details\n\
                38. get_direct_children_by_user - Get direct children of a clip by user\n\
                39. get_remixes_inspired - Get remixes inspired by user\n\
                \n\
                GENERATION TOOLS (4):\n\
                40. submit_generation - Submit a song generation request\n\
                41. poll_generation_status - Poll clip status (returns CDN URLs when complete)\n\
                42. continue_generation - Continue/extend from an existing clip\n\
                43. infill_generation - Infill a section of a clip\n\
                \n\
                Note: Audio URLs are returned directly in poll_generation_status responses when clips are complete.\n\
                Use get_cdn_mp3 to download MP3 files from CDN URLs.\n\
                \n\
                100% COVERAGE: All sunocore generation services exposed through MCP tools.".to_string(),
            ),
        }
    }
}
