use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};

use crate::error::{Error, Result};
use crate::request::{RunLimits, ScriptSource};

static TEMP_DIR_COUNTER: AtomicU64 = AtomicU64::new(0);

#[derive(Debug)]
pub struct LoadedScript {
    pub compile_path: PathBuf,
    temp_dir: Option<PathBuf>,
}

impl LoadedScript {
    pub fn cleanup(mut self) -> Result<()> {
        if let Some(temp_dir) = self.temp_dir.take() {
            fs::remove_dir_all(&temp_dir)?;
        }
        Ok(())
    }

    #[allow(dead_code)]
    pub(crate) fn temp_dir(&self) -> Option<&Path> {
        self.temp_dir.as_deref()
    }
}

impl Drop for LoadedScript {
    fn drop(&mut self) {
        if let Some(temp_dir) = self.temp_dir.take() {
            let _ = fs::remove_dir_all(&temp_dir);
        }
    }
}

pub fn load_script(script: &ScriptSource, limits: &RunLimits) -> Result<LoadedScript> {
    match script {
        ScriptSource::File { path } => {
            let path = PathBuf::from(path);
            let metadata = fs::metadata(&path)?;
            if metadata.len() as usize > limits.max_script_bytes {
                return Err(Error::resource_exhausted(format!(
                    "script exceeds max_script_bytes: {} > {}",
                    metadata.len(),
                    limits.max_script_bytes
                ))
                .with_primary_diagnostic("script-too-large"));
            }

            Ok(LoadedScript {
                compile_path: path,
                temp_dir: None,
            })
        }
        ScriptSource::Inline { source } => load_inline_script(source, limits),
    }
}

fn load_inline_script(source: &str, limits: &RunLimits) -> Result<LoadedScript> {
    if source.len() > limits.max_script_bytes {
        return Err(Error::resource_exhausted(format!(
            "script exceeds max_script_bytes: {} > {}",
            source.len(),
            limits.max_script_bytes
        ))
        .with_primary_diagnostic("script-too-large"));
    }

    let unique = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_err(|err| {
            Error::internal(err.to_string()).with_primary_diagnostic("system-time-error")
        })?
        .as_nanos();
    // System time alone can collide under parallel tests or high-throughput
    // callers. The process-local counter keeps cleanup for concurrent inline
    // runs from racing over the same temp directory.
    let counter = TEMP_DIR_COUNTER.fetch_add(1, Ordering::Relaxed);
    let temp_dir = std::env::temp_dir().join(format!(
        "sandlet-sdk-{}-{unique}-{counter}",
        std::process::id()
    ));
    fs::create_dir_all(&temp_dir)?;

    // Enforce total temporary-space budget for inline scripts.
    let max_temp = limits.max_temp_bytes;
    if max_temp > 0 {
        let mut total = 0usize;
        for entry in fs::read_dir(&temp_dir)? {
            let entry = entry?;
            let md = entry.metadata()?;
            if md.is_file() {
                total += md.len() as usize;
            }
        }
        // Future: grow this accounting as we write files. For now we just gate the write below.
        if total + source.len() > max_temp {
            let _ = fs::remove_dir_all(&temp_dir);
            return Err(Error::resource_exhausted(format!(
                "inline script temp space exceeds max_temp_bytes: {} > {}",
                total + source.len(),
                max_temp
            ))
            .with_primary_diagnostic("temp-space-exhausted"));
        }
    }

    let script_path = temp_dir.join("main.roto");
    fs::write(&script_path, source)?;

    Ok(LoadedScript {
        compile_path: script_path,
        temp_dir: Some(temp_dir),
    })
}

pub fn script_source_from_path(path: impl AsRef<Path>) -> ScriptSource {
    ScriptSource::File {
        path: path.as_ref().display().to_string(),
    }
}

#[cfg(test)]
mod tests {
    use std::time::{SystemTime, UNIX_EPOCH};

    use super::*;

    fn default_limits() -> RunLimits {
        RunLimits {
            max_script_bytes: 64 * 1024,
            max_args: 64,
            max_arg_bytes: 64 * 1024,
            max_output_bytes: 64 * 1024,
            timeout_s: 0,
            max_temp_bytes: 64 * 1024,
            metadata: None,
        }
    }

    fn unique_test_dir(prefix: &str) -> PathBuf {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("system time should be after unix epoch")
            .as_nanos();
        std::env::temp_dir().join(format!("{prefix}-{}-{nanos}", std::process::id()))
    }

    #[test]
    fn file_backed_script_load_succeeds() {
        let tmp = unique_test_dir("sandlet-loader-test");
        let script_path = tmp.join("test.roto");
        fs::create_dir_all(&tmp).unwrap();
        fs::write(&script_path, r#"fn main() -> String { "hello" }"#).unwrap();

        let script = ScriptSource::File {
            path: script_path.to_string_lossy().to_string(),
        };
        let loaded = load_script(&script, &default_limits()).unwrap();
        assert_eq!(loaded.compile_path, script_path);
        assert!(loaded.temp_dir().is_none());
    }

    #[test]
    fn file_backed_script_load_fails_when_max_script_bytes_exceeded() {
        let tmp = unique_test_dir("sandlet-loader-test");
        let script_path = tmp.join("large.roto");
        fs::create_dir_all(&tmp).unwrap();
        fs::write(&script_path, "x").unwrap();

        let mut limits = default_limits();
        limits.max_script_bytes = 0;
        let script = ScriptSource::File {
            path: script_path.to_string_lossy().to_string(),
        };
        let err = load_script(&script, &limits).unwrap_err();
        assert!(err.message_str().contains("max_script_bytes"));
    }

    #[test]
    fn inline_script_load_writes_main_roto() {
        let source = r#"fn main() -> String { "hi" }"#;
        let script = ScriptSource::Inline {
            source: source.into(),
        };
        let loaded = load_script(&script, &default_limits()).unwrap();
        assert!(loaded.temp_dir().is_some());
        let script_path = loaded.temp_dir().unwrap().join("main.roto");
        assert!(script_path.exists());
        let content = fs::read_to_string(&script_path).unwrap();
        assert_eq!(content, source);
    }

    #[test]
    fn inline_script_load_fails_when_max_script_bytes_exceeded() {
        let mut limits = default_limits();
        limits.max_script_bytes = 4;
        let script = ScriptSource::Inline {
            source: "hello world".into(),
        };
        let err = load_script(&script, &limits).unwrap_err();
        assert!(err.message_str().contains("max_script_bytes"));
    }

    #[test]
    fn inline_script_load_fails_when_max_temp_bytes_too_small() {
        let mut limits = default_limits();
        limits.max_temp_bytes = 1;
        let script = ScriptSource::Inline {
            source: "fn main(){}".into(),
        };
        let err = load_script(&script, &limits).unwrap_err();
        assert!(err.message_str().contains("max_temp_bytes"));
    }

    #[test]
    fn explicit_cleanup_removes_temp_directory() {
        let script = ScriptSource::Inline {
            source: r#"fn main() -> String { "ok" }"#.into(),
        };
        let loaded = load_script(&script, &default_limits()).unwrap();
        let temp = loaded.temp_dir().unwrap().to_path_buf();
        assert!(temp.exists());
        loaded.cleanup().unwrap();
        assert!(!temp.exists());
    }

    #[test]
    fn drop_cleanup_removes_temp_directory() {
        let script = ScriptSource::Inline {
            source: r#"fn main() -> String { "ok" }"#.into(),
        };
        let loaded = load_script(&script, &default_limits()).unwrap();
        let temp = loaded.temp_dir().unwrap().to_path_buf();
        assert!(temp.exists());
        drop(loaded);
        assert!(!temp.exists());
    }
}
