//! Filesystem scanner for discovering worktrees not yet tracked in the DB.

use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};

use crate::db::{
    WorktreeKind, WorktreeRecord, WorktreeStatus, id_from_path, now_epoch_secs, repo_name_from_path,
};

#[derive(Debug)]
pub struct DiscoveredWorktree {
    pub path: PathBuf,
    pub kind: WorktreeKind,
    pub creation_mode: &'static str,
    pub source_repo: Option<PathBuf>,
}

#[derive(Debug, Default)]
pub struct DiscoveryReport {
    pub found: Vec<DiscoveredWorktree>,
    pub skipped: u64,
}

fn should_skip_entry(name: &str) -> bool {
    name.starts_with('.')
        || name.ends_with(".ready")
        || name.ends_with(".claimed")
        || name.ends_with(".claiming")
}

fn detect_creation_mode(worktree_path: &Path) -> &'static str {
    let git_entry = worktree_path.join(".git");
    if git_entry.is_file() {
        "linked"
    } else if git_entry.is_dir() {
        "standalone"
    } else {
        "unknown"
    }
}

fn detect_source_repo(worktree_path: &Path) -> Option<PathBuf> {
    let git_entry = worktree_path.join(".git");
    if git_entry.is_file() {
        let content = std::fs::read_to_string(&git_entry).ok()?;
        let gitdir = content.trim().strip_prefix("gitdir: ")?;
        // Walk up from .git/worktrees/<name> → .git → repo root
        Path::new(gitdir)
            .parent()?
            .parent()?
            .parent()
            .map(|p| p.to_path_buf())
    } else if git_entry.is_dir() {
        Some(worktree_path.to_path_buf())
    } else {
        None
    }
}

fn scan_two_level_dir(base_dir: &Path, kind: WorktreeKind, report: &mut DiscoveryReport) {
    let Ok(outer_entries) = std::fs::read_dir(base_dir) else {
        return;
    };

    for outer in outer_entries.flatten() {
        let outer_path = outer.path();
        if !outer_path.is_dir() {
            continue;
        }
        let outer_name = outer.file_name();
        if should_skip_entry(&outer_name.to_string_lossy()) {
            report.skipped += 1;
            continue;
        }

        let Ok(inner_entries) = std::fs::read_dir(&outer_path) else {
            continue;
        };
        for inner in inner_entries.flatten() {
            let path = inner.path();
            if !path.is_dir() || should_skip_entry(&inner.file_name().to_string_lossy()) {
                report.skipped += 1;
                continue;
            }
            report.found.push(DiscoveredWorktree {
                creation_mode: detect_creation_mode(&path),
                source_repo: detect_source_repo(&path),
                path,
                kind,
            });
        }
    }
}

pub fn discover_worktrees(kigi_home: &Path) -> DiscoveryReport {
    let mut report = DiscoveryReport::default();
    scan_two_level_dir(
        &kigi_home.join("worktrees"),
        WorktreeKind::Session,
        &mut report,
    );
    scan_two_level_dir(
        &kigi_home.join("worktree_pool"),
        WorktreeKind::Pool,
        &mut report,
    );
    report
}

fn fs_creation_time(path: &Path) -> i64 {
    std::fs::metadata(path)
        .and_then(|m| m.created())
        .ok()
        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
        .map(|d| d.as_secs() as i64)
        .unwrap_or_else(now_epoch_secs)
}

impl DiscoveredWorktree {
    pub fn into_record(self) -> WorktreeRecord {
        let repo_name = self
            .source_repo
            .as_deref()
            .map(repo_name_from_path)
            .unwrap_or_else(|| "unknown".to_string());
        let source_repo = self.source_repo.unwrap_or_else(|| PathBuf::from("unknown"));
        let created_at = fs_creation_time(&self.path);

        WorktreeRecord {
            id: id_from_path(&self.path),
            path: self.path,
            source_repo,
            repo_name,
            kind: self.kind,
            creation_mode: self.creation_mode.to_owned(),
            git_ref: None,
            head_commit: None,
            session_id: None,
            creator_pid: None,
            created_at,
            last_accessed_at: None,
            status: WorktreeStatus::Alive,
            metadata: None,
        }
    }
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct RebuildReport {
    pub discovered: u64,
    pub registered: u64,
    pub already_tracked: u64,
}

pub fn rebuild_worktree_db(
    db: &crate::db::WorktreeDb,
    kigi_home: &Path,
) -> anyhow::Result<RebuildReport> {
    let discovery = discover_worktrees(kigi_home);
    let mut report = RebuildReport {
        discovered: discovery.found.len() as u64,
        ..Default::default()
    };

    for wt in discovery.found {
        let id = id_from_path(&wt.path);
        let path_str = wt.path.to_string_lossy();
        if db.get_by_id(&id)?.is_some() || db.get(&path_str)?.is_some() {
            report.already_tracked += 1;
            continue;
        }
        db.register(&wt.into_record())?;
        report.registered += 1;
    }

    Ok(report)
}

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

    fn make_fake_linked_worktree(path: &Path, gitdir_target: &str) {
        std::fs::create_dir_all(path).unwrap();
        std::fs::write(path.join(".git"), format!("gitdir: {gitdir_target}\n")).unwrap();
    }

    fn make_fake_standalone_worktree(path: &Path) {
        std::fs::create_dir_all(path.join(".git")).unwrap();
    }

    #[test]
    fn discover_session_worktrees() {
        let tmp = tempfile::TempDir::new().unwrap();
        let kigi_home = tmp.path();

        let wt = kigi_home.join("worktrees/myrepo/worktree-abc123");
        make_fake_linked_worktree(&wt, "/repo/.git/worktrees/abc123");

        let report = discover_worktrees(kigi_home);
        assert_eq!(report.found.len(), 1);
        assert_eq!(report.found[0].kind, WorktreeKind::Session);
        assert_eq!(report.found[0].creation_mode, "linked");
        assert_eq!(report.found[0].path, wt);
    }

    #[test]
    fn discover_pool_worktrees() {
        let tmp = tempfile::TempDir::new().unwrap();
        let kigi_home = tmp.path();

        let wt = kigi_home.join("worktree_pool/inst-1/pool-a");
        make_fake_standalone_worktree(&wt);

        let report = discover_worktrees(kigi_home);
        assert_eq!(report.found.len(), 1);
        assert_eq!(report.found[0].kind, WorktreeKind::Pool);
        assert_eq!(report.found[0].creation_mode, "standalone");
    }

    #[test]
    fn skips_dot_prefixed_and_markers() {
        let tmp = tempfile::TempDir::new().unwrap();
        let kigi_home = tmp.path();

        let base = kigi_home.join("worktrees/myrepo");
        std::fs::create_dir_all(&base).unwrap();

        std::fs::create_dir_all(base.join(".tmp_creating")).unwrap();
        std::fs::create_dir_all(base.join(".hidden")).unwrap();
        std::fs::write(base.join("abc.ready"), "").unwrap();
        std::fs::write(base.join("abc.claimed"), "").unwrap();

        make_fake_standalone_worktree(&base.join("real-session"));

        let report = discover_worktrees(kigi_home);
        assert_eq!(report.found.len(), 1);
        assert_eq!(report.found[0].path, base.join("real-session"));
        assert!(report.skipped > 0);
    }

    #[test]
    fn discover_empty_dirs_is_fine() {
        let tmp = tempfile::TempDir::new().unwrap();
        let report = discover_worktrees(tmp.path());
        assert!(report.found.is_empty());
        assert_eq!(report.skipped, 0);
    }

    #[test]
    fn rebuild_registers_and_skips_duplicates() {
        let tmp = tempfile::TempDir::new().unwrap();
        let kigi_home = tmp.path();

        let wt = kigi_home.join("worktrees/repo/worktree-sess1");
        make_fake_standalone_worktree(&wt);

        let db = crate::db::WorktreeDb::open_in_memory().unwrap();

        let r1 = rebuild_worktree_db(&db, kigi_home).unwrap();
        assert_eq!(r1.discovered, 1);
        assert_eq!(r1.registered, 1);
        assert_eq!(r1.already_tracked, 0);

        let r2 = rebuild_worktree_db(&db, kigi_home).unwrap();
        assert_eq!(r2.discovered, 1);
        assert_eq!(r2.registered, 0);
        assert_eq!(r2.already_tracked, 1);
    }

    #[test]
    fn rebuild_keeps_same_basename_worktrees_in_different_repos() {
        // The cross-repo eviction bug: two repos each have a `wt-abc`
        // worktree. Discovery + rebuild must register BOTH (distinct ids), not
        // collapse them into one and then permanently skip the other.
        let tmp = tempfile::TempDir::new().unwrap();
        // Canonicalize the fake home: production `resolve_kigi_home()` always
        // canonicalizes, and `WorktreeDb::get` canonicalizes path queries. On
        // macOS TempDirs live under the `/var` → `/private/var` symlink, so a
        // raw path stored at registration would never match the query.
        let canon_home = dunce::canonicalize(tmp.path()).unwrap();
        let kigi_home = canon_home.as_path();

        let wt_a = kigi_home.join("worktrees/repo-a/wt-abc");
        let wt_b = kigi_home.join("worktrees/repo-b/wt-abc");
        make_fake_standalone_worktree(&wt_a);
        make_fake_standalone_worktree(&wt_b);

        let db = crate::db::WorktreeDb::open_in_memory().unwrap();
        let report = rebuild_worktree_db(&db, kigi_home).unwrap();
        assert_eq!(report.discovered, 2);
        assert_eq!(
            report.registered, 2,
            "both same-basename worktrees must register"
        );

        let all = db.list(&crate::db::ListFilter::default()).unwrap();
        assert_eq!(all.len(), 2);
        assert!(db.get(&wt_a.to_string_lossy()).unwrap().is_some());
        assert!(db.get(&wt_b.to_string_lossy()).unwrap().is_some());

        // Idempotent: a second rebuild finds both already tracked, skips neither.
        let report2 = rebuild_worktree_db(&db, kigi_home).unwrap();
        assert_eq!(report2.registered, 0);
        assert_eq!(report2.already_tracked, 2);
    }

    #[test]
    fn detect_source_repo_from_linked() {
        let tmp = tempfile::TempDir::new().unwrap();
        let wt = tmp.path().join("wt");
        let gitdir = "/home/user/myrepo/.git/worktrees/wt";
        make_fake_linked_worktree(&wt, gitdir);

        let source = detect_source_repo(&wt);
        assert_eq!(source, Some(PathBuf::from("/home/user/myrepo")));
    }

    #[test]
    fn rebuild_report_serde_round_trip() {
        let report = RebuildReport {
            discovered: 5,
            registered: 3,
            already_tracked: 2,
        };
        let json = serde_json::to_string(&report).unwrap();
        let deser: RebuildReport = serde_json::from_str(&json).unwrap();
        assert_eq!(deser.discovered, 5);
        assert_eq!(deser.registered, 3);
        assert_eq!(deser.already_tracked, 2);
    }
}
