//! MCP server adapter for Sirno.
//!
//! The adapter exposes grouped Sirno command tools and skill resources over stdio.
//! Command behavior remains in `core`; this module only converts JSON parameters
//! into typed core requests and converts core DTOs into MCP tool results.

use std::error::Error;
use std::future::{self, Future};
use std::path::PathBuf;
use std::str::FromStr;

use rmcp::handler::server::router::tool::ToolRouter;
use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::{
    Annotated, CallToolResult, Content, ListResourcesResult, PaginatedRequestParams, RawResource,
    ReadResourceRequestParams, ReadResourceResult, Resource, ResourceContents, ServerCapabilities,
    ServerInfo,
};
use rmcp::service::{MaybeSendFuture, RequestContext};
use rmcp::{
    ErrorData as McpError, RoleServer, ServerHandler, ServiceExt, schemars, schemars::JsonSchema,
    tool, tool_handler, tool_router,
};
use serde::{Deserialize, Serialize};

use crate::core::{
    ArtifactAddRequest, ArtifactRemoveRequest, ArtifactRenameRequest, CoreContext, EntryNewRequest,
    EntryPathRequest, FrostCheckoutRequest, LakeInitRequest, PathSelection, QueryColumn,
    QueryColumns, QueryRequest, RgRequest, StructuralFieldState, StructuralFilter,
    StructuralStateFilter, StructuralTarget, TideResolveRequest, TideSelectionRequest,
    TideStatusMode,
};
use crate::{CheckMode, EntryId, StructuralEdgeDirection, TideWorkitem};

const SKILL_RESOURCE_MIME_TYPE: &str = "text/markdown";

// sirno:witness:design-doc-writer-skill:begin
const DESIGN_DOC_WRITER_SKILL_RESOURCE: SkillResourceSpec = SkillResourceSpec {
    uri: "sirno://skills/design-doc-writer",
    name: "design-doc-writer",
    title: "Design Doc Writer",
    description: "Full design-doc-writer skill text from the bundled lake artifact.",
    content: include_str!("../sirno-docs/.artifacts/design-doc-writer-skill/SKILL.full.md"),
};
// sirno:witness:design-doc-writer-skill:end

// sirno:witness:agent-skills:begin
const SKILL_RESOURCES: &[SkillResourceSpec] = &[
    DESIGN_DOC_WRITER_SKILL_RESOURCE,
    SkillResourceSpec {
        uri: "sirno://skills/sirno-config-writer",
        name: "sirno-config-writer",
        title: "Sirno Config Writer",
        description: "Full Sirno config-writer skill text from the bundled lake artifact.",
        content: include_str!("../sirno-docs/.artifacts/config-writing-discipline/SKILL.full.md"),
    },
    SkillResourceSpec {
        uri: "sirno://skills/sirno-editor",
        name: "sirno-editor",
        title: "Sirno Editor",
        description: "Full Sirno editor skill text from the bundled lake artifact.",
        content: include_str!("../sirno-docs/.artifacts/lake-editing-discipline/SKILL.full.md"),
    },
    SkillResourceSpec {
        uri: "sirno://skills/sirno-explorer",
        name: "sirno-explorer",
        title: "Sirno Explorer",
        description: "Full Sirno explorer skill text from the bundled lake artifact.",
        content: include_str!("../sirno-docs/.artifacts/lake-exploration-discipline/SKILL.full.md"),
    },
    SkillResourceSpec {
        uri: "sirno://skills/sirno-narrative-session",
        name: "sirno-narrative-session",
        title: "Sirno Narrative Session",
        description: "Full Sirno narrative-session skill text from the bundled lake artifact.",
        content: include_str!(
            "../sirno-docs/.artifacts/narrative-session-discipline/SKILL.full.md"
        ),
    },
    SkillResourceSpec {
        uri: "sirno://skills/sirno-skill-synthesizer",
        name: "sirno-skill-synthesizer",
        title: "Sirno Skill Synthesizer",
        description: "Full Sirno skill-synthesizer text from the bundled lake artifact.",
        content: include_str!("../sirno-docs/.artifacts/skill-synthesis-discipline/SKILL.full.md"),
    },
    SkillResourceSpec {
        uri: "sirno://skills/sirno-witness",
        name: "sirno-witness",
        title: "Sirno Witness",
        description: "Full Sirno witness skill text from the bundled lake artifact.",
        content: include_str!("../sirno-docs/.artifacts/witness-linking-discipline/SKILL.full.md"),
    },
];
// sirno:witness:agent-skills:end

#[derive(Clone, Copy, Debug)]
struct SkillResourceSpec {
    uri: &'static str,
    name: &'static str,
    title: &'static str,
    description: &'static str,
    content: &'static str,
}

impl SkillResourceSpec {
    fn for_uri(uri: &str) -> Option<&'static Self> {
        SKILL_RESOURCES.iter().find(|resource| resource.uri == uri)
    }

    fn as_resource(&self) -> Resource {
        Annotated::new(
            RawResource::new(self.uri, self.name)
                .with_title(self.title)
                .with_description(self.description)
                .with_mime_type(SKILL_RESOURCE_MIME_TYPE),
            None,
        )
    }

    fn as_resource_contents(&self) -> ResourceContents {
        ResourceContents::text(self.content, self.uri).with_mime_type(SKILL_RESOURCE_MIME_TYPE)
    }
}

/// Sirno MCP server for one config path.
///
/// Relative config paths are resolved when tools read them,
/// so changing the process current working directory can change the active project.
#[derive(Clone, Debug)]
pub struct SirnoMcpServer {
    context: CoreContext,
    tool_router: ToolRouter<Self>,
}

impl SirnoMcpServer {
    /// Create an MCP server around one core command context.
    pub fn new(context: CoreContext) -> Self {
        Self { context, tool_router: Self::tool_router() }
    }
}

/// Run one Sirno MCP server on stdio until the client closes the transport.
pub async fn run_stdio(context: CoreContext) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
    let service = SirnoMcpServer::new(context).serve(rmcp::transport::stdio()).await?;
    service.waiting().await?;
    Ok(())
}

#[tool_handler(router = self.tool_router)]
impl ServerHandler for SirnoMcpServer {
    fn get_info(&self) -> ServerInfo {
        ServerInfo::new(ServerCapabilities::builder().enable_resources().enable_tools().build())
            .with_instructions("Sirno tools for the project resolved by the active config path.")
    }

    // sirno:witness:interfaces:begin
    fn list_resources(
        &self, _request: Option<PaginatedRequestParams>, _context: RequestContext<RoleServer>,
    ) -> impl Future<Output = Result<ListResourcesResult, McpError>> + MaybeSendFuture + '_ {
        let resources =
            SKILL_RESOURCES.iter().map(SkillResourceSpec::as_resource).collect::<Vec<_>>();
        future::ready(Ok(ListResourcesResult::with_all_items(resources)))
    }

    fn read_resource(
        &self, request: ReadResourceRequestParams, _context: RequestContext<RoleServer>,
    ) -> impl Future<Output = Result<ReadResourceResult, McpError>> + MaybeSendFuture + '_ {
        let result = SkillResourceSpec::for_uri(&request.uri)
            .map(|resource| ReadResourceResult::new(vec![resource.as_resource_contents()]))
            .ok_or_else(|| {
                McpError::resource_not_found(
                    format!("skill resource not found: {}", request.uri),
                    None,
                )
            });
        future::ready(result)
    }
    // sirno:witness:interfaces:end
}

#[tool_router(router = tool_router)]
impl SirnoMcpServer {
    /// Read or change the server process current working directory.
    #[tool(name = "sirno_cwd")]
    fn cwd(&self, Parameters(params): Parameters<CwdParams>) -> McpToolResult {
        result(self.context.cwd(params.path))
    }

    /// Create one Markdown entry.
    #[tool(name = "sirno_entry_new")]
    fn entry_new(&self, Parameters(params): Parameters<EntryNewParams>) -> McpToolResult {
        let request = EntryNewRequest {
            id: entry_id(params.id)?,
            name: params.name,
            desc: params.desc,
            structural: params.structural.into_targets()?,
            body: params.body,
        };
        result(self.context.entry_new(request))
    }

    /// Rename one entry id and its Sirno references.
    #[tool(name = "sirno_entry_rename")]
    fn entry_rename(&self, Parameters(params): Parameters<EntryRenameParams>) -> McpToolResult {
        result(self.context.entry_rename(entry_id(params.old_id)?, entry_id(params.new_id)?))
    }

    /// Freeze one current Frost entry and make its public file read-only.
    #[tool(name = "sirno_entry_freeze")]
    fn entry_freeze(&self, Parameters(params): Parameters<EntryIdParams>) -> McpToolResult {
        result(self.context.entry_freeze(entry_id(params.id)?))
    }

    /// Melt one public Markdown entry and make its file writable.
    #[tool(name = "sirno_entry_melt")]
    fn entry_melt(&self, Parameters(params): Parameters<EntryIdParams>) -> McpToolResult {
        result(self.context.entry_melt(entry_id(params.id)?))
    }

    /// Show filesystem paths related to one entry.
    #[tool(name = "sirno_entry_path")]
    fn entry_path(&self, Parameters(params): Parameters<EntryPathParams>) -> McpToolResult {
        let selection = path_selection(params.entry, params.artifact, params.frost);
        let request = EntryPathRequest::new(
            entry_id(params.id)?,
            selection,
            params.absolute.unwrap_or(false),
        );
        result(self.context.entry_paths(request))
    }

    /// Query public Markdown entries.
    #[tool(name = "sirno_entry_query")]
    fn entry_query(&self, Parameters(params): Parameters<EntryQueryParams>) -> McpToolResult {
        let request = QueryRequest {
            terms: params.terms,
            exact_terms: params.exact_terms,
            has: params.has.into_filters()?,
            is: params.is.into_states()?,
            columns: query_columns(params.columns)?,
        };
        result(self.context.entry_query(request))
    }

    /// Run ripgrep in the configured public Markdown lake.
    #[tool(name = "sirno_entry_rg")]
    fn entry_rg(&self, Parameters(params): Parameters<EntryRgParams>) -> McpToolResult {
        result(self.context.entry_rg(RgRequest {
            with_generated_footer: params.with_generated_footer,
            args: params.args,
        }))
    }

    /// Return repository witness blocks for one entry.
    #[tool(name = "sirno_entry_witness")]
    fn entry_witness(&self, Parameters(params): Parameters<EntryWitnessParams>) -> McpToolResult {
        result(self.context.entry_witness(entry_id(params.id)?, params.verbose))
    }

    /// List artifacts owned by one entry.
    #[tool(name = "sirno_entry_artifact_list")]
    fn entry_artifact_list(&self, Parameters(params): Parameters<EntryIdParams>) -> McpToolResult {
        result(self.context.entry_artifact_list(entry_id(params.id)?))
    }

    /// Copy a file into one entry's artifact tree.
    #[tool(name = "sirno_entry_artifact_add")]
    fn entry_artifact_add(
        &self, Parameters(params): Parameters<ArtifactAddParams>,
    ) -> McpToolResult {
        result(self.context.entry_artifact_add(ArtifactAddRequest {
            id: entry_id(params.id)?,
            source: params.source,
            artifact_path: params.artifact_path,
        }))
    }

    /// Rename one artifact path owned by an entry.
    #[tool(name = "sirno_entry_artifact_rename")]
    fn entry_artifact_rename(
        &self, Parameters(params): Parameters<ArtifactRenameParams>,
    ) -> McpToolResult {
        result(self.context.entry_artifact_rename(ArtifactRenameRequest {
            id: entry_id(params.id)?,
            old_path: params.old_path,
            new_path: params.new_path,
        }))
    }

    /// Remove one artifact owned by an entry.
    #[tool(name = "sirno_entry_artifact_remove")]
    fn entry_artifact_remove(
        &self, Parameters(params): Parameters<ArtifactRemoveParams>,
    ) -> McpToolResult {
        result(self.context.entry_artifact_remove(ArtifactRemoveRequest {
            id: entry_id(params.id)?,
            artifact_path: params.artifact_path,
        }))
    }

    /// Create a Sirno config and ordinary seed entries.
    #[tool(name = "sirno_lake_init")]
    fn lake_init(&self, Parameters(params): Parameters<LakeInitParams>) -> McpToolResult {
        result(self.context.lake_init(LakeInitRequest { lake: params.lake }))
    }

    /// Move the configured public Markdown entry lake.
    #[tool(name = "sirno_lake_move")]
    fn lake_move(&self, Parameters(params): Parameters<LakeMoveParams>) -> McpToolResult {
        result(self.context.lake_move(params.lake))
    }

    /// Check current entry structure.
    #[tool(name = "sirno_lake_check")]
    fn lake_check(&self, Parameters(params): Parameters<LakeCheckParams>) -> McpToolResult {
        result(self.context.lake_check(params.mode.unwrap_or(McpCheckMode::Review).into()))
    }

    /// Render Markdown links in entry footers.
    #[tool(name = "sirno_lake_render")]
    fn lake_render(&self, Parameters(params): Parameters<LakeRenderParams>) -> McpToolResult {
        result(self.context.lake_render(params.dry))
    }

    /// Delete generated Markdown link footers.
    #[tool(name = "sirno_lake_render_delete")]
    fn lake_render_delete(&self) -> McpToolResult {
        result(self.context.lake_render_delete())
    }

    /// Show the current Sirno project status.
    #[tool(name = "sirno_lake_status")]
    fn lake_status(&self) -> McpToolResult {
        result(self.context.lake_status())
    }

    /// Configure Sirno Frost.
    #[tool(name = "sirno_frost_init")]
    fn frost_init(&self, Parameters(params): Parameters<FrostInitParams>) -> McpToolResult {
        result(self.context.frost_init(params.frost))
    }

    /// Move the configured Sirno Frost path.
    #[tool(name = "sirno_frost_move")]
    fn frost_move(&self, Parameters(params): Parameters<FrostMoveParams>) -> McpToolResult {
        result(self.context.frost_move(params.frost))
    }

    /// Freeze the current public Markdown lake.
    #[tool(name = "sirno_frost_commit")]
    fn frost_commit(&self, Parameters(params): Parameters<FrostCommitParams>) -> McpToolResult {
        result(self.context.frost_commit(params.unsafe_resolve_all))
    }

    /// Check out Frost entries into the public Markdown lake.
    #[tool(name = "sirno_frost_checkout")]
    fn frost_checkout(&self, Parameters(params): Parameters<FrostCheckoutParams>) -> McpToolResult {
        result(self.context.frost_checkout(FrostCheckoutRequest {
            version: params.version,
            latest: params.latest,
            unsafe_mutable: params.unsafe_mutable,
        }))
    }

    /// Check out the latest Frost version as the mutable current lake.
    #[tool(name = "sirno_frost_defrost")]
    fn frost_defrost(&self) -> McpToolResult {
        result(self.context.frost_defrost())
    }

    /// Show tide review status.
    #[tool(name = "sirno_tide_status")]
    fn tide_status(&self, Parameters(params): Parameters<TideStatusParams>) -> McpToolResult {
        result(self.context.tide_status(params.show.into()))
    }

    /// Resolve tide workitems.
    #[tool(name = "sirno_tide_resolve")]
    fn tide_resolve(&self, Parameters(params): Parameters<TideResolveParams>) -> McpToolResult {
        result(self.context.tide_resolve(params.into_request()?))
    }

    /// Remove resolved marks from tide workitems.
    #[tool(name = "sirno_tide_unresolve")]
    fn tide_unresolve(&self, Parameters(params): Parameters<TideSelectionParams>) -> McpToolResult {
        result(self.context.tide_unresolve(params.into_request()?))
    }

    /// Clear all tide resolutions from the lock.
    #[tool(name = "sirno_tide_reset")]
    fn tide_reset(&self) -> McpToolResult {
        result(self.context.tide_reset())
    }
}

type McpToolResult = Result<CallToolResult, String>;

fn result<T: Serialize>(result: Result<T, impl ToString>) -> McpToolResult {
    result.map_err(|error| error.to_string()).and_then(structured_result)
}

fn structured_result<T: Serialize>(value: T) -> McpToolResult {
    let structured = serde_json::to_value(&value).map_err(|error| error.to_string())?;
    let text = serde_json::to_string_pretty(&value).map_err(|error| error.to_string())?;
    let mut result = CallToolResult::structured(structured);
    result.content = vec![Content::text(text)];
    Ok(result)
}

fn entry_id(raw: String) -> Result<EntryId, String> {
    EntryId::new(raw).map_err(|error| error.to_string())
}

fn path_selection(
    entry: Option<bool>, artifact: Option<bool>, frost: Option<bool>,
) -> PathSelection {
    let entry = entry.unwrap_or(false);
    let artifact = artifact.unwrap_or(false);
    let frost = frost.unwrap_or(false);
    if !entry && !artifact && !frost {
        PathSelection::all()
    } else {
        PathSelection::new(entry, artifact, frost)
    }
}

fn query_columns(columns: Option<Vec<String>>) -> Result<QueryColumns, String> {
    let Some(columns) = columns else {
        return Ok(QueryColumns::default());
    };
    let columns = columns
        .into_iter()
        .map(|column| QueryColumn::from_str(&column).map_err(|error| error.to_string()))
        .collect::<Result<Vec<_>, _>>()?;
    Ok(QueryColumns::new(columns))
}

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
struct CwdParams {
    path: Option<PathBuf>,
}

#[derive(Clone, Debug, Deserialize, JsonSchema)]
struct EntryIdParams {
    id: String,
}

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
struct McpStructuralTargets(Vec<McpStructuralTarget>);

impl McpStructuralTargets {
    fn into_targets(self) -> Result<Vec<StructuralTarget>, String> {
        self.0
            .into_iter()
            .map(|target| {
                Ok(StructuralTarget { field: target.field, target: entry_id(target.target)? })
            })
            .collect()
    }
}

#[derive(Clone, Debug, Deserialize, JsonSchema)]
struct McpStructuralTarget {
    field: String,
    target: String,
}

#[derive(Clone, Debug, Deserialize, JsonSchema)]
struct EntryNewParams {
    id: String,
    name: Option<String>,
    desc: String,
    #[serde(default)]
    structural: McpStructuralTargets,
    body: Option<String>,
}

#[derive(Clone, Debug, Deserialize, JsonSchema)]
struct EntryRenameParams {
    old_id: String,
    new_id: String,
}

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
struct EntryPathParams {
    id: String,
    entry: Option<bool>,
    artifact: Option<bool>,
    frost: Option<bool>,
    absolute: Option<bool>,
}

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
struct McpStructuralFilters(Vec<McpStructuralFilter>);

impl McpStructuralFilters {
    fn into_filters(self) -> Result<Vec<StructuralFilter>, String> {
        self.0
            .into_iter()
            .map(|filter| {
                Ok(StructuralFilter {
                    field: filter.field,
                    targets: filter
                        .targets
                        .into_iter()
                        .map(entry_id)
                        .collect::<Result<Vec<_>, _>>()?,
                })
            })
            .collect()
    }
}

#[derive(Clone, Debug, Deserialize, JsonSchema)]
struct McpStructuralFilter {
    field: String,
    targets: Vec<String>,
}

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
struct McpStructuralStates(Vec<McpStructuralState>);

impl McpStructuralStates {
    fn into_states(self) -> Result<Vec<StructuralStateFilter>, String> {
        self.0
            .into_iter()
            .map(|state| {
                Ok(StructuralStateFilter { field: state.field, state: state.state.into() })
            })
            .collect()
    }
}

#[derive(Clone, Debug, Deserialize, JsonSchema)]
struct McpStructuralState {
    field: String,
    state: McpStructuralFieldState,
}

#[derive(Clone, Copy, Debug, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
enum McpStructuralFieldState {
    Present,
    Empty,
    Missing,
}

impl From<McpStructuralFieldState> for StructuralFieldState {
    fn from(value: McpStructuralFieldState) -> Self {
        match value {
            | McpStructuralFieldState::Present => Self::Present,
            | McpStructuralFieldState::Empty => Self::Empty,
            | McpStructuralFieldState::Missing => Self::Missing,
        }
    }
}

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
struct EntryQueryParams {
    #[serde(default)]
    terms: Vec<String>,
    #[serde(default)]
    exact_terms: Vec<String>,
    #[serde(default)]
    has: McpStructuralFilters,
    #[serde(default)]
    is: McpStructuralStates,
    columns: Option<Vec<String>>,
}

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
struct EntryRgParams {
    args: Vec<String>,
    #[serde(default)]
    with_generated_footer: bool,
}

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
struct EntryWitnessParams {
    id: String,
    /// Include opening and closing delimiter spans.
    #[serde(default)]
    verbose: bool,
}

#[derive(Clone, Debug, Deserialize, JsonSchema)]
struct ArtifactAddParams {
    id: String,
    source: PathBuf,
    artifact_path: Option<PathBuf>,
}

#[derive(Clone, Debug, Deserialize, JsonSchema)]
struct ArtifactRenameParams {
    id: String,
    old_path: PathBuf,
    new_path: PathBuf,
}

#[derive(Clone, Debug, Deserialize, JsonSchema)]
struct ArtifactRemoveParams {
    id: String,
    artifact_path: PathBuf,
}

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
struct LakeInitParams {
    lake: Option<PathBuf>,
}

#[derive(Clone, Debug, Deserialize, JsonSchema)]
struct LakeMoveParams {
    lake: PathBuf,
}

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
struct LakeCheckParams {
    mode: Option<McpCheckMode>,
}

#[derive(Clone, Copy, Debug, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
enum McpCheckMode {
    Edit,
    Review,
}

impl From<McpCheckMode> for CheckMode {
    fn from(value: McpCheckMode) -> Self {
        match value {
            | McpCheckMode::Edit => Self::Edit,
            | McpCheckMode::Review => Self::Review,
        }
    }
}

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
struct LakeRenderParams {
    #[serde(default)]
    dry: bool,
}

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
struct FrostInitParams {
    frost: Option<PathBuf>,
}

#[derive(Clone, Debug, Deserialize, JsonSchema)]
struct FrostMoveParams {
    frost: PathBuf,
}

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
struct FrostCommitParams {
    #[serde(default)]
    unsafe_resolve_all: bool,
}

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
struct FrostCheckoutParams {
    version: Option<u64>,
    #[serde(default)]
    latest: bool,
    #[serde(default)]
    unsafe_mutable: bool,
}

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
struct TideStatusParams {
    /// Select review entries, full open workitems, or all workitems.
    #[serde(default)]
    show: McpTideStatusMode,
}

#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
enum McpTideStatusMode {
    /// Return only entry ids that need review.
    #[default]
    Review,
    /// Include full open workitem statuses.
    Full,
    /// Include full open and resolved workitem statuses.
    All,
}

impl From<McpTideStatusMode> for TideStatusMode {
    fn from(value: McpTideStatusMode) -> Self {
        match value {
            | McpTideStatusMode::Review => Self::Review,
            | McpTideStatusMode::Full => Self::Full,
            | McpTideStatusMode::All => Self::All,
        }
    }
}

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
struct TideSelectionParams {
    #[serde(default)]
    neighbors: Vec<String>,
    #[serde(default)]
    workitems: Vec<McpTideWorkitem>,
}

impl TideSelectionParams {
    fn into_request(self) -> Result<TideSelectionRequest, String> {
        Ok(TideSelectionRequest {
            neighbors: self.neighbors.into_iter().map(entry_id).collect::<Result<Vec<_>, _>>()?,
            workitems: self
                .workitems
                .into_iter()
                .map(TideWorkitem::try_from)
                .collect::<Result<Vec<_>, _>>()?,
        })
    }
}

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
struct TideResolveParams {
    #[serde(default)]
    infer: bool,
    #[serde(default)]
    neighbors: Vec<String>,
    #[serde(default)]
    workitems: Vec<McpTideWorkitem>,
}

impl TideResolveParams {
    fn into_request(self) -> Result<TideResolveRequest, String> {
        Ok(TideResolveRequest {
            infer: self.infer,
            neighbors: self.neighbors.into_iter().map(entry_id).collect::<Result<Vec<_>, _>>()?,
            workitems: self
                .workitems
                .into_iter()
                .map(TideWorkitem::try_from)
                .collect::<Result<Vec<_>, _>>()?,
        })
    }
}

#[derive(Clone, Debug, Deserialize, JsonSchema)]
struct McpTideWorkitem {
    ripple: String,
    field: String,
    direction: String,
    neighbor: String,
}

impl TryFrom<McpTideWorkitem> for TideWorkitem {
    type Error = String;

    fn try_from(value: McpTideWorkitem) -> Result<Self, Self::Error> {
        TideWorkitem::new(
            entry_id(value.ripple)?,
            value.field,
            StructuralEdgeDirection::from_str(&value.direction)
                .map_err(|error| error.to_string())?,
            entry_id(value.neighbor)?,
        )
        .map_err(|error| error.to_string())
    }
}

#[cfg(test)]
mod tests {
    use std::fs;
    use std::path::{Path, PathBuf};

    use rmcp::model::{CallToolRequestParams, ClientInfo};
    use rmcp::{ClientHandler, ServiceExt};
    use serde_json::json;

    use super::*;
    use crate::{
        CONFIG_FILE_NAME, RepoMember, RepoSettings, SirnoConfig, StructuralEdgeSettings,
        StructuralFieldSettings, StructuralRippleSettings, StructuralSettings,
    };

    // sirno:witness:interfaces:begin
    const EXPECTED_TOOLS: &[&str] = &[
        "sirno_cwd",
        "sirno_entry_artifact_add",
        "sirno_entry_artifact_list",
        "sirno_entry_artifact_remove",
        "sirno_entry_artifact_rename",
        "sirno_entry_freeze",
        "sirno_entry_melt",
        "sirno_entry_new",
        "sirno_entry_path",
        "sirno_entry_query",
        "sirno_entry_rename",
        "sirno_entry_rg",
        "sirno_entry_witness",
        "sirno_frost_checkout",
        "sirno_frost_commit",
        "sirno_frost_defrost",
        "sirno_frost_init",
        "sirno_frost_move",
        "sirno_lake_check",
        "sirno_lake_init",
        "sirno_lake_move",
        "sirno_lake_render",
        "sirno_lake_render_delete",
        "sirno_lake_status",
        "sirno_tide_reset",
        "sirno_tide_resolve",
        "sirno_tide_status",
        "sirno_tide_unresolve",
    ];
    // sirno:witness:interfaces:end

    fn write_project(root: &Path) -> PathBuf {
        let config_path = root.join(CONFIG_FILE_NAME);
        let docs = root.join("docs");
        SirnoConfig::new("docs").write_new(&config_path).unwrap();
        fs::create_dir(&docs).unwrap();
        fs::write(
            docs.join("alpha.md"),
            "\
---
name: Alpha
desc: Alpha entry.
---

Body.
",
        )
        .unwrap();
        config_path
    }

    fn write_witness_project(root: &Path) -> PathBuf {
        let config_path = root.join(CONFIG_FILE_NAME);
        let docs = root.join("docs");
        let src = root.join("src");
        SirnoConfig {
            repo: Some(RepoSettings { members: vec![RepoMember::new("src").unwrap()] }),
            ..SirnoConfig::new("docs")
        }
        .write_new(&config_path)
        .unwrap();
        fs::create_dir(&docs).unwrap();
        fs::create_dir(&src).unwrap();
        fs::write(
            docs.join("alpha.md"),
            "\
---
name: Alpha
desc: Alpha entry.
---

Body.
",
        )
        .unwrap();
        let witness_source = format!(
            "{}{}{}\n{}\n{}{}{}\n",
            "// sirno",
            ":witness:",
            "alpha:begin",
            "pub fn alpha() {}",
            "// sirno",
            ":witness:",
            "alpha:end"
        );
        fs::write(src.join("lib.rs"), witness_source).unwrap();
        config_path
    }

    fn write_open_tide_project(root: &Path) -> PathBuf {
        let config_path = root.join(CONFIG_FILE_NAME);
        let docs = root.join("docs");
        let config = SirnoConfig {
            structural: StructuralSettings::from_fields([(
                "belongs",
                StructuralFieldSettings::new(
                    StructuralEdgeSettings::new(false, StructuralRippleSettings::new(true, false)),
                    StructuralEdgeSettings::default(),
                    StructuralEdgeSettings::default(),
                ),
            )]),
            ..SirnoConfig::new("docs").with_frost("sirno-frost")
        };
        config.write_new(&config_path).unwrap();
        fs::create_dir(&docs).unwrap();
        fs::write(
            docs.join("alpha.md"),
            "\
---
name: Alpha
desc: Alpha entry.
belongs:
  - beta
---

Body.
",
        )
        .unwrap();
        fs::write(
            docs.join("beta.md"),
            "\
---
name: Beta
desc: Beta entry.
---

Body.
",
        )
        .unwrap();
        CoreContext::new(&config_path).frost_commit(true).unwrap();
        fs::write(
            docs.join("alpha.md"),
            "\
---
name: Alpha
desc: Alpha entry.
belongs:
  - beta
---

Changed body.
",
        )
        .unwrap();
        config_path
    }

    fn structured(result: &CallToolResult) -> &serde_json::Value {
        result.structured_content.as_ref().expect("tool result has structured content")
    }

    #[test]
    fn tool_router_exposes_grouped_tool_surface() {
        let server = SirnoMcpServer::new(CoreContext::new("Sirno.toml"));
        let names = server
            .tool_router
            .list_all()
            .into_iter()
            .map(|tool| tool.name.to_string())
            .collect::<Vec<_>>();

        assert_eq!(names, EXPECTED_TOOLS);
    }

    #[test]
    fn direct_tool_call_returns_structured_content_and_pretty_text() {
        let temp = tempfile::tempdir().unwrap();
        let config_path = temp.path().join(CONFIG_FILE_NAME);
        let server = SirnoMcpServer::new(CoreContext::new(&config_path));

        let cwd = server.cwd(Parameters(CwdParams::default())).unwrap();
        let init = server
            .lake_init(Parameters(LakeInitParams { lake: Some(PathBuf::from("docs")) }))
            .unwrap();
        let entry = server
            .entry_new(Parameters(EntryNewParams {
                id: "alpha".to_owned(),
                name: None,
                desc: "Alpha entry.".to_owned(),
                structural: McpStructuralTargets::default(),
                body: Some("Body.".to_owned()),
            }))
            .unwrap();
        let text = entry
            .content
            .first()
            .and_then(|content| content.as_text())
            .map(|text| text.text.as_str())
            .unwrap();

        assert_eq!(structured(&cwd)["ok"], true);
        assert_eq!(structured(&cwd)["changed"], false);
        assert!(structured(&cwd)["path"].as_str().is_some_and(|path| !path.is_empty()));
        assert_eq!(structured(&init)["ok"], true);
        assert_eq!(structured(&entry)["id"], "alpha");
        assert!(text.contains("\n  \"ok\": true,"));
    }

    #[test]
    fn tide_status_defaults_to_review_entries() {
        let temp = tempfile::tempdir().unwrap();
        let config_path = write_open_tide_project(temp.path());
        let server = SirnoMcpServer::new(CoreContext::new(config_path));

        let summary = server.tide_status(Parameters(TideStatusParams::default())).unwrap();
        let full = server
            .tide_status(Parameters(TideStatusParams { show: McpTideStatusMode::Full }))
            .unwrap();
        let all = server
            .tide_status(Parameters(TideStatusParams { show: McpTideStatusMode::All }))
            .unwrap();

        assert_eq!(structured(&summary)["ok"], false);
        assert_eq!(structured(&summary)["review_entries"], json!(["beta"]));
        assert!(structured(&summary).get("statuses").is_none());
        assert_eq!(structured(&full)["review_entries"], json!(["beta"]));
        assert_eq!(structured(&full)["statuses"][0]["workitem"]["neighbor"], "beta");
        assert_eq!(structured(&all)["statuses"][0]["workitem"]["neighbor"], "beta");
    }

    #[test]
    fn entry_witness_defaults_to_body_and_hides_delimiter_spans() {
        let temp = tempfile::tempdir().unwrap();
        let config_path = write_witness_project(temp.path());
        let server = SirnoMcpServer::new(CoreContext::new(config_path));

        let result = server
            .entry_witness(Parameters(EntryWitnessParams {
                id: "alpha".to_owned(),
                verbose: false,
            }))
            .unwrap();
        let record = &structured(&result)["records"][0];

        assert_eq!(record["region"]["start_line"], json!(1));
        assert!(record["body"].as_str().unwrap().contains("pub fn alpha() {}"));
        assert!(record.get("opening").is_none());
        assert!(record.get("closing").is_none());
        assert!(record.get("marker").is_none());
    }

    #[test]
    fn entry_witness_verbose_includes_delimiter_spans() {
        let temp = tempfile::tempdir().unwrap();
        let config_path = write_witness_project(temp.path());
        let server = SirnoMcpServer::new(CoreContext::new(config_path));

        let result = server
            .entry_witness(Parameters(EntryWitnessParams { id: "alpha".to_owned(), verbose: true }))
            .unwrap();
        let record = &structured(&result)["records"][0];

        assert_eq!(record["opening"]["start_line"], json!(1));
        assert_eq!(record["closing"]["start_line"], json!(3));
        assert!(record["body"].as_str().unwrap().contains("pub fn alpha() {}"));
        assert!(record.get("marker").is_none());
    }

    #[derive(Clone, Debug, Default)]
    struct DummyClient;

    impl ClientHandler for DummyClient {
        fn get_info(&self) -> ClientInfo {
            ClientInfo::default()
        }
    }

    #[tokio::test]
    async fn stdio_smoke_lists_tools_and_calls_lake_status() {
        let temp = tempfile::tempdir().unwrap();
        let config_path = write_project(temp.path());
        let server = SirnoMcpServer::new(CoreContext::new(config_path));
        let (server_transport, client_transport) = tokio::io::duplex(8192);

        let server_handle = tokio::spawn(async move {
            server.serve(server_transport).await.unwrap().waiting().await.unwrap();
        });
        let client = DummyClient.serve(client_transport).await.unwrap();

        let tools = client.peer().list_tools(None).await.unwrap();
        assert_eq!(tools.tools.len(), EXPECTED_TOOLS.len());
        assert!(tools.tools.iter().any(|tool| tool.name == "sirno_lake_status"));

        let resources = client.peer().list_resources(None).await.unwrap();
        assert_eq!(resources.resources.len(), SKILL_RESOURCES.len());
        assert!(resources.resources.iter().any(|resource| {
            resource.uri == "sirno://skills/sirno-editor"
                && resource.mime_type.as_deref() == Some(SKILL_RESOURCE_MIME_TYPE)
        }));
        assert!(resources.resources.iter().any(|resource| {
            resource.uri == "sirno://skills/sirno-config-writer"
                && resource.mime_type.as_deref() == Some(SKILL_RESOURCE_MIME_TYPE)
        }));
        assert!(resources.resources.iter().any(|resource| {
            resource.uri == "sirno://skills/design-doc-writer"
                && resource.mime_type.as_deref() == Some(SKILL_RESOURCE_MIME_TYPE)
        }));

        let skill = client
            .peer()
            .read_resource(ReadResourceRequestParams::new("sirno://skills/sirno-editor"))
            .await
            .unwrap();
        let Some(ResourceContents::TextResourceContents { text, mime_type, .. }) =
            skill.contents.first()
        else {
            panic!("expected text skill resource");
        };
        assert_eq!(mime_type.as_deref(), Some(SKILL_RESOURCE_MIME_TYPE));
        assert!(text.contains("# Sirno Editor"));
        assert!(text.contains("## Core Judgment"));

        let config_skill = client
            .peer()
            .read_resource(ReadResourceRequestParams::new("sirno://skills/sirno-config-writer"))
            .await
            .unwrap();
        let Some(ResourceContents::TextResourceContents { text, .. }) =
            config_skill.contents.first()
        else {
            panic!("expected text config-writer resource");
        };
        assert!(text.contains("# Sirno Config Writer"));
        assert!(text.contains("## Required Tables"));

        let design_skill = client
            .peer()
            .read_resource(ReadResourceRequestParams::new("sirno://skills/design-doc-writer"))
            .await
            .unwrap();
        let Some(ResourceContents::TextResourceContents { text, .. }) =
            design_skill.contents.first()
        else {
            panic!("expected text design-doc-writer resource");
        };
        assert!(text.contains("# Design Doc Writer"));
        assert!(text.contains("## Reader Evaluation"));

        let result = client
            .peer()
            .call_tool(
                CallToolRequestParams::new("sirno_lake_status")
                    .with_arguments(json!({}).as_object().unwrap().clone()),
            )
            .await
            .unwrap();

        assert_eq!(result.structured_content.as_ref().unwrap()["ok"], true);
        assert_eq!(result.structured_content.as_ref().unwrap()["entry_count"], 1);

        let cwd = client
            .peer()
            .call_tool(
                CallToolRequestParams::new("sirno_cwd")
                    .with_arguments(json!({}).as_object().unwrap().clone()),
            )
            .await
            .unwrap();
        assert_eq!(cwd.structured_content.as_ref().unwrap()["ok"], true);
        assert_eq!(cwd.structured_content.as_ref().unwrap()["changed"], false);

        client.cancel().await.unwrap();
        server_handle.await.unwrap();
    }
}
