use std::collections::HashSet;
use std::sync::Arc;

use crate::cli;
use crate::context::RunCancellation;
use crate::engine::Engine;
use crate::error::{Error, Result};
use crate::grpc::GrpcServer;
use crate::module::{Module, ModuleMetadata, ModuleRef};
use crate::request::{RunLimits, RunRequest};
use crate::result::{AgentMetadata, RunResult};
use crate::service::AgentService;
use crate::version::SDK_VERSION;

pub struct AgentBuilder {
    agent_name: String,
    modules: Vec<ModuleRef>,
}

impl Default for AgentBuilder {
    fn default() -> Self {
        Self::new()
    }
}

impl AgentBuilder {
    pub fn new() -> Self {
        Self {
            agent_name: String::from("sandlet-agent"),
            modules: Vec::new(),
        }
    }

    pub fn name(mut self, agent_name: impl Into<String>) -> Self {
        self.agent_name = agent_name.into();
        self
    }

    pub fn module<M>(mut self, module: M) -> Self
    where
        M: Module + 'static,
    {
        self.modules.push(Arc::new(module));
        self
    }

    pub fn build(self) -> Result<Agent> {
        validate_modules(&self.modules)?;
        Agent::new_validated(self.agent_name, self.modules)
    }
}

pub struct Agent {
    service: Arc<AgentService>,
}

impl Agent {
    pub fn builder() -> AgentBuilder {
        AgentBuilder::new()
    }

    pub(crate) fn new_validated(
        agent_name: impl Into<String>,
        modules: Vec<ModuleRef>,
    ) -> Result<Self> {
        let defaults = RunLimits::default();
        let mut declared_limits = std::collections::HashMap::new();
        declared_limits.insert(
            "max_script_bytes".to_string(),
            defaults.max_script_bytes.to_string(),
        );
        declared_limits.insert("max_args".to_string(), defaults.max_args.to_string());
        declared_limits.insert(
            "max_arg_bytes".to_string(),
            defaults.max_arg_bytes.to_string(),
        );
        declared_limits.insert(
            "max_output_bytes".to_string(),
            defaults.max_output_bytes.to_string(),
        );
        declared_limits.insert("timeout_s".to_string(), defaults.timeout_s.to_string());
        declared_limits.insert(
            "max_temp_bytes".to_string(),
            defaults.max_temp_bytes.to_string(),
        );

        let mut metadata = std::collections::HashMap::new();
        metadata.insert("target_os".to_string(), std::env::consts::OS.to_string());
        metadata.insert(
            "target_arch".to_string(),
            std::env::consts::ARCH.to_string(),
        );

        let metadata = AgentMetadata {
            agent_name: agent_name.into(),
            modules: module_metadata(&modules),
            sdk_version: SDK_VERSION.to_string(),
            declared_limits,
            metadata,
        };
        let engine = Arc::new(Engine::new(modules));
        let service = Arc::new(AgentService::new(engine, metadata));
        Ok(Self { service })
    }

    pub fn metadata(&self) -> &AgentMetadata {
        self.service.describe()
    }

    pub fn run(self) -> Result<()> {
        cli::run(&self)
    }

    pub fn run_once(&self, request: RunRequest) -> Result<RunResult> {
        self.service.run_once(&request)
    }

    pub fn run_once_with_cancellation(
        &self,
        request: RunRequest,
        cancellation: RunCancellation,
    ) -> Result<RunResult> {
        self.service
            .run_once_with_cancellation(&request, cancellation)
    }

    pub fn serve(&self, listen_addr: &str) -> Result<()> {
        GrpcServer::new(Arc::clone(&self.service)).serve(listen_addr)
    }
}

fn module_metadata(modules: &[ModuleRef]) -> Vec<ModuleMetadata> {
    modules.iter().map(|module| module.metadata()).collect()
}

fn validate_modules(modules: &[ModuleRef]) -> Result<()> {
    let mut module_names = HashSet::<String>::new();
    let mut function_names = HashSet::<String>::new();

    for module in modules {
        let spec = module.spec();

        if spec.name.is_empty() {
            return Err(Error::invalid_request("module name must not be empty")
                .with_primary_diagnostic("empty-module-name"));
        }

        if spec.name.contains('_') {
            return Err(Error::invalid_request(format!(
                "module name '{}' must not contain underscores (use a simple group name like 'strings' or 'ip')",
                spec.name
            ))
            .with_primary_diagnostic("underscore-in-module-name"));
        }

        if !module_names.insert(spec.name.clone()) {
            return Err(
                Error::invalid_request(format!("duplicate module name: '{}'", spec.name))
                    .with_primary_diagnostic("duplicate-module-name"),
            );
        }

        for func in &spec.functions {
            if func.name.is_empty() {
                return Err(Error::invalid_request(format!(
                    "module '{}' contains a function with an empty name",
                    spec.name
                ))
                .with_primary_diagnostic("empty-function-name"));
            }

            if !func.name.starts_with(&format!("{}_", spec.name)) {
                return Err(Error::invalid_request(format!(
                    "function '{}' in module '{}' must be prefixed as '{}_<function>'",
                    func.name, spec.name, spec.name
                ))
                .with_primary_diagnostic("function-prefix-mismatch"));
            }

            if !function_names.insert(func.name.clone()) {
                return Err(Error::invalid_request(format!(
                    "duplicate function export name: '{}'",
                    func.name
                ))
                .with_primary_diagnostic("duplicate-function-name"));
            }
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::AgentBuilder;
    use crate::context::ExecutionContext;
    use crate::error::ErrorKind;
    use crate::module::{FunctionModule, FunctionSpec, Module, ModuleMetadata, ModuleSpec};
    use crate::request::RunLimits;
    use roto::Runtime;

    fn make_module(spec: ModuleSpec) -> FunctionModule {
        FunctionModule::new(
            spec,
            move |_runtime: &mut Runtime<roto::Ctx<ExecutionContext>>,
                  _params: &std::collections::HashMap<String, String>| { Ok(()) },
            std::sync::Arc::new(|| Ok(())),
        )
    }

    fn extract_err(
        result: std::result::Result<super::Agent, crate::error::Error>,
    ) -> crate::error::Error {
        match result {
            Err(e) => e,
            Ok(_) => panic!("expected error, got success"),
        }
    }

    #[test]
    fn duplicate_module_names_rejected() {
        let spec = ModuleSpec {
            name: String::from("strings"),
            doc: String::from("test"),
            capabilities: vec![],
            parameters: vec![],
            functions: vec![FunctionSpec::new(
                "strings",
                "uppercase",
                "doc",
                vec![],
                "String",
            )],
        };
        let result = AgentBuilder::new()
            .module(make_module(spec.clone()))
            .module(make_module(spec))
            .build();
        let err = extract_err(result);
        assert_eq!(err.kind(), ErrorKind::InvalidRequest);
        assert!(err.message_str().contains("duplicate module name"));
    }

    #[test]
    fn duplicate_function_names_within_module_rejected() {
        let spec = ModuleSpec {
            name: String::from("a"),
            doc: String::from("module a"),
            capabilities: vec![],
            parameters: vec![],
            functions: vec![
                FunctionSpec {
                    name: String::from("a_foo"),
                    doc: String::from("doc"),
                    args: vec![],
                    return_type: String::from("String"),
                },
                FunctionSpec {
                    name: String::from("a_foo"),
                    doc: String::from("doc2"),
                    args: vec![],
                    return_type: String::from("String"),
                },
            ],
        };
        let result = AgentBuilder::new().module(make_module(spec)).build();
        let err = extract_err(result);
        assert_eq!(err.kind(), ErrorKind::InvalidRequest);
        assert!(err.message_str().contains("duplicate function export name"));
    }

    #[test]
    fn function_name_violating_namespace_rejected() {
        let spec = ModuleSpec {
            name: String::from("echo"),
            doc: String::from("test"),
            capabilities: vec![],
            parameters: vec![],
            functions: vec![FunctionSpec {
                name: String::from("wrong_prefix_func"),
                doc: String::from("doc"),
                args: vec![],
                return_type: String::from("String"),
            }],
        };
        let result = AgentBuilder::new().module(make_module(spec)).build();
        let err = extract_err(result);
        assert_eq!(err.kind(), ErrorKind::InvalidRequest);
        assert!(err.message_str().contains("must be prefixed as"));
    }

    #[test]
    fn empty_module_name_rejected() {
        let spec = ModuleSpec {
            name: String::new(),
            doc: String::from("test"),
            capabilities: vec![],
            parameters: vec![],
            functions: vec![],
        };
        let result = AgentBuilder::new().module(make_module(spec)).build();
        let err = extract_err(result);
        assert_eq!(err.kind(), ErrorKind::InvalidRequest);
        assert!(err.message_str().contains("module name must not be empty"));
    }

    #[test]
    fn module_name_with_underscore_rejected() {
        let spec = ModuleSpec {
            name: String::from("my_module"),
            doc: String::from("test"),
            capabilities: vec![],
            parameters: vec![],
            functions: vec![],
        };
        let result = AgentBuilder::new().module(make_module(spec)).build();
        let err = extract_err(result);
        assert_eq!(err.kind(), ErrorKind::InvalidRequest);
        assert!(err.message_str().contains("must not contain underscores"));
    }

    #[test]
    fn empty_function_name_rejected() {
        let spec = ModuleSpec {
            name: String::from("m"),
            doc: String::from("test"),
            capabilities: vec![],
            parameters: vec![],
            functions: vec![FunctionSpec {
                name: String::new(),
                doc: String::from("doc"),
                args: vec![],
                return_type: String::from("String"),
            }],
        };
        let result = AgentBuilder::new().module(make_module(spec)).build();
        let err = extract_err(result);
        assert_eq!(err.kind(), ErrorKind::InvalidRequest);
        assert!(err.message_str().contains("empty name"));
    }

    #[test]
    fn metadata_matches_module_spec() {
        let spec = ModuleSpec {
            name: String::from("strings"),
            doc: String::from("String utilities."),
            capabilities: vec![],
            parameters: vec![],
            functions: vec![FunctionSpec::new(
                "strings",
                "uppercase",
                "Uppercase a string.",
                vec![String::from("value")],
                "String",
            )],
        };
        let module = make_module(spec.clone());
        let metadata: ModuleMetadata = module.metadata();
        assert_eq!(metadata.name, "strings");
        assert_eq!(metadata.functions.len(), 1);
        assert_eq!(metadata.functions[0].name, "strings_uppercase");
    }

    #[test]
    fn agent_metadata_reflects_registered_modules() {
        let module = crate::builtin::strings::module();
        let agent = AgentBuilder::new()
            .name("test-agent")
            .module(module)
            .build()
            .expect("build succeeds");
        let meta = agent.metadata();
        assert_eq!(meta.agent_name, "test-agent");
        assert_eq!(meta.modules.len(), 1);
        assert_eq!(meta.modules[0].name, "strings");
        assert_eq!(meta.modules[0].functions.len(), 1);
        assert_eq!(meta.modules[0].functions[0].name, "strings_uppercase");
    }

    #[test]
    fn agent_metadata_declared_limits_populated() {
        let agent = AgentBuilder::new()
            .name("limits-agent")
            .build()
            .expect("build succeeds");
        let meta = agent.metadata();
        let defaults = RunLimits::default();
        assert_eq!(
            meta.declared_limits.get("max_script_bytes"),
            Some(&defaults.max_script_bytes.to_string())
        );
        assert_eq!(
            meta.declared_limits.get("max_args"),
            Some(&defaults.max_args.to_string())
        );
        assert_eq!(
            meta.declared_limits.get("max_arg_bytes"),
            Some(&defaults.max_arg_bytes.to_string())
        );
        assert_eq!(
            meta.declared_limits.get("max_output_bytes"),
            Some(&defaults.max_output_bytes.to_string())
        );
        assert_eq!(
            meta.declared_limits.get("timeout_s"),
            Some(&defaults.timeout_s.to_string())
        );
        assert_eq!(
            meta.declared_limits.get("max_temp_bytes"),
            Some(&defaults.max_temp_bytes.to_string())
        );
    }

    #[test]
    fn agent_metadata_includes_runtime_inspection_fields() {
        let agent = AgentBuilder::new()
            .name("meta-agent")
            .build()
            .expect("build succeeds");
        let meta = agent.metadata();
        assert!(
            meta.metadata.contains_key("target_os"),
            "expected target_os in metadata"
        );
        assert!(
            meta.metadata.contains_key("target_arch"),
            "expected target_arch in metadata"
        );
    }

    #[test]
    fn agent_metadata_sdk_version_matches_package() {
        let agent = AgentBuilder::new()
            .name("version-agent")
            .build()
            .expect("build succeeds");
        let meta = agent.metadata();
        assert_eq!(meta.sdk_version, env!("CARGO_PKG_VERSION"));
    }

    #[test]
    fn valid_multi_module_agent_builds() {
        let spec_a = ModuleSpec {
            name: String::from("a"),
            doc: String::from("module a"),
            capabilities: vec![],
            parameters: vec![],
            functions: vec![FunctionSpec::new("a", "foo", "doc", vec![], "String")],
        };
        let spec_b = ModuleSpec {
            name: String::from("b"),
            doc: String::from("module b"),
            capabilities: vec![],
            parameters: vec![],
            functions: vec![FunctionSpec::new("b", "bar", "doc", vec![], "String")],
        };
        let result = AgentBuilder::new()
            .module(make_module(spec_a))
            .module(make_module(spec_b))
            .build();
        assert!(result.is_ok());
    }
}
