//! Resolve a target (path or repo_id) to its on-disk storage.
use std::path::{Path, PathBuf};

use anyhow::{Context, Result, bail};
use ckg_core::RepoId;
use ckg_storage::{Manifest, Storage};

/// Open the storage for `target` (a filesystem path) or, if `target` is None,
/// the repo containing the current working directory.
///
/// Prints a `WARNING: re-index required` notice to stderr when the
/// per-repo DB carries the `needs_reindex` sentinel — set by the
/// schema-version auto-rebuild path. This catches the silent-empty-DB
/// case after a binary upgrade (#6 in re-review).
pub fn open_for(home: Option<PathBuf>, target: Option<&Path>) -> Result<(PathBuf, Storage)> {
    let base = crate::ckg_home(home)?;
    let path = match target {
        Some(p) => p.to_path_buf(),
        None => std::env::current_dir()?,
    };
    // #2: canonicalize via shared helper so writer (`ckg index`) and
    // reader (`ckg query`/etc.) produce the same key shape even when
    // canonicalize fails on one side. The lexical fallback collapses
    // `..`/`.` and absolutizes against cwd, matching what
    // `RepoId::from_path` does internally — keys converge.
    let canonical = canonicalize_or_lexical(&path);
    // Walk up to find a repo root listed in manifest, else fall back to deriving id.
    let manifest = Manifest::read(&base.join("manifest.json"))?;
    for ancestor in canonical.ancestors() {
        if let Some(entry) = manifest.repos.get(&ancestor.display().to_string()) {
            let id = RepoId::try_new(&entry.repo_id)?;
            // I2: open WITH canonical root verification. The recorded
            // path in the manifest is the source-of-truth for which
            // repo this RepoId belongs to — pass it through to the
            // storage layer so collisions are caught.
            let root = std::path::PathBuf::from(ancestor);
            let storage = Storage::open_at(id, &root, &base)
                .with_context(|| format!("opening storage for {}", entry.repo_id))?;
            warn_if_needs_reindex(&storage, ancestor);
            return Ok((base, storage));
        }
    }
    // Direct repo_id match (24 hex chars)? No canonical root available
    // here — fall back to the no-verify path. Acceptable: this branch
    // is for ad-hoc CLI exploration of a known repo_id.
    if let Some(name) = canonical.file_name().and_then(|n| n.to_str())
        && name.len() == 24
        && name.chars().all(|c| c.is_ascii_hexdigit())
    {
        // Already verified length + hex-chars above. Use `?` instead of
        // `expect` so a future `is_valid` tightening doesn't panic.
        let id = RepoId::try_new(name).map_err(|e| anyhow::anyhow!("{e}"))?;
        // CR-M6: warn the operator that this branch skips the canonical-
        // root check (I2). 24-hex repo_id collisions are 2^-96 in theory
        // but a user pasting a repo_id from an unrelated machine would
        // silently open the wrong DB.
        tracing::warn!(
            "opening repo by raw 24-hex repo_id ({}) — canonical-root verification skipped",
            name
        );
        let storage = Storage::open_unverified(id, &base)?;
        warn_if_needs_reindex(&storage, &canonical);
        return Ok((base.clone(), storage));
    }
    bail!(
        "no indexed repository found for {} (run `ckg index .` first)",
        canonical.display()
    );
}

/// Resolve `path` to a canonical-or-lexical PathBuf. Used by both the
/// indexer (`index.rs`) and the reader (this file) so the manifest
/// key-shape converges even when `Path::canonicalize` fails on one
/// side (e.g. a Docker bind-mount where the source path doesn't exist
/// inside the container). #2 fix.
pub(crate) fn canonicalize_or_lexical(path: &Path) -> PathBuf {
    if let Ok(c) = path.canonicalize() {
        return c;
    }
    // Lexical absolutize: cwd-join + `..`/`.` collapse, no syscalls.
    use std::path::Component;
    let abs: PathBuf = if path.is_absolute() {
        path.to_path_buf()
    } else {
        std::env::current_dir()
            .unwrap_or_else(|_| PathBuf::from("."))
            .join(path)
    };
    let mut out = PathBuf::new();
    for c in abs.components() {
        match c {
            Component::CurDir => {}
            Component::ParentDir => {
                out.pop();
            }
            other => out.push(other.as_os_str()),
        }
    }
    out
}

fn warn_if_needs_reindex(storage: &Storage, root: &Path) {
    if storage.needs_reindex() {
        eprintln!(
            "WARNING: schema was upgraded since this repo was last indexed. \
             Run `ckg index --clean {}` to repopulate the graph; current \
             read results will be empty.",
            root.display()
        );
    }
}
