// ABOUTME: Model Context Protocol (MCP) server implementation
// ABOUTME: Provides tools for AI assistants to post and manage micropub content

use anyhow::Result;
use base64::{engine::general_purpose, Engine as _};
use chrono::{DateTime, Utc};
use rmcp::handler::server::router::prompt::PromptRouter;
use rmcp::handler::server::router::tool::ToolRouter;
use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::{
    CallToolResult, Content, ErrorCode, GetPromptRequestParam, GetPromptResult, Implementation,
    ListPromptsResult, PaginatedRequestParam, PromptMessage, PromptMessageRole, ProtocolVersion,
    ServerCapabilities, ServerInfo,
};
use rmcp::prompt;
use rmcp::prompt_handler;
use rmcp::prompt_router;
use rmcp::service::RequestContext;
use rmcp::tool;
use rmcp::tool_handler;
use rmcp::tool_router;
use rmcp::transport::stdio;
use rmcp::ErrorData as McpError;
use rmcp::{schemars, RoleServer, ServerHandler, ServiceExt};

use crate::client::{MicropubAction, MicropubClient, MicropubRequest};
use crate::config::{load_token, Config};
use crate::draft::Draft;
use crate::draft_push::validate_draft_id;
use crate::publish;
use serde_json::{Map, Value};

/// Parameters for publish_post tool
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct PublishPostArgs {
    /// The content of the post
    pub content: String,
    /// Optional title for the post
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    /// Optional comma-separated categories
    #[serde(skip_serializing_if = "Option::is_none")]
    pub categories: Option<String>,
}

/// Parameters for create_draft tool
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct CreateDraftArgs {
    /// The content of the draft
    pub content: String,
    /// Optional title for the draft
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    /// Optional comma-separated categories
    #[serde(skip_serializing_if = "Option::is_none")]
    pub categories: Option<String>,
}

/// Parameters for publish_backdate tool
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct PublishBackdateArgs {
    /// The draft ID to publish (alphanumeric, hyphens, underscores only)
    #[schemars(regex(pattern = r"^[a-zA-Z0-9_-]+$"))]
    pub draft_id: String,
    /// ISO 8601 formatted date (e.g., 2024-01-15T10:30:00Z)
    pub date: String,
}

/// Parameters for delete_post tool
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct DeletePostArgs {
    /// The URL of the post to delete
    #[schemars(url)]
    pub url: String,
}

/// Parameters for undelete_post tool
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct UndeletePostArgs {
    /// The URL of the post to undelete
    #[schemars(url)]
    pub url: String,
}

/// Parameters for update_post tool
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct UpdatePostArgs {
    /// The URL of the post to update
    #[schemars(url)]
    pub url: String,
    /// New content for the post (replaces existing)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content: Option<String>,
    /// New title for the post (replaces existing)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    /// Comma-separated categories (replaces all existing categories)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub categories: Option<String>,
}

/// Parameters for edit_draft tool
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct EditDraftArgs {
    /// The draft ID to edit (alphanumeric, hyphens, underscores only)
    #[schemars(regex(pattern = r"^[a-zA-Z0-9_-]+$"))]
    pub draft_id: String,
    /// New content for the draft (replaces existing)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub content: Option<String>,
    /// New title for the draft (replaces existing)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    /// Comma-separated categories (replaces all existing categories)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub categories: Option<String>,
}

/// Parameters for search_drafts tool
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SearchDraftsArgs {
    /// Search query to match against draft titles, content, and categories
    pub query: String,
}

/// Parameters for list_posts tool
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct ListPostsArgs {
    /// Number of posts to retrieve (default: 10)
    #[serde(default = "default_limit")]
    pub limit: usize,
    /// Offset for pagination (default: 0)
    #[serde(default)]
    pub offset: usize,
}

/// Parameters for view_draft tool
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct ViewDraftArgs {
    /// The draft ID to view
    #[schemars(regex(pattern = r"^[a-zA-Z0-9_-]+$"))]
    pub draft_id: String,
}

/// Parameters for list_media tool
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct ListMediaArgs {
    /// Number of media items to retrieve (default: 20)
    #[serde(default = "default_media_limit")]
    pub limit: usize,
    /// Offset for pagination (default: 0)
    #[serde(default)]
    pub offset: usize,
}

/// Parameters for upload_media tool
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct UploadMediaArgs {
    /// Path to local file (e.g., ~/Pictures/photo.jpg)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub file_path: Option<String>,

    /// Base64-encoded file data (alternative to file_path)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub file_data: Option<String>,

    /// Filename (required when using file_data)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub filename: Option<String>,

    /// Optional alt text for accessibility
    #[serde(skip_serializing_if = "Option::is_none")]
    pub alt_text: Option<String>,
}

/// Parameters for push_draft tool
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct PushDraftArgs {
    /// The draft ID to push to the server as a server-side draft
    #[schemars(regex(pattern = r"^[a-zA-Z0-9_-]+$"))]
    pub draft_id: String,
    /// Optional ISO 8601 formatted date for backdating (e.g., 2024-01-15T10:30:00Z)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub backdate: Option<String>,
}

fn default_limit() -> usize {
    10
}

fn default_media_limit() -> usize {
    20
}

/// Parameters for quick-note prompt
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct QuickNotePromptArgs {
    /// The topic or subject you want to write about (1-200 characters)
    #[schemars(length(min = 1, max = 200))]
    pub topic: String,
}

/// Parameters for photo-post prompt
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct PhotoPostPromptArgs {
    /// What the photo is about or depicts (1-200 characters)
    #[schemars(length(min = 1, max = 200))]
    pub subject: String,
}

/// Parameters for article-draft prompt
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct ArticleDraftPromptArgs {
    /// The article topic or title (1-200 characters)
    #[schemars(length(min = 1, max = 200))]
    pub topic: String,
    /// Key points to cover (optional, max 500 characters)
    #[serde(skip_serializing_if = "Option::is_none")]
    #[schemars(length(max = 500))]
    pub key_points: Option<String>,
}

/// Parameters for backdate-memory prompt
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct BackdateMemoryPromptArgs {
    /// What event or memory to record (1-300 characters)
    #[schemars(length(min = 1, max = 300))]
    pub memory: String,
    /// When it happened (e.g., "last Tuesday", "2024-01-15") (1-100 characters)
    #[schemars(length(min = 1, max = 100))]
    pub when: String,
}

/// Parameters for categorized-post prompt
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct CategorizedPostPromptArgs {
    /// The post topic (1-200 characters)
    #[schemars(length(min = 1, max = 200))]
    pub topic: String,
    /// Categories for the post (comma-separated, 1-100 characters)
    #[schemars(length(min = 1, max = 100))]
    pub categories: String,
}

/// MCP server state
#[derive(Clone)]
pub struct MicropubMcp {
    tool_router: ToolRouter<MicropubMcp>,
    prompt_router: PromptRouter<MicropubMcp>,
}

impl MicropubMcp {
    /// Create a new MCP server instance
    pub fn new() -> Result<Self> {
        Ok(Self {
            tool_router: Self::tool_router(),
            prompt_router: Self::prompt_router(),
        })
    }
}

/// Load the micropub client from the default profile config and token.
/// Shared by tools that need to send requests to the micropub endpoint
/// without going through CLI functions that println! to stdout.
fn load_micropub_client() -> std::result::Result<MicropubClient, McpError> {
    let config = Config::load().map_err(|e| {
        McpError::new(
            ErrorCode::INTERNAL_ERROR,
            format!("Failed to load config: {}", e),
            None,
        )
    })?;

    let profile_name = &config.default_profile;
    if profile_name.is_empty() {
        return Err(McpError::new(
            ErrorCode::INTERNAL_ERROR,
            "No profile configured. Run 'micropub auth <domain>' first.".to_string(),
            None,
        ));
    }

    let profile = config.get_profile(profile_name).ok_or_else(|| {
        McpError::new(
            ErrorCode::INTERNAL_ERROR,
            "Profile not found".to_string(),
            None,
        )
    })?;

    let micropub_endpoint = profile.micropub_endpoint.as_ref().ok_or_else(|| {
        McpError::new(
            ErrorCode::INTERNAL_ERROR,
            "No micropub endpoint configured".to_string(),
            None,
        )
    })?;

    let token = load_token(profile_name).map_err(|e| {
        McpError::new(
            ErrorCode::INTERNAL_ERROR,
            format!("Failed to load token: {}", e),
            None,
        )
    })?;

    Ok(MicropubClient::new(micropub_endpoint.clone(), token))
}

/// Truncate a string to a maximum number of characters, respecting UTF-8 boundaries.
fn truncate_with_ellipsis(s: &str, max_chars: usize) -> String {
    let char_count: usize = s.chars().count();
    if char_count <= max_chars {
        s.to_string()
    } else if max_chars < 4 {
        // Too short for ellipsis to be useful; just truncate
        s.chars().take(max_chars).collect()
    } else {
        let truncated: String = s.chars().take(max_chars - 3).collect();
        format!("{}...", truncated)
    }
}

#[tool_router]
impl MicropubMcp {
    /// Create and publish a post immediately
    #[tool(
        description = "Create and publish a micropub post with optional title and categories. Automatically detects and uploads local image files (e.g., ![alt](~/photo.jpg) or <img src='/path/image.png'>) and replaces them with permanent URLs before publishing."
    )]
    async fn publish_post(
        &self,
        Parameters(args): Parameters<PublishPostArgs>,
    ) -> Result<CallToolResult, McpError> {
        // Validate content is not empty
        if args.content.trim().is_empty() {
            return Err(McpError::invalid_params(
                "Content cannot be empty".to_string(),
                None,
            ));
        }

        // Create a draft first
        let mut draft = Draft::new(uuid::Uuid::new_v4().to_string());
        draft.content = args.content;
        draft.metadata.name = args.title;

        // Parse categories as comma-separated, filtering empty entries
        if let Some(cats) = args.categories {
            draft.metadata.category = cats
                .split(',')
                .map(|s| s.trim().to_string())
                .filter(|s| !s.is_empty())
                .collect();
        }

        let draft_path = draft.save().map_err(|e| {
            McpError::new(
                ErrorCode::INTERNAL_ERROR,
                format!("Failed to save draft: {}", e),
                None,
            )
        })?;

        // Publish it
        let draft_path_str = draft_path.to_str().ok_or_else(|| {
            McpError::new(
                ErrorCode::INTERNAL_ERROR,
                "Draft path contains invalid UTF-8".to_string(),
                None,
            )
        })?;

        let uploads = publish::cmd_publish(draft_path_str, None)
            .await
            .map_err(|e| {
                McpError::new(
                    ErrorCode::INTERNAL_ERROR,
                    format!("Failed to publish: {}", e),
                    None,
                )
            })?;

        let mut message = String::from("Post published successfully!");

        if !uploads.is_empty() {
            message.push_str("\n\nUploaded media:");
            for (filename, url) in uploads {
                message.push_str(&format!("\n- {} -> {}", filename, url));
            }
        }

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

    /// Create a draft post without publishing
    #[tool(description = "Create a draft micropub post for later editing and publishing")]
    async fn create_draft(
        &self,
        Parameters(args): Parameters<CreateDraftArgs>,
    ) -> Result<CallToolResult, McpError> {
        // Validate content is not empty
        if args.content.trim().is_empty() {
            return Err(McpError::invalid_params(
                "Content cannot be empty".to_string(),
                None,
            ));
        }

        let mut draft = Draft::new(uuid::Uuid::new_v4().to_string());
        draft.content = args.content;
        draft.metadata.name = args.title;

        // Parse categories as comma-separated, filtering empty entries
        if let Some(cats) = args.categories {
            draft.metadata.category = cats
                .split(',')
                .map(|s| s.trim().to_string())
                .filter(|s| !s.is_empty())
                .collect();
        }

        draft.save().map_err(|e| {
            McpError::new(
                ErrorCode::INTERNAL_ERROR,
                format!("Failed to create draft: {}", e),
                None,
            )
        })?;

        Ok(CallToolResult::success(vec![Content::text(format!(
            "Draft created with ID: {}",
            draft.id
        ))]))
    }

    /// List all draft posts
    #[tool(description = "List all draft micropub posts")]
    async fn list_drafts(&self) -> Result<CallToolResult, McpError> {
        let draft_ids = Draft::list_all().map_err(|e| {
            McpError::new(
                ErrorCode::INTERNAL_ERROR,
                format!("Failed to list drafts: {}", e),
                None,
            )
        })?;

        if draft_ids.is_empty() {
            return Ok(CallToolResult::success(vec![Content::text(
                "No drafts found.",
            )]));
        }

        let mut output = String::from("Drafts:\n");
        for id in draft_ids {
            if let Ok(draft) = Draft::load(&id) {
                let title = draft
                    .metadata
                    .name
                    .unwrap_or_else(|| "[untitled]".to_string());
                output.push_str(&format!("- {} ({})\n", title, id));
            }
        }

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

    /// Publish a draft with a backdated timestamp
    #[tool(description = "Publish a draft post with a specific past date (ISO 8601 format)")]
    async fn publish_backdate(
        &self,
        Parameters(args): Parameters<PublishBackdateArgs>,
    ) -> Result<CallToolResult, McpError> {
        // Validate draft_id format to prevent path traversal
        validate_draft_id(&args.draft_id)
            .map_err(|e| McpError::invalid_params(format!("Invalid draft ID: {}", e), None))?;

        // Parse the date
        let parsed_date = DateTime::parse_from_rfc3339(&args.date)
            .map_err(|e| {
                McpError::invalid_params(
                    format!(
                        "Invalid date format: {}. Use ISO 8601 like 2024-01-15T10:30:00Z",
                        e
                    ),
                    None,
                )
            })?
            .with_timezone(&Utc);

        // Load draft path
        let draft_path = crate::config::get_drafts_dir()
            .map_err(|e| {
                McpError::new(
                    ErrorCode::INTERNAL_ERROR,
                    format!("Failed to get drafts dir: {}", e),
                    None,
                )
            })?
            .join(format!("{}.md", args.draft_id));

        if !draft_path.exists() {
            return Err(McpError::invalid_params(
                format!("Draft not found: {}", args.draft_id),
                None,
            ));
        }

        // Publish with backdate
        let draft_path_str = draft_path.to_str().ok_or_else(|| {
            McpError::new(
                ErrorCode::INTERNAL_ERROR,
                "Draft path contains invalid UTF-8".to_string(),
                None,
            )
        })?;

        let uploads = publish::cmd_publish(draft_path_str, Some(parsed_date))
            .await
            .map_err(|e| {
                McpError::new(
                    ErrorCode::INTERNAL_ERROR,
                    format!("Failed to publish: {}", e),
                    None,
                )
            })?;

        let mut message = format!("Post published with backdated timestamp: {}", args.date);

        if !uploads.is_empty() {
            message.push_str("\n\nUploaded media:");
            for (filename, url) in uploads {
                message.push_str(&format!("\n- {} -> {}", filename, url));
            }
        }

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

    /// Delete a published post
    #[tool(description = "Delete a published micropub post by URL")]
    async fn delete_post(
        &self,
        Parameters(args): Parameters<DeletePostArgs>,
    ) -> Result<CallToolResult, McpError> {
        if args.url.is_empty() {
            return Err(McpError::invalid_params(
                "URL cannot be empty".to_string(),
                None,
            ));
        }

        let client = load_micropub_client()?;

        let request = MicropubRequest {
            action: MicropubAction::Delete,
            properties: Map::new(),
            url: Some(args.url.clone()),
        };

        client.send(&request).await.map_err(|e| {
            McpError::new(
                ErrorCode::INTERNAL_ERROR,
                format!("Failed to delete post: {}", e),
                None,
            )
        })?;

        Ok(CallToolResult::success(vec![Content::text(format!(
            "Post deleted: {}",
            args.url
        ))]))
    }

    /// Restore a deleted post
    #[tool(description = "Restore a previously deleted micropub post by URL")]
    async fn undelete_post(
        &self,
        Parameters(args): Parameters<UndeletePostArgs>,
    ) -> Result<CallToolResult, McpError> {
        if args.url.is_empty() {
            return Err(McpError::invalid_params(
                "URL cannot be empty".to_string(),
                None,
            ));
        }

        let client = load_micropub_client()?;

        let request = MicropubRequest {
            action: MicropubAction::Undelete,
            properties: Map::new(),
            url: Some(args.url.clone()),
        };

        client.send(&request).await.map_err(|e| {
            McpError::new(
                ErrorCode::INTERNAL_ERROR,
                format!("Failed to undelete post: {}", e),
                None,
            )
        })?;

        Ok(CallToolResult::success(vec![Content::text(format!(
            "Post restored: {}",
            args.url
        ))]))
    }

    /// Update an existing published post
    #[tool(
        description = "Update an existing micropub post. Provide the post URL and any fields to change. Only provided fields are updated; omitted fields remain unchanged."
    )]
    async fn update_post(
        &self,
        Parameters(args): Parameters<UpdatePostArgs>,
    ) -> Result<CallToolResult, McpError> {
        if args.url.is_empty() {
            return Err(McpError::invalid_params(
                "URL cannot be empty".to_string(),
                None,
            ));
        }

        // Build replace map from provided fields
        let mut replace = Map::new();

        if let Some(content) = args.content {
            replace.insert(
                "content".to_string(),
                Value::Array(vec![Value::String(content)]),
            );
        }
        if let Some(title) = args.title {
            replace.insert("name".to_string(), Value::Array(vec![Value::String(title)]));
        }
        if let Some(cats) = args.categories {
            let cat_values: Vec<Value> = cats
                .split(',')
                .map(|s| s.trim())
                .filter(|s| !s.is_empty())
                .map(|s| Value::String(s.to_string()))
                .collect();
            replace.insert("category".to_string(), Value::Array(cat_values));
        }

        if replace.is_empty() {
            return Err(McpError::invalid_params(
                "At least one field (content, title, or categories) must be provided".to_string(),
                None,
            ));
        }

        let client = load_micropub_client()?;

        let request = MicropubRequest {
            action: MicropubAction::Update {
                replace,
                add: Map::new(),
                delete: Vec::new(),
            },
            properties: Map::new(),
            url: Some(args.url.clone()),
        };

        client.send(&request).await.map_err(|e| {
            McpError::new(
                ErrorCode::INTERNAL_ERROR,
                format!("Failed to update post: {}", e),
                None,
            )
        })?;

        Ok(CallToolResult::success(vec![Content::text(format!(
            "Post updated: {}",
            args.url
        ))]))
    }

    /// Edit a local draft without opening an editor
    #[tool(
        description = "Edit a local draft's content, title, or categories. Only provided fields are updated; omitted fields remain unchanged."
    )]
    async fn edit_draft(
        &self,
        Parameters(args): Parameters<EditDraftArgs>,
    ) -> Result<CallToolResult, McpError> {
        validate_draft_id(&args.draft_id)
            .map_err(|e| McpError::invalid_params(format!("Invalid draft ID: {}", e), None))?;

        let has_updates =
            args.content.is_some() || args.title.is_some() || args.categories.is_some();

        if !has_updates {
            return Err(McpError::invalid_params(
                "At least one field (content, title, or categories) must be provided".to_string(),
                None,
            ));
        }

        let mut draft = Draft::load(&args.draft_id).map_err(|e| {
            McpError::new(
                ErrorCode::INTERNAL_ERROR,
                format!("Failed to load draft: {}", e),
                None,
            )
        })?;

        if let Some(content) = args.content {
            draft.content = content;
        }
        if let Some(title) = args.title {
            // Empty title clears the title (sets to None)
            draft.metadata.name = if title.trim().is_empty() {
                None
            } else {
                Some(title)
            };
        }
        if let Some(cats) = args.categories {
            draft.metadata.category = cats
                .split(',')
                .map(|s| s.trim().to_string())
                .filter(|s| !s.is_empty())
                .collect();
        }

        draft.save().map_err(|e| {
            McpError::new(
                ErrorCode::INTERNAL_ERROR,
                format!("Failed to save draft: {}", e),
                None,
            )
        })?;

        let title_display = draft
            .metadata
            .name
            .unwrap_or_else(|| "[untitled]".to_string());

        Ok(CallToolResult::success(vec![Content::text(format!(
            "Draft updated: {} ({})",
            title_display, args.draft_id
        ))]))
    }

    /// Search local drafts by text
    #[tool(
        description = "Search local drafts by text query. Matches against title, content, and categories."
    )]
    async fn search_drafts(
        &self,
        Parameters(args): Parameters<SearchDraftsArgs>,
    ) -> Result<CallToolResult, McpError> {
        let query = args.query.trim();
        if query.is_empty() {
            return Err(McpError::invalid_params(
                "Search query cannot be empty".to_string(),
                None,
            ));
        }

        let draft_ids = Draft::list_all().map_err(|e| {
            McpError::new(
                ErrorCode::INTERNAL_ERROR,
                format!("Failed to list drafts: {}", e),
                None,
            )
        })?;

        if draft_ids.is_empty() {
            return Ok(CallToolResult::success(vec![Content::text(
                "No drafts found.",
            )]));
        }

        let query_lower = query.to_lowercase();
        let mut output = String::new();
        let mut found_count = 0;

        for id in draft_ids {
            let draft = match Draft::load(&id) {
                Ok(d) => d,
                Err(_) => continue,
            };

            let mut matches = Vec::new();

            if let Some(ref title) = draft.metadata.name {
                if title.to_lowercase().contains(&query_lower) {
                    matches.push("title");
                }
            }

            if draft.content.to_lowercase().contains(&query_lower) {
                matches.push("content");
            }

            if draft
                .metadata
                .category
                .iter()
                .any(|c| c.to_lowercase().contains(&query_lower))
            {
                matches.push("category");
            }

            if !matches.is_empty() {
                found_count += 1;
                let title = draft
                    .metadata
                    .name
                    .unwrap_or_else(|| "[untitled]".to_string());
                output.push_str(&format!(
                    "- {} ({}) [matched: {}]\n",
                    title,
                    id,
                    matches.join(", ")
                ));

                if matches.contains(&"content") {
                    if let Some(snippet) = draft
                        .content
                        .lines()
                        .find(|line| line.to_lowercase().contains(&query_lower))
                    {
                        let preview = truncate_with_ellipsis(snippet, 80);
                        output.push_str(&format!("  > {}\n", preview));
                    }
                }
            }
        }

        if found_count == 0 {
            Ok(CallToolResult::success(vec![Content::text(format!(
                "No drafts found matching '{}'.",
                query
            ))]))
        } else {
            let header = format!("Found {} draft(s) matching '{}':\n\n", found_count, query);
            Ok(CallToolResult::success(vec![Content::text(format!(
                "{}{}",
                header, output
            ))]))
        }
    }

    /// Get authentication status
    #[tool(description = "Check which micropub account is currently authenticated")]
    async fn whoami(&self) -> Result<CallToolResult, McpError> {
        let config = Config::load().map_err(|e| {
            McpError::new(
                ErrorCode::INTERNAL_ERROR,
                format!("Failed to load config: {}", e),
                None,
            )
        })?;

        let profile_name = &config.default_profile;
        if profile_name.is_empty() {
            return Ok(CallToolResult::success(vec![Content::text(
                "No profile configured. Run 'micropub auth <domain>' first.",
            )]));
        }

        let profile = config.get_profile(profile_name).ok_or_else(|| {
            McpError::new(
                ErrorCode::INTERNAL_ERROR,
                "Profile not found".to_string(),
                None,
            )
        })?;

        let output = format!(
            "Authenticated as:\n  Profile: {}\n  Domain: {}\n  Micropub: {}",
            profile_name,
            profile.domain,
            profile
                .micropub_endpoint
                .as_deref()
                .unwrap_or("(not configured)")
        );

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

    /// List published posts
    #[tool(description = "List published micropub posts with pagination")]
    async fn list_posts(
        &self,
        Parameters(args): Parameters<ListPostsArgs>,
    ) -> Result<CallToolResult, McpError> {
        let posts = crate::operations::fetch_posts(args.limit, args.offset)
            .await
            .map_err(|e| {
                McpError::new(
                    ErrorCode::INTERNAL_ERROR,
                    format!("Failed to fetch posts: {}", e),
                    None,
                )
            })?;

        if posts.is_empty() {
            return Ok(CallToolResult::success(vec![Content::text(
                "No posts found.",
            )]));
        }

        let mut output = String::from("Posts:\n\n");
        for post in posts {
            let title = post.name.unwrap_or_else(|| "[untitled]".to_string());
            output.push_str(&format!("- {} ({})\n", title, post.url));
            output.push_str(&format!("  Published: {}\n", post.published));
            if !post.categories.is_empty() {
                output.push_str(&format!("  Categories: {}\n", post.categories.join(", ")));
            }
            if !post.content.is_empty() {
                let preview = truncate_with_ellipsis(&post.content, 100);
                output.push_str(&format!("  Preview: {}\n", preview));
            }
            output.push('\n');
        }

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

    /// View a specific draft
    #[tool(description = "View the content of a specific draft")]
    async fn view_draft(
        &self,
        Parameters(args): Parameters<ViewDraftArgs>,
    ) -> Result<CallToolResult, McpError> {
        // Validate draft_id format to prevent path traversal
        validate_draft_id(&args.draft_id)
            .map_err(|e| McpError::invalid_params(format!("Invalid draft ID: {}", e), None))?;

        let draft = Draft::load(&args.draft_id)
            .map_err(|e| McpError::invalid_params(format!("Failed to load draft: {}", e), None))?;

        let mut output = String::new();
        output.push_str(&format!("Draft: {}\n\n", args.draft_id));

        if let Some(ref title) = draft.metadata.name {
            output.push_str(&format!("Title: {}\n", title));
        }
        if !draft.metadata.category.is_empty() {
            output.push_str(&format!(
                "Categories: {}\n",
                draft.metadata.category.join(", ")
            ));
        }
        output.push_str(&format!("\nContent:\n{}", draft.content));

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

    /// List media files
    #[tool(description = "List uploaded media files with pagination")]
    async fn list_media(
        &self,
        Parameters(args): Parameters<ListMediaArgs>,
    ) -> Result<CallToolResult, McpError> {
        let media_items = crate::operations::fetch_media(args.limit, args.offset)
            .await
            .map_err(|e| {
                McpError::new(
                    ErrorCode::INTERNAL_ERROR,
                    format!("Failed to fetch media: {}", e),
                    None,
                )
            })?;

        if media_items.is_empty() {
            return Ok(CallToolResult::success(vec![Content::text(
                "No media files found.",
            )]));
        }

        let mut output = String::from("Media files:\n\n");
        for item in media_items {
            output.push_str(&format!("- {}\n", item.url));
            if let Some(ref name) = item.name {
                output.push_str(&format!("  Name: {}\n", name));
            }
            output.push_str(&format!("  Uploaded: {}\n\n", item.uploaded));
        }

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

    /// Upload a media file to the micropub media endpoint
    #[tool(
        description = "Upload an image or media file to your micropub site. Supports local file paths (e.g., ~/Pictures/photo.jpg) or base64-encoded data. Returns URL and markdown snippet for use in posts."
    )]
    async fn upload_media(
        &self,
        Parameters(args): Parameters<UploadMediaArgs>,
    ) -> Result<CallToolResult, McpError> {
        // Validate: must have file_path OR (file_data + filename)
        let has_path = args.file_path.is_some();
        let has_data = args.file_data.is_some();
        let has_filename = args.filename.is_some();

        if !has_path && !has_data {
            return Err(McpError::invalid_params(
                "Must provide either file_path OR file_data".to_string(),
                None,
            ));
        }

        if has_path && has_data {
            return Err(McpError::invalid_params(
                "Cannot provide both file_path and file_data".to_string(),
                None,
            ));
        }

        if has_data && !has_filename {
            return Err(McpError::invalid_params(
                "filename is required when using file_data".to_string(),
                None,
            ));
        }

        // Get config and profile
        let config = Config::load().map_err(|e| {
            McpError::new(
                ErrorCode::INTERNAL_ERROR,
                format!("Failed to load config: {}", e),
                None,
            )
        })?;

        let profile_name = &config.default_profile;
        if profile_name.is_empty() {
            return Err(McpError::new(
                ErrorCode::INTERNAL_ERROR,
                "No profile configured. Run 'micropub auth <domain>' first.".to_string(),
                None,
            ));
        }

        let profile = config.get_profile(profile_name).ok_or_else(|| {
            McpError::new(
                ErrorCode::INTERNAL_ERROR,
                "Profile not found".to_string(),
                None,
            )
        })?;

        let media_endpoint = profile.media_endpoint.as_ref().ok_or_else(|| {
            McpError::new(
                ErrorCode::INTERNAL_ERROR,
                "No media endpoint configured. Server may not support media uploads.".to_string(),
                None,
            )
        })?;

        let token = crate::config::load_token(profile_name).map_err(|e| {
            McpError::new(
                ErrorCode::INTERNAL_ERROR,
                format!("Failed to load token: {}", e),
                None,
            )
        })?;

        // Handle file_path upload
        let (url, filename_str, mime_type) = if let Some(file_path) = args.file_path {
            let resolved_path = crate::media::resolve_path(&file_path, None)
                .map_err(|e| McpError::invalid_params(format!("Invalid file path: {}", e), None))?;

            let filename = resolved_path
                .file_name()
                .and_then(|n| n.to_str())
                .ok_or_else(|| McpError::invalid_params("Invalid filename".to_string(), None))?
                .to_string();

            let mime = mime_guess::from_path(&resolved_path).first_or_octet_stream();

            let url = crate::media::upload_file(media_endpoint, &token, &resolved_path)
                .await
                .map_err(|e| {
                    McpError::new(
                        ErrorCode::INTERNAL_ERROR,
                        format!("Upload failed: {}", e),
                        None,
                    )
                })?;

            (url, filename, mime.to_string())
        } else if let Some(file_data) = args.file_data {
            let filename = args.filename.unwrap(); // Already validated above

            // Sanitize filename to prevent path traversal: strip directory components
            let safe_filename = std::path::Path::new(&filename)
                .file_name()
                .and_then(|n| n.to_str())
                .ok_or_else(|| {
                    McpError::invalid_params(
                        "Filename must be a simple filename without path separators".to_string(),
                        None,
                    )
                })?;

            // Decode base64
            let decoded = general_purpose::STANDARD.decode(&file_data).map_err(|e| {
                McpError::invalid_params(format!("Invalid base64 data: {}", e), None)
            })?;

            // Write to temp file with UUID prefix to avoid collisions
            let temp_dir = std::env::temp_dir();
            let temp_path = temp_dir.join(format!(
                "micropub-{}-{}",
                uuid::Uuid::new_v4(),
                safe_filename
            ));

            std::fs::write(&temp_path, decoded).map_err(|e| {
                McpError::new(
                    ErrorCode::INTERNAL_ERROR,
                    format!("Failed to write temp file: {}", e),
                    None,
                )
            })?;

            let mime = mime_guess::from_path(&temp_path).first_or_octet_stream();

            let url = crate::media::upload_file(media_endpoint, &token, &temp_path)
                .await
                .map_err(|e| {
                    // Clean up temp file
                    let _ = std::fs::remove_file(&temp_path);
                    McpError::new(
                        ErrorCode::INTERNAL_ERROR,
                        format!("Upload failed: {}", e),
                        None,
                    )
                })?;

            // Clean up temp file
            let _ = std::fs::remove_file(&temp_path);

            (url, safe_filename.to_string(), mime.to_string())
        } else {
            unreachable!("Validation ensures file_path or file_data is present");
        };

        // Build response
        let alt_text = args.alt_text.unwrap_or_default();
        let markdown = if alt_text.is_empty() {
            format!("![]({})", url)
        } else {
            format!("![{}]({})", alt_text, url)
        };

        let response = serde_json::json!({
            "url": url,
            "filename": filename_str,
            "mime_type": mime_type,
            "markdown": markdown
        });

        Ok(CallToolResult::success(vec![Content::text(
            serde_json::to_string_pretty(&response).unwrap_or_else(|_| response.to_string()),
        )]))
    }

    /// Push a local draft to the server as a server-side draft
    #[tool(
        description = "Push a local draft to the server as a server-side draft (post-status: draft). Uploads any media files and returns the server URL. Can be used to create new server drafts or update existing ones. Supports backdating."
    )]
    async fn push_draft(
        &self,
        Parameters(args): Parameters<PushDraftArgs>,
    ) -> Result<CallToolResult, McpError> {
        // Validate draft_id format to prevent path traversal
        validate_draft_id(&args.draft_id)
            .map_err(|e| McpError::invalid_params(format!("Invalid draft ID: {}", e), None))?;

        // Parse backdate if provided
        let backdate_parsed = if let Some(date_str) = args.backdate {
            let parsed = DateTime::parse_from_rfc3339(&date_str)
                .map_err(|e| {
                    McpError::invalid_params(
                        format!(
                            "Invalid backdate format: {}. Use ISO 8601 like 2024-01-15T10:30:00Z",
                            e
                        ),
                        None,
                    )
                })?
                .with_timezone(&Utc);
            Some(parsed)
        } else {
            None
        };

        // Push draft to server
        let result = crate::draft_push::cmd_push_draft(&args.draft_id, backdate_parsed)
            .await
            .map_err(|e| {
                McpError::new(
                    ErrorCode::INTERNAL_ERROR,
                    format!("Failed to push draft: {}", e),
                    None,
                )
            })?;

        // Build response JSON
        let response = serde_json::json!({
            "url": result.url,
            "is_update": result.is_update,
            "status": "server-draft",
            "uploaded_media": result.uploads.iter().map(|(filename, url)| {
                serde_json::json!({
                    "filename": filename,
                    "url": url
                })
            }).collect::<Vec<_>>()
        });

        Ok(CallToolResult::success(vec![Content::text(
            serde_json::to_string_pretty(&response).unwrap_or_else(|_| response.to_string()),
        )]))
    }
}

/// Prompts for common micropub workflows
#[prompt_router]
impl MicropubMcp {
    /// Template for posting a quick note or thought
    #[prompt(
        name = "quick-note",
        description = "Post a quick note or thought to your micropub site"
    )]
    async fn quick_note(
        &self,
        Parameters(args): Parameters<QuickNotePromptArgs>,
    ) -> Result<GetPromptResult, McpError> {
        // Validate and normalize topic
        let topic = args.topic.trim();
        if topic.is_empty() {
            return Err(McpError::invalid_params(
                "Topic cannot be empty".to_string(),
                None,
            ));
        }

        Ok(GetPromptResult {
            description: Some("Quick note posting workflow".to_string()),
            messages: vec![
                PromptMessage::new_text(
                    PromptMessageRole::User,
                    format!("I want to post a quick note about: {}", topic),
                ),
                PromptMessage::new_text(
                    PromptMessageRole::Assistant,
                    format!(
                        "I'll help you create a quick note about {}. What would you like to say?",
                        topic
                    ),
                ),
            ],
        })
    }

    /// Template for posting a photo with caption
    #[prompt(
        name = "photo-post",
        description = "Create a photo post with caption for your micropub site"
    )]
    async fn photo_post(
        &self,
        Parameters(args): Parameters<PhotoPostPromptArgs>,
    ) -> Result<GetPromptResult, McpError> {
        // Validate and normalize subject
        let subject = args.subject.trim();
        if subject.is_empty() {
            return Err(McpError::invalid_params(
                "Subject cannot be empty".to_string(),
                None,
            ));
        }

        Ok(GetPromptResult {
            description: Some("Photo post workflow".to_string()),
            messages: vec![
                PromptMessage::new_text(
                    PromptMessageRole::User,
                    format!("I want to post a photo about: {}", subject),
                ),
                PromptMessage::new_text(
                    PromptMessageRole::Assistant,
                    format!(
                        "I'll help you create a photo post about {}. You can:\n\
                         1. Upload the image first with 'upload_media' tool, then use the URL\n\
                         2. Reference a local file (e.g., ~/Pictures/photo.jpg) and I'll auto-upload when publishing\n\n\
                         Please provide the photo path and a caption.",
                        subject
                    ),
                ),
            ],
        })
    }

    /// Template for creating a longer article draft
    #[prompt(
        name = "article-draft",
        description = "Create a longer article draft for later editing and publishing"
    )]
    async fn article_draft(
        &self,
        Parameters(args): Parameters<ArticleDraftPromptArgs>,
    ) -> Result<GetPromptResult, McpError> {
        // Validate and normalize topic
        let topic = args.topic.trim();
        if topic.is_empty() {
            return Err(McpError::invalid_params(
                "Topic cannot be empty".to_string(),
                None,
            ));
        }

        // Validate and normalize key_points if provided
        let key_points = args.key_points.as_ref().map(|p| p.trim());
        if let Some(points) = key_points {
            if points.is_empty() {
                return Err(McpError::invalid_params(
                    "Key points cannot be empty if provided".to_string(),
                    None,
                ));
            }
        }

        let key_points_text = if let Some(points) = key_points {
            format!("\n\nKey points to cover:\n{}", points)
        } else {
            String::new()
        };

        Ok(GetPromptResult {
            description: Some("Article draft creation workflow".to_string()),
            messages: vec![
                PromptMessage::new_text(
                    PromptMessageRole::User,
                    format!(
                        "I want to write an article about: {}{}",
                        topic, key_points_text
                    ),
                ),
                PromptMessage::new_text(
                    PromptMessageRole::Assistant,
                    format!(
                        "I'll help you draft an article about {}. Let's start with:\n\
                         1. A compelling title\n\
                         2. An introduction that hooks the reader\n\
                         3. Main body sections{}\n\
                         4. A conclusion\n\n\
                         This will be saved as a draft for you to edit before publishing.",
                        topic,
                        if key_points.is_some() {
                            " covering your key points"
                        } else {
                            ""
                        }
                    ),
                ),
            ],
        })
    }

    /// Template for backdating a memory or past event
    #[prompt(
        name = "backdate-memory",
        description = "Record a memory or past event with its original date"
    )]
    async fn backdate_memory(
        &self,
        Parameters(args): Parameters<BackdateMemoryPromptArgs>,
    ) -> Result<GetPromptResult, McpError> {
        // Validate and normalize memory
        let memory = args.memory.trim();
        if memory.is_empty() {
            return Err(McpError::invalid_params(
                "Memory cannot be empty".to_string(),
                None,
            ));
        }

        // Validate and normalize when
        let when = args.when.trim();
        if when.is_empty() {
            return Err(McpError::invalid_params(
                "When cannot be empty".to_string(),
                None,
            ));
        }

        Ok(GetPromptResult {
            description: Some("Backdated memory recording workflow".to_string()),
            messages: vec![
                PromptMessage::new_text(
                    PromptMessageRole::User,
                    format!("I want to record this memory from {}: {}", when, memory),
                ),
                PromptMessage::new_text(
                    PromptMessageRole::Assistant,
                    format!(
                        "I'll help you record this memory from {}. Let's:\n\
                         1. Write out the full memory in detail\n\
                         2. Convert '{}' to a specific date (ISO 8601 format)\n\
                         3. Save it as a draft\n\
                         4. Publish it with the backdated timestamp\n\n\
                         Tell me more about what happened.",
                        when, when
                    ),
                ),
            ],
        })
    }

    /// Template for creating a categorized post
    #[prompt(
        name = "categorized-post",
        description = "Create a post with specific categories for organization"
    )]
    async fn categorized_post(
        &self,
        Parameters(args): Parameters<CategorizedPostPromptArgs>,
    ) -> Result<GetPromptResult, McpError> {
        // Validate and normalize topic
        let topic = args.topic.trim();
        if topic.is_empty() {
            return Err(McpError::invalid_params(
                "Topic cannot be empty".to_string(),
                None,
            ));
        }

        // Validate and normalize categories
        let categories = args.categories.trim();
        if categories.is_empty() {
            return Err(McpError::invalid_params(
                "Categories cannot be empty".to_string(),
                None,
            ));
        }

        Ok(GetPromptResult {
            description: Some("Categorized post workflow".to_string()),
            messages: vec![
                PromptMessage::new_text(
                    PromptMessageRole::User,
                    format!(
                        "I want to post about {} in categories: {}",
                        topic, categories
                    ),
                ),
                PromptMessage::new_text(
                    PromptMessageRole::Assistant,
                    format!(
                        "I'll help you create a post about {} with categories: {}.\n\n\
                         What would you like to say? I'll make sure to tag it appropriately.",
                        topic, categories
                    ),
                ),
            ],
        })
    }

    /// General micropub posting workflow
    #[prompt(
        name = "new-post",
        description = "General workflow for creating a new micropub post"
    )]
    async fn new_post(&self) -> GetPromptResult {
        GetPromptResult {
            description: Some("General micropub posting workflow".to_string()),
            messages: vec![
                PromptMessage::new_text(
                    PromptMessageRole::User,
                    "I want to create a new post".to_string(),
                ),
                PromptMessage::new_text(
                    PromptMessageRole::Assistant,
                    "I'll help you create a new micropub post! What type of post would you like to make?\n\n\
                     - Quick note or thought\n\
                     - Photo with caption\n\
                     - Longer article (saved as draft)\n\
                     - Backdated memory\n\
                     - Categorized post\n\n\
                     Or just tell me what you want to post and I'll figure out the best format!".to_string(),
                ),
            ],
        }
    }
}

/// Implement ServerHandler to provide server metadata
#[tool_handler]
#[prompt_handler(router = self.prompt_router)]
impl ServerHandler for MicropubMcp {
    fn get_info(&self) -> ServerInfo {
        ServerInfo {
            protocol_version: ProtocolVersion::V_2024_11_05,
            capabilities: ServerCapabilities::builder()
                .enable_tools()
                .enable_prompts()
                .build(),
            server_info: Implementation::from_build_env(),
            instructions: Some(
                "Micropub MCP server for posting and managing micropub content via AI assistants.\n\n\
                 IMAGE UPLOADS:\n\
                 - Use 'upload_media' tool to upload images explicitly (supports file paths or base64 data)\n\
                 - Or use 'publish_post' with local image paths (e.g., ![alt](~/photo.jpg)) - they'll auto-upload\n\n\
                 POST MANAGEMENT:\n\
                 - Use 'update_post' to modify an existing post's content, title, or categories\n\
                 - Use 'undelete_post' to restore a previously deleted post\n\n\
                 DRAFT EDITING:\n\
                 - Use 'edit_draft' to update a draft's content, title, or categories without an editor\n\
                 - Use 'search_drafts' to find drafts by text across titles, content, and categories\n\n\
                 SERVER-SIDE DRAFTS:\n\
                 - Use 'push_draft' tool to save drafts to server with post-status: draft\n\
                 - Drafts remain editable locally and can be re-pushed to update\n\
                 - Use 'publish_post' to change server draft to published status\n\
                 - Supports media upload and backdating when pushing drafts\n\n\
                 All uploads and draft operations require authentication via 'micropub auth <domain>' first."
                    .to_string(),
            ),
        }
    }
}

/// Run the MCP server
pub async fn run_server() -> Result<()> {
    eprintln!("Starting Micropub MCP server...");
    eprintln!("Ready to receive requests via stdio");

    // Create server and serve via stdio
    let service = MicropubMcp::new()?.serve(stdio()).await?;

    // Wait for shutdown
    service.waiting().await?;

    Ok(())
}
