//! TSV (Tab-Separated Values) storage utilities.
//!
//! Handles reading and writing TSV files for session listings and indices.
//! TSV format is human-readable and grep-able, making it ideal for manual
//! inspection and command-line processing.

use crate::types::ConnectorIndexRow;
use std::path::Path;
use tokio::io::AsyncWriteExt;
use uuid::Uuid;

/// Write connector index to a TSV file atomically
///
/// This function:
/// 1. Generates the header line
/// 2. Formats each row as tab-separated values
/// 3. Writes to a temporary file
/// 4. Renames to the target path (atomic operation)
///
/// TSV format:
/// ```text
/// connector_uid\ttype\ttitle\tclient_native_id\talias_of\tcreated_at
/// 018c8f7e-...\tOpenCode\tLocal Dev\topencode@...\t\t2025-01-15T12:34:56Z
/// ```
///
/// # Arguments
/// * `path` - Path to the TSV file
/// * `rows` - Rows to write
pub async fn write_connector_index(path: &Path, rows: &[ConnectorIndexRow]) -> std::io::Result<()> {
    // Generate header
    let header = "connector_uid\ttype\ttitle\tclient_native_id\talias_of\tcreated_at\tfingerprint\n";

    // Format rows
    let mut content = String::from(header);
    for row in rows {
        let alias_of_str = row
            .alias_of
            .as_ref()
            .map(|u| u.to_string())
            .unwrap_or_default();

        let fingerprint_str = row.fingerprint.as_deref().unwrap_or("");

        let line = format!(
            "{}\t{}\t{}\t{}\t{}\t{}\t{}\n",
            row.connector_uid,
            row.r#type,
            row.title,
            row.client_native_id,
            alias_of_str,
            row.created_at.to_rfc3339(),
            fingerprint_str,
        );
        content.push_str(&line);
    }

    // Write to temp file
    let temp_path = path.with_extension("tmp");
    let mut file = tokio::fs::File::create(&temp_path).await?;
    file.write_all(content.as_bytes()).await?;
    file.sync_all().await?;
    drop(file);

    // Atomically rename
    tokio::fs::rename(&temp_path, path).await?;

    Ok(())
}

/// Read connector index from a TSV file
///
/// If the file doesn't exist, returns an empty vector.
///
/// # Arguments
/// * `path` - Path to the TSV file
///
/// # Returns
/// Vector of connector index rows
pub async fn read_connector_index(path: &Path) -> std::io::Result<Vec<ConnectorIndexRow>> {
    // Check if file exists
    if !path.exists() {
        return Ok(Vec::new());
    }

    // Read file to string
    let content = tokio::fs::read_to_string(path).await?;

    // Parse line by line
    let mut rows = Vec::new();
    for (line_num, line) in content.lines().enumerate() {
        // Skip header (line 0)
        if line_num == 0 {
            continue;
        }

        // Skip empty lines
        if line.trim().is_empty() {
            continue;
        }

        // Split by tab
        let parts: Vec<&str> = line.split('\t').collect();
        // Accept 6 columns (legacy, no fingerprint) or 7 columns (with fingerprint)
        if parts.len() != 6 && parts.len() != 7 {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                format!(
                    "Invalid TSV format at line {}: expected 6 or 7 fields, got {}",
                    line_num + 1,
                    parts.len()
                ),
            ));
        }

        // Parse fields
        let connector_uid = Uuid::parse_str(parts[0]).map_err(|e| {
            std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                format!("Invalid UUID at line {}: {}", line_num + 1, e),
            )
        })?;

        let r#type = parts[1].to_string();
        let title = parts[2].to_string();
        let client_native_id = parts[3].to_string();

        let alias_of = if parts[4].is_empty() {
            None
        } else {
            Some(Uuid::parse_str(parts[4]).map_err(|e| {
                std::io::Error::new(
                    std::io::ErrorKind::InvalidData,
                    format!("Invalid alias_of UUID at line {}: {}", line_num + 1, e),
                )
            })?)
        };

        let created_at = chrono::DateTime::parse_from_rfc3339(parts[5])
            .map_err(|e| {
                std::io::Error::new(
                    std::io::ErrorKind::InvalidData,
                    format!("Invalid timestamp at line {}: {}", line_num + 1, e),
                )
            })?
            .with_timezone(&chrono::Utc);

        // Parse optional fingerprint (7th column, may be absent in legacy files)
        let fingerprint = if parts.len() >= 7 && !parts[6].is_empty() {
            Some(parts[6].to_string())
        } else {
            None
        };

        rows.push(ConnectorIndexRow {
            connector_uid,
            r#type,
            title,
            client_native_id,
            alias_of,
            created_at,
            fingerprint,
        });
    }

    Ok(rows)
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::{DateTime, Utc};
    use std::time::SystemTime;

    #[tokio::test]
    async fn test_write_and_read_roundtrip() {
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("test_tsv_{}.tsv", Uuid::now_v7()));

        let uid1 = Uuid::now_v7();
        let uid2 = Uuid::now_v7();
        let uid3 = Uuid::now_v7();
        let now = DateTime::<Utc>::from(SystemTime::now());

        let rows = vec![
            ConnectorIndexRow {
                connector_uid: uid1,
                r#type: "OpenCode".to_string(),
                title: "Local Dev".to_string(),
                client_native_id: "opencode@localhost:12225".to_string(),
                alias_of: None,
                created_at: now,
                fingerprint: None,
            },
            ConnectorIndexRow {
                connector_uid: uid2,
                r#type: "ACP".to_string(),
                title: "Remote Agent".to_string(),
                client_native_id: "acp@http://localhost:3000".to_string(),
                alias_of: Some(uid3),
                created_at: now,
                fingerprint: None,
            },
        ];

        // Write
        write_connector_index(&file_path, &rows).await.unwrap();

        // Read back
        let read_rows = read_connector_index(&file_path).await.unwrap();

        // Verify
        assert_eq!(read_rows.len(), 2);
        assert_eq!(read_rows[0].connector_uid, uid1);
        assert_eq!(read_rows[0].r#type, "OpenCode");
        assert_eq!(read_rows[0].title, "Local Dev");
        assert_eq!(read_rows[0].alias_of, None);

        assert_eq!(read_rows[1].connector_uid, uid2);
        assert_eq!(read_rows[1].r#type, "ACP");
        assert_eq!(read_rows[1].alias_of, Some(uid3));

        // Clean up
        tokio::fs::remove_file(&file_path).await.ok();
    }

    #[tokio::test]
    async fn test_optional_field_handling() {
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("test_optional_{}.tsv", Uuid::now_v7()));

        let uid1 = Uuid::now_v7();
        let uid2 = Uuid::now_v7();
        let now = DateTime::<Utc>::from(SystemTime::now());

        let rows = vec![
            ConnectorIndexRow {
                connector_uid: uid1,
                r#type: "Type1".to_string(),
                title: "Title1".to_string(),
                client_native_id: "client1".to_string(),
                alias_of: None, // Empty alias_of
                created_at: now,
                fingerprint: None,
            },
            ConnectorIndexRow {
                connector_uid: uid2,
                r#type: "Type2".to_string(),
                title: "Title2".to_string(),
                client_native_id: "client2".to_string(),
                alias_of: Some(uid1), // Non-empty alias_of
                created_at: now,
                fingerprint: None,
            },
        ];

        // Write
        write_connector_index(&file_path, &rows).await.unwrap();

        // Verify raw content has empty string for None
        let content = tokio::fs::read_to_string(&file_path).await.unwrap();
        let lines: Vec<&str> = content.lines().collect();

        // First data line should have empty alias_of (two consecutive tabs)
        assert!(lines[1].contains("\t\t"));

        // Second data line should have a UUID for alias_of
        assert!(lines[2].contains(&uid1.to_string()));

        // Read back
        let read_rows = read_connector_index(&file_path).await.unwrap();

        // Verify optional field handling
        assert_eq!(read_rows[0].alias_of, None);
        assert_eq!(read_rows[1].alias_of, Some(uid1));

        // Clean up
        tokio::fs::remove_file(&file_path).await.ok();
    }

    #[tokio::test]
    async fn test_header_generation() {
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("test_header_{}.tsv", Uuid::now_v7()));

        // Write empty index
        write_connector_index(&file_path, &[]).await.unwrap();

        // Read raw content
        let content = tokio::fs::read_to_string(&file_path).await.unwrap();

        // Verify header
        assert_eq!(
            content.trim(),
            "connector_uid\ttype\ttitle\tclient_native_id\talias_of\tcreated_at\tfingerprint"
        );

        // Clean up
        tokio::fs::remove_file(&file_path).await.ok();
    }

    #[tokio::test]
    async fn test_rfc3339_timestamp_formatting() {
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("test_timestamp_{}.tsv", Uuid::now_v7()));

        let uid = Uuid::now_v7();
        let timestamp = DateTime::<Utc>::from(SystemTime::now());

        let rows = vec![ConnectorIndexRow {
            connector_uid: uid,
            r#type: "Test".to_string(),
            title: "Title".to_string(),
            client_native_id: "client".to_string(),
            alias_of: None,
            created_at: timestamp,
            fingerprint: None,
        }];

        // Write
        write_connector_index(&file_path, &rows).await.unwrap();

        // Read raw content
        let content = tokio::fs::read_to_string(&file_path).await.unwrap();

        // Verify RFC 3339 format in content
        assert!(content.contains('T'));
        assert!(content.contains('Z') || content.contains('+'));

        // Read back and verify timestamp is preserved
        let read_rows = read_connector_index(&file_path).await.unwrap();
        let diff =
            (timestamp.timestamp_millis() - read_rows[0].created_at.timestamp_millis()).abs();
        assert!(diff < 1000, "Timestamp difference too large");

        // Clean up
        tokio::fs::remove_file(&file_path).await.ok();
    }

    #[tokio::test]
    async fn test_missing_file_returns_empty_vec() {
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("nonexistent_{}.tsv", Uuid::now_v7()));

        // Should return empty vec, not error
        let rows = read_connector_index(&file_path).await.unwrap();
        assert_eq!(rows.len(), 0);
    }

    #[tokio::test]
    async fn test_malformed_tsv_error() {
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("test_malformed_{}.tsv", Uuid::now_v7()));

        // Write malformed TSV (missing fields)
        let content =
            "connector_uid\ttype\ttitle\tclient_native_id\talias_of\tcreated_at\nuid1\ttype1\n";
        tokio::fs::write(&file_path, content).await.unwrap();

        // Should fail with InvalidData
        let result = read_connector_index(&file_path).await;
        assert!(result.is_err());

        match result {
            Err(e) => {
                assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
                assert!(e.to_string().contains("expected 6 or 7 fields"));
            }
            Ok(_) => panic!("Expected error"),
        }

        // Clean up
        tokio::fs::remove_file(&file_path).await.ok();
    }

    #[tokio::test]
    async fn test_invalid_uuid_error() {
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("test_invalid_uuid_{}.tsv", Uuid::now_v7()));

        // Write TSV with invalid UUID
        let content = "connector_uid\ttype\ttitle\tclient_native_id\talias_of\tcreated_at\ninvalid-uuid\tType\tTitle\tClient\t\t2025-01-15T12:34:56Z\n";
        tokio::fs::write(&file_path, content).await.unwrap();

        // Should fail with InvalidData
        let result = read_connector_index(&file_path).await;
        assert!(result.is_err());

        match result {
            Err(e) => {
                assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
                assert!(e.to_string().contains("Invalid UUID"));
            }
            Ok(_) => panic!("Expected error"),
        }

        // Clean up
        tokio::fs::remove_file(&file_path).await.ok();
    }

    #[tokio::test]
    async fn test_invalid_timestamp_error() {
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("test_invalid_timestamp_{}.tsv", Uuid::now_v7()));

        let uid = Uuid::now_v7();

        // Write TSV with invalid timestamp
        let content = format!("connector_uid\ttype\ttitle\tclient_native_id\talias_of\tcreated_at\n{}\tType\tTitle\tClient\t\tinvalid-timestamp\n", uid);
        tokio::fs::write(&file_path, content).await.unwrap();

        // Should fail with InvalidData
        let result = read_connector_index(&file_path).await;
        assert!(result.is_err());

        match result {
            Err(e) => {
                assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
                assert!(e.to_string().contains("Invalid timestamp"));
            }
            Ok(_) => panic!("Expected error"),
        }

        // Clean up
        tokio::fs::remove_file(&file_path).await.ok();
    }

    #[tokio::test]
    async fn test_atomic_write() {
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("test_atomic_{}.tsv", Uuid::now_v7()));

        let uid1 = Uuid::now_v7();
        let uid2 = Uuid::now_v7();
        let now = DateTime::<Utc>::from(SystemTime::now());

        let rows1 = vec![ConnectorIndexRow {
            connector_uid: uid1,
            r#type: "First".to_string(),
            title: "First Write".to_string(),
            client_native_id: "client1".to_string(),
            alias_of: None,
            created_at: now,
            fingerprint: None,
        }];

        let rows2 = vec![ConnectorIndexRow {
            connector_uid: uid2,
            r#type: "Second".to_string(),
            title: "Second Write".to_string(),
            client_native_id: "client2".to_string(),
            alias_of: None,
            created_at: now,
            fingerprint: None,
        }];

        // Write first version
        write_connector_index(&file_path, &rows1).await.unwrap();

        // Verify first version
        let read1 = read_connector_index(&file_path).await.unwrap();
        assert_eq!(read1.len(), 1);
        assert_eq!(read1[0].title, "First Write");

        // Overwrite with second version
        write_connector_index(&file_path, &rows2).await.unwrap();

        // Verify second version
        let read2 = read_connector_index(&file_path).await.unwrap();
        assert_eq!(read2.len(), 1);
        assert_eq!(read2[0].title, "Second Write");

        // Temp file should not exist
        let temp_path = file_path.with_extension("tmp");
        assert!(!temp_path.exists());

        // Clean up
        tokio::fs::remove_file(&file_path).await.ok();
    }

    #[tokio::test]
    async fn test_legacy_six_column_tsv_compatibility() {
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("test_legacy_tsv_{}.tsv", Uuid::now_v7()));

        let uid = Uuid::now_v7();

        // Write a legacy 6-column TSV (no fingerprint column)
        let content = format!(
            "connector_uid\ttype\ttitle\tclient_native_id\talias_of\tcreated_at\n{}\tOpenCode\tLegacy\tclient-legacy\t\t2025-01-15T12:34:56Z\n",
            uid
        );
        tokio::fs::write(&file_path, content).await.unwrap();

        // Should parse successfully with fingerprint = None
        let rows = read_connector_index(&file_path).await.unwrap();
        assert_eq!(rows.len(), 1);
        assert_eq!(rows[0].connector_uid, uid);
        assert_eq!(rows[0].r#type, "OpenCode");
        assert_eq!(rows[0].title, "Legacy");
        assert_eq!(rows[0].fingerprint, None);

        // Clean up
        tokio::fs::remove_file(&file_path).await.ok();
    }

    #[tokio::test]
    async fn test_fingerprint_roundtrip() {
        let temp_dir = std::env::temp_dir();
        let file_path = temp_dir.join(format!("test_fingerprint_{}.tsv", Uuid::now_v7()));

        let uid1 = Uuid::now_v7();
        let uid2 = Uuid::now_v7();
        let now = DateTime::<Utc>::from(SystemTime::now());

        let rows = vec![
            ConnectorIndexRow {
                connector_uid: uid1,
                r#type: "ACP".to_string(),
                title: "Claude CLI".to_string(),
                client_native_id: "acp-claude-1".to_string(),
                alias_of: None,
                created_at: now,
                fingerprint: Some("acp/stdio:/usr/bin/claude".to_string()),
            },
            ConnectorIndexRow {
                connector_uid: uid2,
                r#type: "OpenCode".to_string(),
                title: "No Fingerprint".to_string(),
                client_native_id: "opencode@localhost".to_string(),
                alias_of: None,
                created_at: now,
                fingerprint: None,
            },
        ];

        // Write
        write_connector_index(&file_path, &rows).await.unwrap();

        // Read back
        let read_rows = read_connector_index(&file_path).await.unwrap();

        assert_eq!(read_rows.len(), 2);
        assert_eq!(
            read_rows[0].fingerprint,
            Some("acp/stdio:/usr/bin/claude".to_string())
        );
        assert_eq!(read_rows[1].fingerprint, None);

        // Clean up
        tokio::fs::remove_file(&file_path).await.ok();
    }
}
