//! Desktop-owned MCP template tool provider.
//!
//! This module is a driving-adapter bridge: it maps the public MCP
//! `idea_template_*` tools to the existing application template use cases.

use std::sync::Arc;

use application::{
    CreateTemplate, CreateTemplateInput, DeleteTemplate, DeleteTemplateInput, ListTemplates,
    ReadTemplate, ReadTemplateInput, UpdateTemplate, UpdateTemplateInput,
};
use async_trait::async_trait;
use domain::{Project, TemplateId};
use infrastructure::{TemplateToolError, TemplateToolProvider};
use serde_json::{json, Value};

use crate::dto::{parse_profile_id, parse_template_id, TemplateDto, TemplateListDto};

/// Concrete provider for the public MCP template tools.
#[derive(Clone)]
pub struct AppTemplateToolProvider {
    /// Template creation use case.
    pub create: Arc<CreateTemplate>,
    /// Template read use case.
    pub read: Arc<ReadTemplate>,
    /// Template list use case.
    pub list: Arc<ListTemplates>,
    /// Template update use case.
    pub update: Arc<UpdateTemplate>,
    /// Template delete use case.
    pub delete: Arc<DeleteTemplate>,
}

#[async_trait]
impl TemplateToolProvider for AppTemplateToolProvider {
    async fn handle_template_tool(
        &self,
        _project: &Project,
        _requester: &str,
        name: &str,
        arguments: Value,
    ) -> Result<Value, TemplateToolError> {
        let result = match name {
            "idea_template_list" => {
                let templates = self.list.execute().await.map_err(template_error)?;
                json!(TemplateListDto::from(templates))
            }
            "idea_template_read" => {
                let template_id = parse_json_template_id(&arguments)?;
                let template = self
                    .read
                    .execute(ReadTemplateInput { template_id })
                    .await
                    .map_err(template_error)?;
                json!(TemplateDto::from(template))
            }
            "idea_template_create" => {
                let template = self
                    .create
                    .execute(CreateTemplateInput {
                        name: required_str(&arguments, "name")?.to_owned(),
                        content: required_str(&arguments, "content")?.to_owned(),
                        default_profile_id: parse_profile_id(required_str(
                            &arguments,
                            "defaultProfileId",
                        )?)
                        .map_err(dto_tool_error)?,
                    })
                    .await
                    .map_err(template_error)?;
                json!(TemplateDto::from(template))
            }
            "idea_template_update" => {
                let template = self
                    .update
                    .execute(UpdateTemplateInput {
                        template_id: parse_json_template_id(&arguments)?,
                        content: required_str(&arguments, "content")?.to_owned(),
                    })
                    .await
                    .map_err(template_error)?;
                json!(TemplateDto::from(template))
            }
            "idea_template_delete" => {
                let template_id = parse_json_template_id(&arguments)?;
                self.delete
                    .execute(DeleteTemplateInput { template_id })
                    .await
                    .map_err(template_error)?;
                json!({ "deleted": true, "templateId": template_id })
            }
            other => {
                return Err(TemplateToolError::new(
                    "notFound",
                    format!("unknown template tool: {other}"),
                ))
            }
        };
        Ok(result)
    }
}

fn template_error(err: application::AppError) -> TemplateToolError {
    let code = match err.code() {
        "NOT_FOUND" => "notFound",
        "INVALID" => "invalid",
        "STORE" => "store",
        "FILESYSTEM" => "filesystem",
        _ => "internal",
    };
    TemplateToolError::new(code, err.to_string())
}

fn dto_tool_error(err: crate::dto::ErrorDto) -> TemplateToolError {
    let code = match err.code.as_str() {
        "NOT_FOUND" => "notFound",
        "INVALID" => "invalid",
        "STORE" => "store",
        "FILESYSTEM" => "filesystem",
        _ => "internal",
    };
    TemplateToolError::new(code, err.message)
}

fn parse_json_template_id(arguments: &Value) -> Result<TemplateId, TemplateToolError> {
    parse_template_id(required_str(arguments, "templateId")?).map_err(dto_tool_error)
}

fn required_str<'a>(arguments: &'a Value, key: &str) -> Result<&'a str, TemplateToolError> {
    arguments
        .get(key)
        .and_then(Value::as_str)
        .ok_or_else(|| TemplateToolError::new("invalid", format!("missing {key}")))
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;
    use std::sync::Mutex;

    use application::{
        CreateTemplate, DeleteTemplate, ListTemplates, ReadTemplate, UpdateTemplate,
    };
    use domain::ports::{EventBus, EventStream, IdGenerator, StoreError, TemplateStore};
    use domain::{
        AgentTemplate, DomainEvent, MarkdownDoc, ProfileId, ProjectId, ProjectPath, RemoteRef,
        TemplateVersion,
    };
    use uuid::Uuid;

    use super::*;

    #[derive(Default)]
    struct FakeTemplateStore {
        templates: Mutex<HashMap<TemplateId, AgentTemplate>>,
    }

    #[async_trait]
    impl TemplateStore for FakeTemplateStore {
        async fn list(&self) -> Result<Vec<AgentTemplate>, StoreError> {
            Ok(self.templates.lock().unwrap().values().cloned().collect())
        }

        async fn get(&self, id: TemplateId) -> Result<AgentTemplate, StoreError> {
            self.templates
                .lock()
                .unwrap()
                .get(&id)
                .cloned()
                .ok_or(StoreError::NotFound)
        }

        async fn save(&self, template: &AgentTemplate) -> Result<(), StoreError> {
            self.templates
                .lock()
                .unwrap()
                .insert(template.id, template.clone());
            Ok(())
        }

        async fn delete(&self, id: TemplateId) -> Result<(), StoreError> {
            self.templates
                .lock()
                .unwrap()
                .remove(&id)
                .map(|_| ())
                .ok_or(StoreError::NotFound)
        }
    }

    struct FakeIds;

    impl IdGenerator for FakeIds {
        fn new_uuid(&self) -> Uuid {
            Uuid::from_u128(81)
        }
    }

    #[derive(Default)]
    struct FakeEvents {
        events: Mutex<Vec<DomainEvent>>,
    }

    impl EventBus for FakeEvents {
        fn publish(&self, event: DomainEvent) {
            self.events.lock().unwrap().push(event);
        }

        fn subscribe(&self) -> EventStream {
            Box::new(std::iter::empty())
        }
    }

    fn project() -> Project {
        Project::new(
            ProjectId::from_uuid(Uuid::from_u128(1)),
            "demo",
            ProjectPath::new("/tmp/project").unwrap(),
            RemoteRef::local(),
            1_000,
        )
        .unwrap()
    }

    fn provider(store: Arc<FakeTemplateStore>, events: Arc<FakeEvents>) -> AppTemplateToolProvider {
        let store_port = store as Arc<dyn TemplateStore>;
        AppTemplateToolProvider {
            create: Arc::new(CreateTemplate::new(
                Arc::clone(&store_port),
                Arc::new(FakeIds),
            )),
            read: Arc::new(ReadTemplate::new(Arc::clone(&store_port))),
            list: Arc::new(ListTemplates::new(Arc::clone(&store_port))),
            update: Arc::new(UpdateTemplate::new(
                Arc::clone(&store_port),
                events as Arc<dyn EventBus>,
            )),
            delete: Arc::new(DeleteTemplate::new(store_port)),
        }
    }

    #[tokio::test]
    async fn template_provider_crud_round_trips_and_update_publishes_event() {
        let store = Arc::new(FakeTemplateStore::default());
        let events = Arc::new(FakeEvents::default());
        let provider = provider(Arc::clone(&store), Arc::clone(&events));
        let profile_id = ProfileId::from_uuid(Uuid::from_u128(9));

        let created = provider
            .handle_template_tool(
                &project(),
                "agent",
                "idea_template_create",
                json!({
                    "name": "Base",
                    "content": "v1",
                    "defaultProfileId": profile_id,
                }),
            )
            .await
            .unwrap();
        let template_id = created["id"].as_str().unwrap().to_owned();
        assert_eq!(created["version"], json!(TemplateVersion::INITIAL));

        let read = provider
            .handle_template_tool(
                &project(),
                "agent",
                "idea_template_read",
                json!({ "templateId": template_id }),
            )
            .await
            .unwrap();
        assert_eq!(read["name"], "Base");
        assert_eq!(read["contentMd"], "v1");

        let listed = provider
            .handle_template_tool(&project(), "agent", "idea_template_list", json!({}))
            .await
            .unwrap();
        assert_eq!(listed.as_array().unwrap().len(), 1);

        let updated = provider
            .handle_template_tool(
                &project(),
                "agent",
                "idea_template_update",
                json!({ "templateId": template_id, "content": "v2" }),
            )
            .await
            .unwrap();
        assert_eq!(updated["contentMd"], "v2");
        assert_eq!(updated["version"], json!(2));
        assert!(matches!(
            events.events.lock().unwrap().as_slice(),
            [DomainEvent::TemplateUpdated { .. }]
        ));

        let deleted = provider
            .handle_template_tool(
                &project(),
                "agent",
                "idea_template_delete",
                json!({ "templateId": template_id }),
            )
            .await
            .unwrap();
        assert_eq!(deleted["deleted"], true);

        let missing = provider
            .handle_template_tool(
                &project(),
                "agent",
                "idea_template_read",
                json!({ "templateId": template_id }),
            )
            .await
            .expect_err("deleted template must be missing");
        assert_eq!(missing.code, "notFound");
    }

    #[tokio::test]
    async fn template_provider_rejects_invalid_tool_arguments() {
        let provider = provider(
            Arc::new(FakeTemplateStore::default()),
            Arc::new(FakeEvents::default()),
        );

        let err = provider
            .handle_template_tool(
                &project(),
                "agent",
                "idea_template_create",
                json!({ "name": "Base", "content": "v1", "defaultProfileId": "bad" }),
            )
            .await
            .expect_err("bad profile id should be invalid");

        assert_eq!(err.code, "invalid");
    }

    #[test]
    fn template_provider_preserves_content_only_b4_boundary() {
        let template = AgentTemplate::new(
            TemplateId::from_uuid(Uuid::from_u128(7)),
            "Base",
            MarkdownDoc::new("v1"),
            ProfileId::from_uuid(Uuid::from_u128(9)),
        )
        .unwrap();
        let updated = template.with_updated_content(MarkdownDoc::new("v2"));

        assert_eq!(updated.name, "Base");
        assert_eq!(updated.default_profile_id, template.default_profile_id);
        assert_eq!(updated.version, TemplateVersion(2));
    }
}
