//! Bounded file manipulation for the agent harness.
//!
//! The agent may create and update app-managed artifacts inside approved
//! subdirectories only. Arbitrary filesystem access is denied by construction.

use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

use chrono::Utc;
use serde::{Deserialize, Serialize};
use thiserror::Error;

const ARTIFACTS_DIR_NAME: &str = "artifacts";
const NOTES_SUBDIR: &str = "notes";
const REPORTS_SUBDIR: &str = "reports";
const EXPORTS_SUBDIR: &str = "exports";

#[derive(Debug, Error)]
pub enum ArtifactError {
    #[error("path escapes the artifacts directory: {0}")]
    PathEscape(String),
    #[error("file name contains invalid characters: {0}")]
    InvalidFileName(String),
    #[error("file name is empty")]
    EmptyFileName,
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    #[error("JSON error: {0}")]
    Json(#[from] serde_json::Error),
}

fn format_system_time(t: SystemTime) -> String {
    let duration = t.duration_since(UNIX_EPOCH).unwrap_or_default();
    chrono::DateTime::from_timestamp(duration.as_secs() as i64, duration.subsec_nanos())
        .map(|dt| dt.to_rfc3339())
        .unwrap_or_default()
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ArtifactKind {
    Note,
    Report,
    Export,
}

impl ArtifactKind {
    pub const fn all() -> [ArtifactKind; 3] {
        [
            ArtifactKind::Note,
            ArtifactKind::Report,
            ArtifactKind::Export,
        ]
    }

    pub fn subdir(self) -> &'static str {
        match self {
            ArtifactKind::Note => NOTES_SUBDIR,
            ArtifactKind::Report => REPORTS_SUBDIR,
            ArtifactKind::Export => EXPORTS_SUBDIR,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtifactMetadata {
    pub name: String,
    pub kind: ArtifactKind,
    pub size_bytes: u64,
    pub created_at: String,
    pub updated_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtifactEntry {
    pub metadata: ArtifactMetadata,
    pub content: String,
}

pub struct ArtifactStore {
    root: PathBuf,
}

impl ArtifactStore {
    pub fn new(app_data_dir: &Path) -> Result<Self, ArtifactError> {
        let root = app_data_dir.join(ARTIFACTS_DIR_NAME);
        std::fs::create_dir_all(&root)?;
        for kind in ArtifactKind::all() {
            std::fs::create_dir_all(root.join(kind.subdir()))?;
        }
        Ok(Self { root })
    }

    pub fn save(
        &self,
        kind: ArtifactKind,
        name: &str,
        content: &str,
    ) -> Result<ArtifactMetadata, ArtifactError> {
        validate_file_name(name)?;
        let dir = self.root.join(kind.subdir());
        let path = dir.join(name);
        ensure_within(&self.root, &path)?;

        let now = Utc::now().to_rfc3339();
        let existed = path.exists();
        let created_at = if existed {
            read_created_at(&path)?.unwrap_or_else(|| now.clone())
        } else {
            now.clone()
        };

        std::fs::write(&path, content)?;

        let size_bytes = content.len() as u64;
        Ok(ArtifactMetadata {
            name: name.to_string(),
            kind,
            size_bytes,
            created_at,
            updated_at: now,
        })
    }

    pub fn load(&self, kind: ArtifactKind, name: &str) -> Result<ArtifactEntry, ArtifactError> {
        validate_file_name(name)?;
        let path = self.root.join(kind.subdir()).join(name);
        ensure_within(&self.root, &path)?;

        let content = std::fs::read_to_string(&path)?;
        let metadata = std::fs::metadata(&path)?;
        let created_at = read_created_at(&path)?.unwrap_or_else(|| {
            metadata
                .created()
                .map(format_system_time)
                .unwrap_or_default()
        });
        let updated_at = metadata
            .modified()
            .map(format_system_time)
            .unwrap_or_default();

        Ok(ArtifactEntry {
            metadata: ArtifactMetadata {
                name: name.to_string(),
                kind,
                size_bytes: metadata.len(),
                created_at,
                updated_at,
            },
            content,
        })
    }

    pub fn delete(&self, kind: ArtifactKind, name: &str) -> Result<(), ArtifactError> {
        validate_file_name(name)?;
        let path = self.root.join(kind.subdir()).join(name);
        ensure_within(&self.root, &path)?;
        std::fs::remove_file(&path)?;
        Ok(())
    }

    pub fn list(&self, kind: ArtifactKind) -> Result<Vec<ArtifactMetadata>, ArtifactError> {
        let dir = self.root.join(kind.subdir());
        if !dir.exists() {
            return Ok(Vec::new());
        }

        let mut entries = Vec::new();
        for entry in std::fs::read_dir(&dir)? {
            let entry = entry?;
            let path = entry.path();
            if !path.is_file() {
                continue;
            }
            let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
            if file_name.starts_with('.') {
                continue;
            }

            let metadata = entry.metadata()?;
            let created_at = read_created_at(&path)?.unwrap_or_else(|| {
                metadata
                    .created()
                    .map(format_system_time)
                    .unwrap_or_default()
            });
            let updated_at = metadata
                .modified()
                .map(format_system_time)
                .unwrap_or_default();

            entries.push(ArtifactMetadata {
                name: file_name.to_string(),
                kind,
                size_bytes: metadata.len(),
                created_at,
                updated_at,
            });
        }

        entries.sort_by(|a, b| a.name.cmp(&b.name));
        Ok(entries)
    }
}

fn validate_file_name(name: &str) -> Result<(), ArtifactError> {
    if name.is_empty() {
        return Err(ArtifactError::EmptyFileName);
    }
    if name.contains('/')
        || name.contains('\\')
        || name.contains('\0')
        || name == "."
        || name == ".."
    {
        return Err(ArtifactError::InvalidFileName(name.to_string()));
    }
    Ok(())
}

fn ensure_within(root: &Path, candidate: &Path) -> Result<(), ArtifactError> {
    let canonical_root = root
        .canonicalize()
        .map_err(|_| ArtifactError::PathEscape(root.display().to_string()))?;

    let canonical_candidate = match candidate.canonicalize() {
        Ok(c) => c,
        Err(_) => {
            let parent = candidate.parent().unwrap_or(Path::new("."));
            let file_name = candidate
                .file_name()
                .ok_or_else(|| ArtifactError::PathEscape(candidate.display().to_string()))?;
            match parent.canonicalize() {
                Ok(canonical_parent) => canonical_parent.join(file_name),
                Err(_) => {
                    return Err(ArtifactError::PathEscape(candidate.display().to_string()));
                }
            }
        }
    };

    if !canonical_candidate.starts_with(&canonical_root) {
        return Err(ArtifactError::PathEscape(candidate.display().to_string()));
    }
    Ok(())
}

fn read_created_at(path: &Path) -> Result<Option<String>, ArtifactError> {
    let metadata = path.metadata().map_err(ArtifactError::Io)?;
    match metadata.created() {
        Ok(t) => Ok(Some(format_system_time(t))),
        Err(_) => Ok(None),
    }
}

#[cfg(test)]
mod tests {
    use tempfile::TempDir;

    use super::*;

    fn setup() -> (TempDir, ArtifactStore) {
        let tmp = TempDir::new().unwrap();
        let store = ArtifactStore::new(tmp.path()).unwrap();
        (tmp, store)
    }

    #[test]
    fn creates_subdirectories_on_init() {
        let tmp = TempDir::new().unwrap();
        let store = ArtifactStore::new(tmp.path()).unwrap();
        assert!(store.root.join(NOTES_SUBDIR).exists());
        assert!(store.root.join(REPORTS_SUBDIR).exists());
        assert!(store.root.join(EXPORTS_SUBDIR).exists());
    }

    #[test]
    fn save_and_load_roundtrip() {
        let (_tmp, store) = setup();
        let meta = store
            .save(ArtifactKind::Note, "test.md", "hello world")
            .unwrap();
        assert_eq!(meta.name, "test.md");
        assert_eq!(meta.kind, ArtifactKind::Note);

        let entry = store.load(ArtifactKind::Note, "test.md").unwrap();
        assert_eq!(entry.content, "hello world");
        assert_eq!(entry.metadata.name, "test.md");
    }

    #[test]
    fn save_overwrites_existing() {
        let (_tmp, store) = setup();
        store.save(ArtifactKind::Report, "report.md", "v1").unwrap();
        let meta = store.save(ArtifactKind::Report, "report.md", "v2").unwrap();
        let entry = store.load(ArtifactKind::Report, "report.md").unwrap();
        assert_eq!(entry.content, "v2");
        assert_eq!(meta.size_bytes, 2);
    }

    #[test]
    fn delete_removes_file() {
        let (_tmp, store) = setup();
        store
            .save(ArtifactKind::Export, "export.json", "{}")
            .unwrap();
        store.delete(ArtifactKind::Export, "export.json").unwrap();
        assert!(store.load(ArtifactKind::Export, "export.json").is_err());
    }

    #[test]
    fn list_returns_sorted_entries() {
        let (_tmp, store) = setup();
        store.save(ArtifactKind::Note, "b.md", "b").unwrap();
        store.save(ArtifactKind::Note, "a.md", "a").unwrap();
        let entries = store.list(ArtifactKind::Note).unwrap();
        assert_eq!(entries.len(), 2);
        assert_eq!(entries[0].name, "a.md");
        assert_eq!(entries[1].name, "b.md");
    }

    #[test]
    fn list_ignores_dotfiles() {
        let (_tmp, store) = setup();
        let dotfile = store.root.join(NOTES_SUBDIR).join(".hidden");
        std::fs::write(&dotfile, "hidden").unwrap();
        let entries = store.list(ArtifactKind::Note).unwrap();
        assert!(entries.is_empty());
    }

    #[test]
    fn rejects_empty_file_name() {
        let (_tmp, store) = setup();
        assert!(store.save(ArtifactKind::Note, "", "content").is_err());
    }

    #[test]
    fn rejects_path_traversal() {
        let (_tmp, store) = setup();
        assert!(store
            .save(ArtifactKind::Note, "../etc/passwd", "x")
            .is_err());
        assert!(store
            .save(ArtifactKind::Note, "..\\windows\\system32", "x")
            .is_err());
        assert!(store.load(ArtifactKind::Note, "../../secret").is_err());
        assert!(store.delete(ArtifactKind::Note, "/etc/passwd").is_err());
    }

    #[test]
    fn rejects_dot_and_dotdot() {
        let (_tmp, store) = setup();
        assert!(store.save(ArtifactKind::Note, ".", "x").is_err());
        assert!(store.save(ArtifactKind::Note, "..", "x").is_err());
    }

    #[test]
    fn list_empty_when_no_files() {
        let (_tmp, store) = setup();
        let entries = store.list(ArtifactKind::Report).unwrap();
        assert!(entries.is_empty());
    }

    #[test]
    fn validate_file_name_rejects_null_byte() {
        assert!(validate_file_name("foo\0bar").is_err());
    }
}
