//! File-based ProjectStore implementation.
//!
//! Uses one directory per project under a configurable root.
//! Follows the archivist pattern with atomic JSON writes.

use crate::error::{ProjectError, Result};
use crate::params::*;
use crate::storage::io::{read_json, read_json_or_default, write_json};
use crate::storage::paths::ProjectPaths;
use crate::traits::ProjectStore;
use chrono::Utc;
use dirigent_protocol::project::{
    AccessMode, Project, ProjectBinding, ProjectRepository, Worktree,
};
use std::path::PathBuf;
use tracing::{debug, info};
use uuid::Uuid;

/// File-based project store.
///
/// Each project gets its own directory under the root:
/// ```text
/// root/
///   {project_uuid}/
///     project.json
///     repositories.json   (Phase 2)
///     bindings.json        (Phase 5)
///     worktrees.json       (Phase 4)
/// ```
pub struct FileBasedProjectStore {
    paths: ProjectPaths,
}

impl FileBasedProjectStore {
    /// Create a new file-based store at the given root directory.
    ///
    /// The root directory will be created if it doesn't exist.
    pub async fn new(root: impl Into<PathBuf>) -> std::io::Result<Self> {
        let root = root.into();
        tokio::fs::create_dir_all(&root).await?;
        Ok(Self {
            paths: ProjectPaths::new(root),
        })
    }

    /// Find which project owns a given repository ID.
    async fn find_project_for_repo(&self, repo_id: &Uuid) -> Result<Uuid> {
        let project_ids = self.scan_project_ids().await?;
        for project_id in &project_ids {
            let repos_path = self.paths.repositories_json(project_id);
            let repos: Vec<ProjectRepository> = read_json_or_default(&repos_path).await?;
            if repos.iter().any(|r| r.id == *repo_id) {
                return Ok(*project_id);
            }
        }
        Err(ProjectError::RepositoryNotFound(*repo_id))
    }

    /// Scan the root directory for project UUIDs.
    async fn scan_project_ids(&self) -> Result<Vec<Uuid>> {
        let mut ids = Vec::new();
        let mut entries = tokio::fs::read_dir(self.paths.root()).await?;

        while let Some(entry) = entries.next_entry().await? {
            if entry.file_type().await?.is_dir() {
                if let Some(name) = entry.file_name().to_str() {
                    if let Ok(uuid) = Uuid::parse_str(name) {
                        ids.push(uuid);
                    }
                }
            }
        }

        Ok(ids)
    }
}

#[async_trait::async_trait]
impl ProjectStore for FileBasedProjectStore {
    async fn create_project(&self, params: CreateProjectParams) -> Result<Project> {
        // Validate
        if params.name.trim().is_empty() {
            return Err(ProjectError::Validation(
                "project name cannot be empty".to_string(),
            ));
        }

        let now = Utc::now();
        let project = Project {
            id: Uuid::now_v7(),
            name: params.name,
            description: params.description,
            icon: params.icon,
            owner: params.owner,
            members: vec![],
            tags: params.tags,
            languages: params.languages,
            linked_projects: vec![],
            metadata: if params.metadata.is_null() {
                serde_json::json!({})
            } else {
                params.metadata
            },
            created_at: now,
            updated_at: now,
        };

        // Create project directory
        let project_dir = self.paths.project_dir(&project.id);
        tokio::fs::create_dir_all(&project_dir).await?;

        // Write project.json
        let path = self.paths.project_json(&project.id);
        write_json(&path, &project).await?;

        info!(project_id = %project.id, name = %project.name, "Created project");
        Ok(project)
    }

    async fn get_project(&self, id: &Uuid) -> Result<Project> {
        let path = self.paths.project_json(id);
        read_json(&path).await.map_err(|e| match e.kind() {
            std::io::ErrorKind::NotFound => ProjectError::NotFound(*id),
            _ => ProjectError::Storage(e),
        })
    }

    async fn list_projects(&self, filter: ProjectFilter) -> Result<Vec<Project>> {
        let ids = self.scan_project_ids().await?;
        let mut projects = Vec::new();

        for id in ids {
            match self.get_project(&id).await {
                Ok(project) => {
                    // Apply filters
                    if let Some(ref owner) = filter.owner {
                        if project.owner != *owner {
                            continue;
                        }
                    }
                    if let Some(ref name_contains) = filter.name_contains {
                        if !project
                            .name
                            .to_lowercase()
                            .contains(&name_contains.to_lowercase())
                        {
                            continue;
                        }
                    }
                    if !filter.tags.is_empty()
                        && !filter.tags.iter().all(|t| project.tags.contains(t))
                    {
                        continue;
                    }
                    projects.push(project);
                }
                Err(e) => {
                    debug!(project_id = %id, error = %e, "Skipping unreadable project");
                }
            }
        }

        // Sort by name for consistent ordering
        projects.sort_by(|a, b| a.name.cmp(&b.name));
        Ok(projects)
    }

    async fn update_project(&self, id: &Uuid, update: ProjectUpdate) -> Result<Project> {
        let mut project = self.get_project(id).await?;

        if let Some(name) = update.name {
            if name.trim().is_empty() {
                return Err(ProjectError::Validation(
                    "project name cannot be empty".to_string(),
                ));
            }
            project.name = name;
        }
        if let Some(description) = update.description {
            project.description = description;
        }
        if let Some(icon) = update.icon {
            project.icon = icon;
        }
        if let Some(tags) = update.tags {
            project.tags = tags;
        }
        if let Some(languages) = update.languages {
            project.languages = languages;
        }
        if let Some(metadata) = update.metadata {
            project.metadata = metadata;
        }

        project.updated_at = Utc::now();

        let path = self.paths.project_json(id);
        write_json(&path, &project).await?;

        info!(project_id = %id, "Updated project");
        Ok(project)
    }

    async fn delete_project(&self, id: &Uuid) -> Result<()> {
        let project_dir = self.paths.project_dir(id);
        if !project_dir.exists() {
            return Err(ProjectError::NotFound(*id));
        }

        tokio::fs::remove_dir_all(&project_dir).await?;
        info!(project_id = %id, "Deleted project");
        Ok(())
    }

    // --- Repository management (Phase 2 - scaffolded) ---

    async fn add_repository(&self, params: AddRepositoryParams) -> Result<ProjectRepository> {
        // Ensure project exists
        let _ = self.get_project(&params.project_id).await?;

        let now = Utc::now();
        let repo = ProjectRepository {
            id: Uuid::now_v7(),
            project_id: params.project_id,
            path: params.path,
            is_primary: params.is_primary,
            label: params.label,
            access: AccessMode::ReadWrite,
            created_at: now,
            updated_at: now,
        };

        // Read existing repos, append, write back
        let repos_path = self.paths.repositories_json(&params.project_id);
        let mut repos: Vec<ProjectRepository> = read_json_or_default(&repos_path).await?;

        // If this is primary, unset others
        if repo.is_primary {
            for r in repos.iter_mut() {
                r.is_primary = false;
            }
        }

        repos.push(repo.clone());
        write_json(&repos_path, &repos).await?;

        info!(repo_id = %repo.id, project_id = %params.project_id, "Added repository");
        Ok(repo)
    }

    async fn remove_repository(&self, id: &Uuid) -> Result<()> {
        // Scan all projects to find the repo
        let project_ids = self.scan_project_ids().await?;
        for project_id in project_ids {
            let repos_path = self.paths.repositories_json(&project_id);
            let mut repos: Vec<ProjectRepository> = read_json_or_default(&repos_path).await?;
            let original_len = repos.len();
            repos.retain(|r| r.id != *id);
            if repos.len() < original_len {
                write_json(&repos_path, &repos).await?;
                info!(repo_id = %id, "Removed repository");
                return Ok(());
            }
        }
        Err(ProjectError::RepositoryNotFound(*id))
    }

    async fn set_primary_repository(&self, project_id: &Uuid, repo_id: &Uuid) -> Result<()> {
        let repos_path = self.paths.repositories_json(project_id);
        let mut repos: Vec<ProjectRepository> = read_json_or_default(&repos_path).await?;

        let mut found = false;
        for r in repos.iter_mut() {
            r.is_primary = r.id == *repo_id;
            if r.id == *repo_id {
                found = true;
            }
        }

        if !found {
            return Err(ProjectError::RepositoryNotFound(*repo_id));
        }

        write_json(&repos_path, &repos).await?;
        Ok(())
    }

    async fn list_repositories(&self, project_id: &Uuid) -> Result<Vec<ProjectRepository>> {
        let repos_path = self.paths.repositories_json(project_id);
        Ok(read_json_or_default(&repos_path).await?)
    }

    // --- Worktrees (Phase 4) ---

    async fn add_worktree(&self, params: AddWorktreeParams) -> Result<Worktree> {
        // Find which project owns this repository
        let project_id = self.find_project_for_repo(&params.repository_id).await?;

        let worktree = Worktree {
            id: Uuid::now_v7(),
            repository_id: params.repository_id,
            path: params.path,
            branch: params.branch,
            work_branch: params.work_branch,
            naming_strategy: params.naming_strategy,
            created_at: Utc::now(),
        };

        let wt_path = self.paths.worktrees_json(&project_id);
        let mut worktrees: Vec<Worktree> = read_json_or_default(&wt_path).await?;
        worktrees.push(worktree.clone());
        write_json(&wt_path, &worktrees).await?;

        info!(worktree_id = %worktree.id, repo_id = %params.repository_id, "Added worktree");
        Ok(worktree)
    }

    async fn remove_worktree(&self, worktree_id: &Uuid) -> Result<()> {
        let project_ids = self.scan_project_ids().await?;
        for project_id in project_ids {
            let wt_path = self.paths.worktrees_json(&project_id);
            let mut worktrees: Vec<Worktree> = read_json_or_default(&wt_path).await?;
            let original_len = worktrees.len();
            worktrees.retain(|w| w.id != *worktree_id);
            if worktrees.len() < original_len {
                write_json(&wt_path, &worktrees).await?;
                info!(worktree_id = %worktree_id, "Removed worktree");
                return Ok(());
            }
        }
        Err(ProjectError::WorktreeNotFound(*worktree_id))
    }

    async fn list_worktrees(&self, repository_id: &Uuid) -> Result<Vec<Worktree>> {
        let project_id = self.find_project_for_repo(repository_id).await?;
        let wt_path = self.paths.worktrees_json(&project_id);
        let all: Vec<Worktree> = read_json_or_default(&wt_path).await?;
        Ok(all
            .into_iter()
            .filter(|w| w.repository_id == *repository_id)
            .collect())
    }

    async fn update_worktree(
        &self,
        worktree_id: &Uuid,
        update: WorktreeUpdate,
    ) -> Result<Worktree> {
        let project_ids = self.scan_project_ids().await?;
        for project_id in &project_ids {
            let wt_path = self.paths.worktrees_json(project_id);
            let mut worktrees: Vec<Worktree> = read_json_or_default(&wt_path).await?;
            if let Some(wt) = worktrees.iter_mut().find(|w| w.id == *worktree_id) {
                if let Some(branch) = update.branch {
                    wt.branch = branch;
                }
                if let Some(work_branch) = update.work_branch {
                    wt.work_branch = work_branch;
                }
                let updated = wt.clone();
                write_json(&wt_path, &worktrees).await?;
                return Ok(updated);
            }
        }
        Err(ProjectError::WorktreeNotFound(*worktree_id))
    }

    // --- Bindings (Phase 5 - scaffolded) ---

    async fn bind(&self, params: BindParams) -> Result<ProjectBinding> {
        let _ = self.get_project(&params.project_id).await?;

        let binding = ProjectBinding {
            id: Uuid::now_v7(),
            project_id: params.project_id,
            connector_id: params.connector_id,
            session_id: params.session_id,
            working_dir: params.working_dir,
        };

        let bindings_path = self.paths.bindings_json(&params.project_id);
        let mut bindings: Vec<ProjectBinding> = read_json_or_default(&bindings_path).await?;
        bindings.push(binding.clone());
        write_json(&bindings_path, &bindings).await?;

        Ok(binding)
    }

    async fn unbind(&self, binding_id: &Uuid) -> Result<()> {
        let project_ids = self.scan_project_ids().await?;
        for project_id in project_ids {
            let bindings_path = self.paths.bindings_json(&project_id);
            let mut bindings: Vec<ProjectBinding> = read_json_or_default(&bindings_path).await?;
            let original_len = bindings.len();
            bindings.retain(|b| b.id != *binding_id);
            if bindings.len() < original_len {
                write_json(&bindings_path, &bindings).await?;
                return Ok(());
            }
        }
        Err(ProjectError::BindingNotFound(*binding_id))
    }

    async fn list_bindings(&self, project_id: &Uuid) -> Result<Vec<ProjectBinding>> {
        let bindings_path = self.paths.bindings_json(project_id);
        Ok(read_json_or_default(&bindings_path).await?)
    }

    // --- Resolution (Phase 2 - scaffolded) ---

    async fn resolve_working_dir(
        &self,
        project_id: &Uuid,
        repo_id: Option<&Uuid>,
    ) -> Result<PathBuf> {
        // Verify the project exists before falling back to default_working_dir.
        // Without this, a missing project directory yields an empty repo list
        // (via read_json_or_default) and silently falls through to the default.
        self.get_project(project_id).await?;

        let repos = self.list_repositories(project_id).await?;

        // If repo_id specified, use that
        if let Some(rid) = repo_id {
            if let Some(repo) = repos.iter().find(|r| r.id == *rid) {
                return Ok(repo.path.clone());
            }
            return Err(ProjectError::RepositoryNotFound(*rid));
        }

        // Use primary repo, or first repo
        if let Some(repo) = repos.iter().find(|r| r.is_primary).or(repos.first()) {
            return Ok(repo.path.clone());
        }

        Err(ProjectError::Validation(format!(
            "project {} has no repositories configured",
            project_id
        )))
    }
}
