//! Path management for archive directory structure.
//!
//! Defines the archive directory layout and provides utilities for
//! constructing paths to various archive components.

use std::path::PathBuf;
use std::sync::Arc;

use tokio::sync::Mutex;
use uuid::Uuid;

/// Archive path utilities
///
/// Provides methods to generate paths for all archive components:
/// - Sessions: `.contexts/{scroll_id}/`
/// - Connectors: `.db/connectors/{connector_uid}/`
/// - Files: `.files/{ab}/{cd}/{ef}/{...}` (sharded by SHA-256)
///
/// Also carries the per-archive mutex that serialises `store_file`'s
/// read-modify-rewrite of `.files/file_index.ndjson` — the shared
/// `file_index.tmp` path made concurrent calls race on `rename`.
pub struct ArchivePaths {
    root: PathBuf,
    /// Guards the critical section in `storage::files::store_file` that
    /// rewrites the per-archive file index. Cloneable `Arc` so callers
    /// that share the same `ArchivePaths` instance serialise correctly.
    file_index_lock: Arc<Mutex<()>>,
}

impl ArchivePaths {
    /// Create a new ArchivePaths instance
    pub fn new(root: PathBuf) -> Self {
        Self {
            root,
            file_index_lock: Arc::new(Mutex::new(())),
        }
    }

    /// Get the archive root directory
    pub fn root(&self) -> &PathBuf {
        &self.root
    }

    /// Acquire the per-archive file-index lock. Held across the
    /// `read → modify → append-temp → rename` sequence in
    /// `storage::files::store_file`.
    pub(crate) fn file_index_lock(&self) -> Arc<Mutex<()>> {
        Arc::clone(&self.file_index_lock)
    }

    // ========================================================================
    // Session Paths
    // ========================================================================

    /// Get the directory for a specific session
    ///
    /// Returns: `{root}/.contexts/{scroll_id}`
    pub fn session_dir(&self, scroll_id: Uuid) -> PathBuf {
        self.root.join(".contexts").join(scroll_id.to_string())
    }

    /// Get the session metadata JSON file path
    ///
    /// Returns: `{root}/.contexts/{scroll_id}/session.json`
    pub fn session_json(&self, scroll_id: Uuid) -> PathBuf {
        self.session_dir(scroll_id).join("session.json")
    }

    /// Get the messages NDJSON file path for WRITE operations
    ///
    /// For read operations, use `messages_path_for_read()` which supports both .jsonl and .ndjson
    ///
    /// Returns: `{root}/.contexts/{scroll_id}/messages.ndjson`
    #[deprecated(since = "0.2.0", note = "Use messages_path_for_write() instead")]
    pub fn messages_ndjson(&self, scroll_id: Uuid) -> PathBuf {
        self.session_dir(scroll_id).join("messages.ndjson")
    }

    /// Resolve messages file path for reading.
    /// Checks for .jsonl first, falls back to .ndjson
    pub fn messages_path_for_read(&self, scroll_id: Uuid) -> PathBuf {
        let session_dir = self.session_dir(scroll_id);
        self.resolve_ndjson_or_jsonl(&session_dir, "messages")
    }

    /// Get the messages file path for WRITE operations.
    /// Always returns .jsonl path (new canonical format).
    pub fn messages_path_for_write(&self, scroll_id: Uuid) -> PathBuf {
        self.session_dir(scroll_id).join("messages.jsonl")
    }

    /// Get the events file path for meta sessions (.jsonl format)
    ///
    /// Meta sessions (AcpConnection) store connection events in events.jsonl
    /// instead of messages. These events track connection lifecycle and session navigation.
    ///
    /// Returns: `{root}/.contexts/{scroll_id}/events.jsonl`
    pub fn events_path(&self, scroll_id: Uuid) -> PathBuf {
        self.session_dir(scroll_id).join("events.jsonl")
    }

    /// Get the DAG index file path.
    ///
    /// Returns: `{root}/.db/dag.jsonl`
    pub fn dag_path(&self) -> PathBuf {
        self.root.join(".db").join("dag.jsonl")
    }

    /// Resolve sessions mapping file path for reading.
    /// Checks for .jsonl first, falls back to .ndjson
    pub fn sessions_path_for_read(&self, connector_uid: Uuid) -> PathBuf {
        let connector_dir = self.connector_dir(connector_uid);
        self.resolve_ndjson_or_jsonl(&connector_dir, "sessions")
    }

    /// Get the sessions file path for WRITE operations.
    /// Always returns .jsonl path (new canonical format).
    pub fn sessions_path_for_write(&self, connector_uid: Uuid) -> PathBuf {
        self.connector_dir(connector_uid).join("sessions.jsonl")
    }

    // ========================================================================
    // Connector Paths
    // ========================================================================

    /// Get the directory for a specific connector
    ///
    /// Returns: `{root}/.db/connectors/{connector_uid}`
    pub fn connector_dir(&self, connector_uid: Uuid) -> PathBuf {
        self.root
            .join(".db")
            .join("connectors")
            .join(connector_uid.to_string())
    }

    /// Get the connector index TSV file path
    ///
    /// Returns: `{root}/.db/connectors/index.tsv`
    pub fn connector_index_tsv(&self) -> PathBuf {
        self.root.join(".db").join("connectors").join("index.tsv")
    }

    // ========================================================================
    // File Storage Paths
    // ========================================================================

    /// Get the blob path for a file using sharded storage
    ///
    /// Sharding strategy:
    /// - Input: `sha256:abcdef0123456789...`
    /// - Strip `sha256:` prefix
    /// - Shard by first 6 characters (2-char segments)
    /// - Returns: `{root}/.files/ab/cd/ef/0123456789...`
    ///
    /// # Arguments
    /// * `file_id` - File identifier (e.g., "sha256:abcdef...")
    pub fn file_blob_path(&self, file_id: &str) -> PathBuf {
        // Strip "sha256:" prefix if present
        let hash = file_id.strip_prefix("sha256:").unwrap_or(file_id);

        // Extract first 6 chars for sharding (3 levels of 2 chars each)
        // If hash is shorter, we'll just use what we have
        let (shard1, remainder) = if hash.len() >= 2 {
            hash.split_at(2)
        } else {
            (hash, "")
        };

        let (shard2, remainder) = if remainder.len() >= 2 {
            remainder.split_at(2)
        } else {
            (remainder, "")
        };

        let (shard3, _) = if remainder.len() >= 2 {
            remainder.split_at(2)
        } else {
            (remainder, remainder)
        };

        // Build the sharded path
        let mut path = self.root.join(".files");

        if !shard1.is_empty() {
            path = path.join(shard1);
        }
        if !shard2.is_empty() {
            path = path.join(shard2);
        }
        if !shard3.is_empty() {
            path = path.join(shard3);
        }

        // Use the full hash (without prefix) as filename
        path.join(hash)
    }

    // ========================================================================
    // Directory Creation
    // ========================================================================

    /// Ensure all required directories exist for a session
    ///
    /// Creates the session directory if it doesn't exist.
    pub async fn ensure_dirs(&self, scroll_id: Uuid) -> std::io::Result<()> {
        let session_dir = self.session_dir(scroll_id);
        tokio::fs::create_dir_all(session_dir).await
    }

    /// Ensure the connector directory exists
    ///
    /// Creates `.db/connectors/{connector_uid}/` if it doesn't exist.
    /// This should be called before any operations that write to connector-specific files.
    pub async fn ensure_connector_dir(&self, connector_uid: Uuid) -> std::io::Result<()> {
        let connector_dir = self.connector_dir(connector_uid);
        tokio::fs::create_dir_all(&connector_dir).await
    }

    /// Generic resolution: prefer .jsonl, fall back to .ndjson
    ///
    /// This enables backward compatibility with existing .ndjson archives
    /// while supporting the more widely-recognized .jsonl extension.
    fn resolve_ndjson_or_jsonl(&self, dir: &std::path::Path, base_name: &str) -> PathBuf {
        // Check for .jsonl first (newer, more prominent extension)
        let jsonl_path = dir.join(format!("{}.jsonl", base_name));
        if jsonl_path.exists() {
            return jsonl_path;
        }

        // Fall back to .ndjson (legacy format, still canonical for writes in Phase 1)
        dir.join(format!("{}.ndjson", base_name))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::Path;

    #[test]
    fn test_session_dir() {
        let paths = ArchivePaths::new(PathBuf::from("/archive"));
        let scroll_id = Uuid::parse_str("018c8f7e-7b6a-7e3c-9f2d-1a2b3c4d5e6f").unwrap();

        let session_dir = paths.session_dir(scroll_id);
        assert_eq!(
            session_dir,
            Path::new("/archive/.contexts/018c8f7e-7b6a-7e3c-9f2d-1a2b3c4d5e6f")
        );
    }

    #[test]
    fn test_session_json() {
        let paths = ArchivePaths::new(PathBuf::from("/archive"));
        let scroll_id = Uuid::parse_str("018c8f7e-7b6a-7e3c-9f2d-1a2b3c4d5e6f").unwrap();

        let json_path = paths.session_json(scroll_id);
        assert_eq!(
            json_path,
            Path::new("/archive/.contexts/018c8f7e-7b6a-7e3c-9f2d-1a2b3c4d5e6f/session.json")
        );
    }

    #[test]
    fn test_messages_ndjson() {
        let paths = ArchivePaths::new(PathBuf::from("/archive"));
        let scroll_id = Uuid::parse_str("018c8f7e-7b6a-7e3c-9f2d-1a2b3c4d5e6f").unwrap();

        let messages_path = paths.messages_ndjson(scroll_id);
        assert_eq!(
            messages_path,
            Path::new("/archive/.contexts/018c8f7e-7b6a-7e3c-9f2d-1a2b3c4d5e6f/messages.ndjson")
        );
    }

    #[test]
    fn test_connector_dir() {
        let paths = ArchivePaths::new(PathBuf::from("/archive"));
        let connector_uid = Uuid::parse_str("018c8f7e-7b6a-7e3c-9f2d-1a2b3c4d5e6f").unwrap();

        let connector_dir = paths.connector_dir(connector_uid);
        assert_eq!(
            connector_dir,
            Path::new("/archive/.db/connectors/018c8f7e-7b6a-7e3c-9f2d-1a2b3c4d5e6f")
        );
    }

    #[test]
    fn test_connector_index_tsv() {
        let paths = ArchivePaths::new(PathBuf::from("/archive"));

        let index_path = paths.connector_index_tsv();
        assert_eq!(index_path, Path::new("/archive/.db/connectors/index.tsv"));
    }

    #[test]
    fn test_file_blob_path_with_prefix() {
        let paths = ArchivePaths::new(PathBuf::from("/archive"));
        let file_id = "sha256:abcdef0123456789";

        let blob_path = paths.file_blob_path(file_id);
        assert_eq!(
            blob_path,
            Path::new("/archive/.files/ab/cd/ef/abcdef0123456789")
        );
    }

    #[test]
    fn test_file_blob_path_without_prefix() {
        let paths = ArchivePaths::new(PathBuf::from("/archive"));
        let file_id = "abcdef0123456789";

        let blob_path = paths.file_blob_path(file_id);
        assert_eq!(
            blob_path,
            Path::new("/archive/.files/ab/cd/ef/abcdef0123456789")
        );
    }

    #[test]
    fn test_file_blob_path_short_hash() {
        let paths = ArchivePaths::new(PathBuf::from("/archive"));

        // Very short hash (less than 6 chars)
        let file_id = "sha256:abc";
        let blob_path = paths.file_blob_path(file_id);
        assert_eq!(blob_path, Path::new("/archive/.files/ab/c/abc"));

        // 4 char hash
        let file_id = "sha256:abcd";
        let blob_path = paths.file_blob_path(file_id);
        assert_eq!(blob_path, Path::new("/archive/.files/ab/cd/abcd"));

        // 5 char hash
        let file_id = "sha256:abcde";
        let blob_path = paths.file_blob_path(file_id);
        assert_eq!(blob_path, Path::new("/archive/.files/ab/cd/e/abcde"));
    }

    #[test]
    fn test_file_blob_path_long_hash() {
        let paths = ArchivePaths::new(PathBuf::from("/archive"));
        let file_id = "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";

        let blob_path = paths.file_blob_path(file_id);
        assert_eq!(
            blob_path,
            Path::new("/archive/.files/ab/cd/ef/abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789")
        );
    }

    #[test]
    fn test_paths_use_correct_separators() {
        let paths = ArchivePaths::new(PathBuf::from("/archive"));
        let scroll_id = Uuid::parse_str("018c8f7e-7b6a-7e3c-9f2d-1a2b3c4d5e6f").unwrap();

        // All paths should use PathBuf which handles platform separators
        let session_dir = paths.session_dir(scroll_id);
        let session_json = paths.session_json(scroll_id);
        let messages_ndjson = paths.messages_ndjson(scroll_id);

        // On Windows, these should contain backslashes; on Unix, forward slashes
        // PathBuf handles this automatically, so we just verify the components
        assert!(session_dir.to_string_lossy().contains(".contexts"));
        assert!(session_json.to_string_lossy().contains("session.json"));
        assert!(messages_ndjson
            .to_string_lossy()
            .contains("messages.ndjson"));
    }

    #[tokio::test]
    async fn test_ensure_dirs() {
        // Create a temporary directory for testing
        let temp_dir = std::env::temp_dir().join(format!("archivist_test_{}", Uuid::now_v7()));
        let paths = ArchivePaths::new(temp_dir.clone());
        let scroll_id = Uuid::now_v7();

        // Directory should not exist yet
        assert!(!paths.session_dir(scroll_id).exists());

        // Create the directory
        paths.ensure_dirs(scroll_id).await.unwrap();

        // Directory should now exist
        assert!(paths.session_dir(scroll_id).exists());

        // Clean up
        tokio::fs::remove_dir_all(temp_dir).await.ok();
    }

    #[tokio::test]
    async fn test_messages_path_for_read_ndjson_only() {
        // Create a temporary directory with only .ndjson file
        let temp_dir = std::env::temp_dir().join(format!("archivist_test_{}", Uuid::now_v7()));
        let paths = ArchivePaths::new(temp_dir.clone());
        let scroll_id = Uuid::now_v7();

        // Create session directory and .ndjson file
        paths.ensure_dirs(scroll_id).await.unwrap();
        let ndjson_path = paths.messages_ndjson(scroll_id);
        tokio::fs::write(&ndjson_path, "test content").await.unwrap();

        // messages_path_for_read should return the .ndjson path
        let resolved_path = paths.messages_path_for_read(scroll_id);
        assert_eq!(resolved_path, ndjson_path);
        assert!(resolved_path.to_string_lossy().ends_with("messages.ndjson"));

        // Clean up
        tokio::fs::remove_dir_all(temp_dir).await.ok();
    }

    #[tokio::test]
    async fn test_messages_path_for_read_jsonl_preferred() {
        // Create a temporary directory with both .ndjson and .jsonl files
        let temp_dir = std::env::temp_dir().join(format!("archivist_test_{}", Uuid::now_v7()));
        let paths = ArchivePaths::new(temp_dir.clone());
        let scroll_id = Uuid::now_v7();

        // Create session directory and both files
        paths.ensure_dirs(scroll_id).await.unwrap();
        let session_dir = paths.session_dir(scroll_id);
        let ndjson_path = session_dir.join("messages.ndjson");
        let jsonl_path = session_dir.join("messages.jsonl");

        tokio::fs::write(&ndjson_path, "old content").await.unwrap();
        tokio::fs::write(&jsonl_path, "new content").await.unwrap();

        // messages_path_for_read should prefer .jsonl
        let resolved_path = paths.messages_path_for_read(scroll_id);
        assert_eq!(resolved_path, jsonl_path);
        assert!(resolved_path.to_string_lossy().ends_with("messages.jsonl"));

        // Clean up
        tokio::fs::remove_dir_all(temp_dir).await.ok();
    }
}
