use std::collections::HashMap;
use camino::{Utf8Path, Utf8PathBuf};
use crate::types::*;
use crate::error::{AntError, Result};

/// Discover the Claude Code home directory (~/.claude/).
pub fn discover_claude_home() -> Result<Utf8PathBuf> {
    let home = dirs::home_dir().ok_or(AntError::HomeNotFound)?;
    let claude_dir = home.join(".claude");
    if !claude_dir.exists() {
        return Err(AntError::HomeNotFound);
    }
    Utf8PathBuf::try_from(claude_dir.to_path_buf())
        .map_err(|e| AntError::InvalidPath(e.to_string()))
}

/// Normalise a native path to forward slashes for consistent storage.
fn normalize_to_forward_slashes(path: &str) -> String {
    path.replace('\\', "/")
}

/// Resolve the original filesystem path for a Claude project directory.
///
/// Priority:
/// 1. `projectPath` from `sessions-index.json` (authoritative, cheap)
/// 2. `cwd` from the first user message in any session JSONL (authoritative, costs one file parse)
/// 3. `decode_project_path` (lossy fallback for empty project directories)
pub fn resolve_original_path(dir_name: &str, sessions: &[SessionRef]) -> String {
    // 1. Try sessions-index.json projectPath
    for session in sessions {
        if let Some(ref idx) = session.index_entry {
            if let Some(ref path) = idx.project_path {
                if !path.is_empty() {
                    return normalize_to_forward_slashes(path);
                }
            }
        }
    }

    // 2. Try cwd from first user message in any session
    for session in sessions {
        if let Ok(msgs) = crate::parser::parse_session(&session.jsonl_path) {
            for msg in &msgs {
                if let crate::types::RawMessage::User(user) = msg {
                    if let Some(ref cwd) = user.cwd {
                        if !cwd.is_empty() {
                            return normalize_to_forward_slashes(cwd);
                        }
                    }
                }
            }
        }
    }

    // 3. Lossy fallback
    decode_project_path(dir_name)
}

/// Discover all Claude Code project directories under the given home.
pub fn discover_projects(home: &Utf8Path) -> Result<Vec<ClaudeProject>> {
    let projects_dir = home.join("projects");
    if !projects_dir.as_std_path().exists() {
        return Ok(Vec::new());
    }

    let mut projects = Vec::new();
    for entry in std::fs::read_dir(projects_dir.as_std_path())? {
        let entry = entry?;
        let path = entry.path();
        if !path.is_dir() {
            continue;
        }
        let dir_name = match path.file_name().and_then(|n| n.to_str()) {
            Some(name) => name.to_string(),
            None => continue,
        };

        let utf8_path = match Utf8PathBuf::try_from(path.clone()) {
            Ok(p) => p,
            Err(_) => continue,
        };

        let sessions = discover_sessions(&utf8_path)?;
        let original_path = resolve_original_path(&dir_name, &sessions);

        projects.push(ClaudeProject {
            path: utf8_path,
            original_path,
            sessions,
        });
    }

    Ok(projects)
}

/// Decode an encoded project folder name back to the original path (lossy).
///
/// **Warning**: Claude Code's encoding replaces `\`, `/`, AND `_` all with
/// `-`, making this decoding ambiguous. For example, `G--dev-projects-adk-rust`
/// could be `G:/dev/projects/adk-rust` or `G:/dev/projects/adk/rust`. Prefer
/// [`resolve_original_path`] which reads ground truth from `sessions-index.json`
/// or session JSONL files. This function is a last-resort fallback for empty
/// project directories with no sessions or index.
pub fn decode_project_path(encoded: &str) -> String {
    // Split on "--" to recover path segments separated by the original separators.
    let parts: Vec<&str> = encoded.split("--").collect();

    if parts.is_empty() {
        return encoded.to_string();
    }

    let mut result = String::new();

    let first = parts[0];

    if first.len() == 1 && first.chars().next().map_or(false, |c| c.is_ascii_uppercase()) {
        // Windows drive letter: "G" → "G:"
        result.push_str(first);
        result.push(':');
    } else if first.starts_with('-') || first.is_empty() {
        // Unix-style absolute path: the original path started with "/".
        // The first segment has a leading "-" that encoded the root separator.
        // Strip that leading "-" to recover the first directory component.
        let component = first.trim_start_matches('-');
        result.push('/');
        if !component.is_empty() {
            // Single dashes within the component are path separators.
            result.push_str(&component.replace('-', "/"));
        }
    } else {
        result.push_str(first);
    }

    // Remaining "--"-separated parts are additional path components.
    // Within each part, single "-" represent path separators.
    for part in &parts[1..] {
        result.push('/');
        result.push_str(&part.replace('-', "/"));
    }

    result
}

/// Discover all session JSONL files in a project directory.
pub fn discover_sessions(project_dir: &Utf8Path) -> Result<Vec<SessionRef>> {
    let index = load_session_index(project_dir);
    let mut sessions = Vec::new();

    for entry in std::fs::read_dir(project_dir.as_std_path())? {
        let entry = entry?;
        let path = entry.path();

        // Only .jsonl files
        let extension = path.extension().and_then(|e| e.to_str());
        if extension != Some("jsonl") {
            continue;
        }

        let stem = match path.file_stem().and_then(|s| s.to_str()) {
            Some(s) => s.to_string(),
            None => continue,
        };

        let utf8_path = match Utf8PathBuf::try_from(path) {
            Ok(p) => p,
            Err(_) => continue,
        };

        // Check for artifacts directory (same name as the session stem).
        let artifacts_dir = {
            let dir = project_dir.join(&stem);
            if dir.as_std_path().is_dir() {
                Some(dir)
            } else {
                None
            }
        };

        let index_entry = index.as_ref().and_then(|idx| idx.get(&stem).cloned());

        sessions.push(SessionRef {
            id: stem,
            jsonl_path: utf8_path,
            artifacts_dir,
            index_entry,
        });
    }

    Ok(sessions)
}

/// Load `sessions-index.json` if it exists in the given project directory.
fn load_session_index(project_dir: &Utf8Path) -> Option<HashMap<String, SessionIndexEntry>> {
    let index_path = project_dir.join("sessions-index.json");
    if !index_path.as_std_path().exists() {
        return None;
    }

    let content = std::fs::read_to_string(index_path.as_std_path()).ok()?;
    serde_json::from_str::<HashMap<String, SessionIndexEntry>>(&content).ok()
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    #[test]
    fn decode_project_path_windows() {
        assert_eq!(
            decode_project_path("G--dev-projects-dirigent"),
            "G:/dev/projects/dirigent"
        );
    }

    #[test]
    fn decode_project_path_windows_users() {
        assert_eq!(
            decode_project_path("C--Users-g4b-tmp"),
            "C:/Users/g4b/tmp"
        );
    }

    #[test]
    fn decode_project_path_unix() {
        assert_eq!(
            decode_project_path("-home-user-projects-foo"),
            "/home/user/projects/foo"
        );
    }

    #[test]
    fn discover_sessions_in_temp_dir() {
        let tmp = TempDir::new().unwrap();
        let project_dir = Utf8Path::from_path(tmp.path()).unwrap();

        // Create fake session files.
        std::fs::write(project_dir.join("abc-def-123.jsonl").as_std_path(), "{}\n").unwrap();
        std::fs::write(project_dir.join("xyz-456-789.jsonl").as_std_path(), "{}\n").unwrap();
        // Create an artifacts directory for one session.
        std::fs::create_dir(project_dir.join("abc-def-123").as_std_path()).unwrap();

        let sessions = discover_sessions(project_dir).unwrap();
        assert_eq!(sessions.len(), 2);

        let with_artifacts = sessions.iter().find(|s| s.id == "abc-def-123").unwrap();
        assert!(with_artifacts.artifacts_dir.is_some());

        let without_artifacts = sessions.iter().find(|s| s.id == "xyz-456-789").unwrap();
        assert!(without_artifacts.artifacts_dir.is_none());
    }

    #[test]
    fn discover_sessions_ignores_non_jsonl() {
        let tmp = TempDir::new().unwrap();
        let project_dir = Utf8Path::from_path(tmp.path()).unwrap();

        std::fs::write(project_dir.join("session.jsonl").as_std_path(), "{}\n").unwrap();
        std::fs::write(
            project_dir.join("sessions-index.json").as_std_path(),
            "{}",
        )
        .unwrap();
        std::fs::create_dir(project_dir.join("some-dir").as_std_path()).unwrap();

        let sessions = discover_sessions(project_dir).unwrap();
        assert_eq!(sessions.len(), 1);
        assert_eq!(sessions[0].id, "session");
    }

    #[test]
    fn discover_sessions_loads_index_entry() {
        let tmp = TempDir::new().unwrap();
        let project_dir = Utf8Path::from_path(tmp.path()).unwrap();

        std::fs::write(project_dir.join("abc-123.jsonl").as_std_path(), "{}\n").unwrap();

        let index_json = r#"{
            "abc-123": {
                "sessionId": "abc-123",
                "firstPrompt": "Hello",
                "summary": "A test session",
                "messageCount": 5
            }
        }"#;
        std::fs::write(
            project_dir.join("sessions-index.json").as_std_path(),
            index_json,
        )
        .unwrap();

        let sessions = discover_sessions(project_dir).unwrap();
        assert_eq!(sessions.len(), 1);

        let entry = sessions[0].index_entry.as_ref().unwrap();
        assert_eq!(entry.session_id.as_deref(), Some("abc-123"));
        assert_eq!(entry.first_prompt.as_deref(), Some("Hello"));
        assert_eq!(entry.message_count, Some(5));
    }

    #[test]
    fn resolve_original_path_prefers_index_project_path() {
        let sessions = vec![SessionRef {
            id: "test-session".to_string(),
            jsonl_path: Utf8PathBuf::from("/tmp/fake.jsonl"),
            artifacts_dir: None,
            index_entry: Some(SessionIndexEntry {
                session_id: Some("test-session".to_string()),
                first_prompt: None,
                summary: None,
                message_count: None,
                created: None,
                modified: None,
                git_branch: None,
                project_path: Some(r"G:\dev\projects\bevy_sprite3d".to_string()),
            }),
        }];
        let result = resolve_original_path("G--dev-projects-bevy-sprite3d", &sessions);
        assert_eq!(result, "G:/dev/projects/bevy_sprite3d");
    }

    #[test]
    fn resolve_original_path_falls_back_to_decode() {
        let sessions: Vec<SessionRef> = vec![];
        let result = resolve_original_path("G--dev-projects-dirigent", &sessions);
        assert_eq!(result, "G:/dev/projects/dirigent");
    }

    #[test]
    fn discover_projects_empty_when_no_projects_dir() {
        let tmp = TempDir::new().unwrap();
        let home_dir = Utf8Path::from_path(tmp.path()).unwrap();

        // No "projects" subdirectory — should return empty vec, not an error.
        let projects = discover_projects(home_dir).unwrap();
        assert!(projects.is_empty());
    }
}
