use serde::{Deserialize, Serialize};

use crate::diagnostic::{Category, Diagnostic, Severity};
use crate::error::{Error, Result};

#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct RunLimits {
    pub max_script_bytes: usize,
    #[serde(default = "default_max_args")]
    pub max_args: usize,
    #[serde(default = "default_max_arg_bytes")]
    pub max_arg_bytes: usize,
    #[serde(default = "default_max_output_bytes")]
    pub max_output_bytes: usize,
    #[serde(default = "default_timeout_s")]
    pub timeout_s: u32,
    #[serde(default = "default_max_temp_bytes")]
    pub max_temp_bytes: usize,
    #[serde(default)]
    pub metadata: Option<String>,
}

fn default_max_args() -> usize {
    64
}

fn default_max_arg_bytes() -> usize {
    64 * 1024
}

fn default_timeout_s() -> u32 {
    0
}

fn default_max_temp_bytes() -> usize {
    64 * 1024
}

fn default_max_output_bytes() -> usize {
    64 * 1024
}

impl Default for RunLimits {
    fn default() -> Self {
        Self {
            max_script_bytes: 64 * 1024,
            max_args: default_max_args(),
            max_arg_bytes: default_max_arg_bytes(),
            max_output_bytes: default_max_output_bytes(),
            timeout_s: default_timeout_s(),
            max_temp_bytes: default_max_temp_bytes(),
            metadata: None,
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ScriptSource {
    File { path: String },
    Inline { source: String },
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct RunRequest {
    pub script: ScriptSource,
    pub entrypoint: String,
    #[serde(default)]
    pub args: Vec<String>,
    #[serde(default)]
    pub parameters: std::collections::HashMap<String, String>,
    #[serde(default)]
    pub limits: RunLimits,
}

impl RunRequest {
    pub fn validate(&self) -> Result<()> {
        if self.entrypoint.is_empty() {
            return Err(
                Error::invalid_request("entrypoint must not be empty").with_diagnostic(
                    Diagnostic::new(
                        Category::InvalidRequest,
                        Severity::Error,
                        "entrypoint must not be empty",
                    )
                    .with_code("empty-entrypoint"),
                ),
            );
        }

        if self.entrypoint != self.entrypoint.trim() {
            return Err(Error::invalid_request(format!(
                "entrypoint must not have leading or trailing whitespace: '{}'",
                self.entrypoint
            ))
            .with_diagnostic(
                Diagnostic::new(
                    Category::InvalidRequest,
                    Severity::Error,
                    "entrypoint must not have leading or trailing whitespace",
                )
                .with_code("whitespace-entrypoint"),
            ));
        }

        if self.entrypoint != "main" {
            return Err(Error::invalid_request(format!(
                "entrypoint must be 'main', got '{}'",
                self.entrypoint
            ))
            .with_diagnostic(
                Diagnostic::new(
                    Category::InvalidRequest,
                    Severity::Error,
                    format!("entrypoint must be 'main', got '{}'", self.entrypoint),
                )
                .with_code("invalid-entrypoint"),
            ));
        }

        if self.args.len() > self.limits.max_args {
            return Err(Error::invalid_request(format!(
                "argument count {} exceeds limit {}",
                self.args.len(),
                self.limits.max_args
            ))
            .with_diagnostic(
                Diagnostic::new(
                    Category::InvalidRequest,
                    Severity::Error,
                    format!(
                        "argument count {} exceeds limit {}",
                        self.args.len(),
                        self.limits.max_args
                    ),
                )
                .with_code("too-many-args"),
            ));
        }

        for (i, arg) in self.args.iter().enumerate() {
            if arg.len() > self.limits.max_arg_bytes {
                return Err(Error::invalid_request(format!(
                    "argument {} exceeds max_arg_bytes: {} > {}",
                    i,
                    arg.len(),
                    self.limits.max_arg_bytes
                ))
                .with_diagnostic(
                    Diagnostic::new(
                        Category::InvalidRequest,
                        Severity::Error,
                        format!(
                            "argument {} exceeds max_arg_bytes: {} > {}",
                            i,
                            arg.len(),
                            self.limits.max_arg_bytes
                        ),
                    )
                    .with_code("oversized-argument"),
                ));
            }
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::error::ErrorKind;

    fn default_limits() -> RunLimits {
        RunLimits::default()
    }

    fn inline_request(entrypoint: &str) -> RunRequest {
        RunRequest {
            script: ScriptSource::Inline {
                source: r#"fn main() -> String { "ok" }"#.into(),
            },
            entrypoint: entrypoint.into(),
            args: vec![],
            parameters: std::collections::HashMap::new(),
            limits: default_limits(),
        }
    }

    #[test]
    fn validate_rejects_whitespace_padded_entrypoint() {
        let req = inline_request(" main");
        let err = req.validate().unwrap_err();
        assert_eq!(
            err.diagnostics().first().and_then(|d| d.code.as_deref()),
            Some("whitespace-entrypoint")
        );
        assert_eq!(err.kind(), ErrorKind::InvalidRequest);
    }

    #[test]
    fn validate_rejects_trailing_whitespace_entrypoint() {
        let req = inline_request("main ");
        let err = req.validate().unwrap_err();
        assert_eq!(
            err.diagnostics().first().and_then(|d| d.code.as_deref()),
            Some("whitespace-entrypoint")
        );
        assert_eq!(err.kind(), ErrorKind::InvalidRequest);
    }

    #[test]
    fn validate_accepts_main() {
        let req = inline_request("main");
        assert!(req.validate().is_ok());
    }

    #[test]
    fn validate_rejects_empty_entrypoint() {
        let req = inline_request("");
        let err = req.validate().unwrap_err();
        assert_eq!(
            err.diagnostics().first().and_then(|d| d.code.as_deref()),
            Some("empty-entrypoint")
        );
        assert_eq!(err.kind(), ErrorKind::InvalidRequest);
    }
}
