//! The `Transport` trait and its companion types.
//!
//! `DirState` does all of its filesystem I/O through a
//! [`Transport`] implementation: read the dirstate file, write it
//! back, acquire a lock, stat a tracked file, read a symlink, list
//! a directory.  The pure crate never touches `std::fs` directly
//! (CLAUDE.md "Filesystem goes through Transport, not std::fs") so
//! the pyo3 adapter can supply a Python-file-backed transport, and
//! tests can use a `MemoryTransport` that models the filesystem in
//! a `HashMap`.

use super::LockState;

/// Stat result returned by [`Transport::lstat`].  Mirrors the subset of
/// `os.stat_result` fields that dirstate logic actually inspects:
/// mode (for kind + executable), size, mtime/ctime (for the cutoff
/// check), dev/ino (fed into `pack_stat`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StatInfo {
    pub mode: u32,
    pub size: u64,
    pub mtime: i64,
    pub ctime: i64,
    pub dev: u64,
    pub ino: u64,
}

impl StatInfo {
    /// Whether `mode` indicates a regular file (S_IFREG).
    pub fn is_file(&self) -> bool {
        self.mode & 0o170000 == 0o100000
    }
    /// Whether `mode` indicates a directory (S_IFDIR).
    pub fn is_dir(&self) -> bool {
        self.mode & 0o170000 == 0o040000
    }
    /// Whether `mode` indicates a symlink (S_IFLNK).
    pub fn is_symlink(&self) -> bool {
        self.mode & 0o170000 == 0o120000
    }
}

/// One entry yielded by [`Transport::list_dir`] — mirrors the
/// per-child tuple Python's `DirReader.read_dir` returns.
#[derive(Debug, Clone)]
pub struct DirEntryInfo {
    /// The child's utf8 basename (no trailing slash).
    pub basename: Vec<u8>,
    /// Filesystem kind, or `None` for kinds dirstate doesn't track
    /// (block / char / socket / fifo).
    pub kind: Option<crate::osutils::Kind>,
    /// Stat info from `lstat` on the child.
    pub stat: StatInfo,
    /// Absolute path of the child on disk (utf8 bytes).
    pub abspath: Vec<u8>,
}

/// Errors returned by [`Transport`] operations.
///
/// Variants are coarse on purpose: callers generally either propagate
/// the error or match on `NotFound` / `LockContention`. I/O errors are
/// normalised into `(ErrorKind, String)` so the enum stays
/// `Clone + PartialEq + Eq` and tests can compare values directly.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TransportError {
    /// The backing file does not exist. Returned by `read_all` /
    /// `exists` / lock acquisition when there is nothing to open.
    NotFound(String),
    /// A lock was requested but another process already holds it, or
    /// the transport is already locked in an incompatible mode.
    LockContention(String),
    /// The caller tried to operate on an unlocked transport (read,
    /// write, or unlock without a prior `lock_read` / `lock_write`).
    NotLocked,
    /// The caller tried to acquire a second lock while one was still
    /// held. Dirstate's model is that you unlock before relocking;
    /// explicit rather than RAII.
    AlreadyLocked,
    /// Catch-all for I/O errors from the underlying store. The
    /// `(ErrorKind, message)` pair is preserved so callers can branch
    /// on kind without losing the original diagnostic.
    Io {
        kind: std::io::ErrorKind,
        message: String,
    },
    /// Catch-all for backend-specific failures that don't map to any
    /// of the above (typically wrapped Python exceptions on the pyo3
    /// adapter side).
    Other(String),
}

impl From<std::io::Error> for TransportError {
    fn from(e: std::io::Error) -> Self {
        if e.kind() == std::io::ErrorKind::NotFound {
            TransportError::NotFound(e.to_string())
        } else {
            TransportError::Io {
                kind: e.kind(),
                message: e.to_string(),
            }
        }
    }
}

impl std::fmt::Display for TransportError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TransportError::NotFound(p) => write!(f, "No such file: {}", p),
            TransportError::LockContention(p) => write!(f, "Lock contention: {}", p),
            TransportError::NotLocked => write!(f, "Transport is not locked"),
            TransportError::AlreadyLocked => write!(f, "Transport is already locked"),
            TransportError::Io { kind, message } => {
                write!(f, "I/O error ({:?}): {}", kind, message)
            }
            TransportError::Other(s) => write!(f, "Transport error: {}", s),
        }
    }
}

impl std::error::Error for TransportError {}

/// Decode a utf8-bytes path (as dirstate stores them) into a [`PathBuf`].
///
/// On Unix the bytes are used verbatim via `OsStr`; on other platforms
/// they are decoded as utf8.
#[cfg(unix)]
pub fn bytes_to_path(b: &[u8]) -> Result<std::path::PathBuf, TransportError> {
    use std::os::unix::ffi::OsStrExt;
    Ok(std::path::PathBuf::from(std::ffi::OsStr::from_bytes(b)))
}

#[cfg(not(unix))]
pub fn bytes_to_path(b: &[u8]) -> Result<std::path::PathBuf, TransportError> {
    String::from_utf8(b.to_vec())
        .map(std::path::PathBuf::from)
        .map_err(|e| TransportError::Other(e.to_string()))
}

/// Encode a [`Path`] back into the utf8-bytes form dirstate stores.
#[cfg(unix)]
pub fn path_to_bytes(p: &std::path::Path) -> Vec<u8> {
    use std::os::unix::ffi::OsStrExt;
    p.as_os_str().as_bytes().to_vec()
}

#[cfg(not(unix))]
pub fn path_to_bytes(p: &std::path::Path) -> Vec<u8> {
    p.to_string_lossy().into_owned().into_bytes()
}

/// Build a [`StatInfo`] from filesystem metadata.
pub fn stat_info_from_metadata(m: &std::fs::Metadata) -> Result<StatInfo, TransportError> {
    #[cfg(unix)]
    {
        use std::os::unix::fs::MetadataExt;
        Ok(StatInfo {
            mode: m.mode(),
            size: m.size(),
            mtime: m.mtime(),
            ctime: m.ctime(),
            dev: m.dev(),
            ino: m.ino(),
        })
    }
    #[cfg(not(unix))]
    {
        let _ = m;
        Err(TransportError::Other(
            "lstat unsupported on this platform".to_string(),
        ))
    }
}

fn kind_from_stat(stat: &StatInfo) -> Option<crate::osutils::Kind> {
    if stat.is_file() {
        Some(crate::osutils::Kind::File)
    } else if stat.is_dir() {
        Some(crate::osutils::Kind::Directory)
    } else if stat.is_symlink() {
        Some(crate::osutils::Kind::Symlink)
    } else {
        None
    }
}

/// `lstat(2)` on a dirstate path: stat without following symlinks.
///
/// Path-keyed and stateless, so both the real-filesystem and pyo3
/// transports share this implementation.
pub fn lstat_path(abspath: &[u8]) -> Result<StatInfo, TransportError> {
    let path = bytes_to_path(abspath)?;
    let metadata = std::fs::symlink_metadata(&path)?;
    stat_info_from_metadata(&metadata)
}

/// `readlink(2)` on a dirstate path, returning the target as utf8 bytes.
pub fn read_link_path(abspath: &[u8]) -> Result<Vec<u8>, TransportError> {
    let path = bytes_to_path(abspath)?;
    let target = std::fs::read_link(&path)?;
    Ok(path_to_bytes(&target))
}

/// Whether `abspath` is a directory carrying its own `.bzr/`, i.e. a
/// potential nested tree reference.
pub fn is_tree_reference_dir_path(abspath: &[u8]) -> Result<bool, TransportError> {
    if abspath.is_empty() {
        return Ok(false);
    }
    let path = bytes_to_path(abspath)?;
    Ok(path.join(".bzr").is_dir())
}

/// List a directory, returning one [`DirEntryInfo`] per child with its
/// `lstat` info filled in.
pub fn list_dir_path(abspath: &[u8]) -> Result<Vec<DirEntryInfo>, TransportError> {
    let path = bytes_to_path(abspath)?;
    let entries = std::fs::read_dir(&path)?;
    let mut out = Vec::new();
    for entry in entries {
        let entry = entry?;
        let name = entry.file_name();
        let basename = path_to_bytes(std::path::Path::new(&name));
        let metadata = entry.metadata()?;
        let stat = stat_info_from_metadata(&metadata)?;
        let kind = kind_from_stat(&stat);
        out.push(DirEntryInfo {
            basename,
            kind,
            stat,
            abspath: path_to_bytes(&entry.path()),
        });
    }
    Ok(out)
}

/// Single-file backing store for a [`DirState`].
///
/// Unlike `bazaar::transport::Transport` (the knit-side path-keyed
/// byte store), a dirstate transport represents exactly one file held
/// open across a lock.  The real-filesystem backend is a thin wrapper
/// over `std::fs`; tests use a `MemoryTransport`, and the pyo3 layer
/// uses a `PyFileTransport` that delegates to a Python file-like
/// object.  Operations:
///
/// * [`Transport::exists`] — whether the backing file exists.  Used
///   by `on_file` to decide whether to create a fresh dirstate.
/// * [`Transport::lock_read`] / [`Transport::lock_write`] — acquire
///   a lock on the backing file.  Explicit rather than RAII; the
///   caller must pair each lock with an `unlock`.  Re-locking while
///   already locked returns `AlreadyLocked`.
/// * [`Transport::unlock`] — release the current lock.
/// * [`Transport::lock_state`] — observe the current lock state.
/// * [`Transport::read_all`] — return the full file contents.
///   Requires a read or write lock.  The returned bytes are owned;
///   callers parse in memory (no streaming `readline` — the pure-Rust
///   `read_header` operates on a byte slice).
/// * [`Transport::write_all`] — replace the full file contents,
///   truncating any trailing bytes from the previous version.
///   Requires a write lock.  Implementations are expected to flush
///   before returning, but are not required to fdatasync — call
///   [`Transport::fdatasync`] for that.
/// * [`Transport::fdatasync`] — force the current contents to durable
///   storage.  Optional no-op for stores where fsync has no meaning
///   (e.g. in-memory tests); the trait method exists so
///   `DirState.save` can call it unconditionally.
///
/// The `&mut self` receivers are deliberate: every operation either
/// mutates the lock state, the file contents, or both.  Callers that
/// need shared access should wrap an implementation in their own
/// synchronisation primitive.
pub trait Transport {
    /// Whether the backing file exists. Does not require a lock.
    fn exists(&self) -> Result<bool, TransportError>;

    /// Acquire a read lock on the backing file. Returns
    /// `AlreadyLocked` if any lock is already held.
    fn lock_read(&mut self) -> Result<(), TransportError>;

    /// Acquire a write lock on the backing file. Returns
    /// `AlreadyLocked` if any lock is already held.
    fn lock_write(&mut self) -> Result<(), TransportError>;

    /// Release the current lock. Returns `NotLocked` if no lock was
    /// held.
    fn unlock(&mut self) -> Result<(), TransportError>;

    /// Current lock state, or `None` if no lock is held.
    fn lock_state(&self) -> Option<LockState>;

    /// Read the full contents of the backing file. Requires a read
    /// or write lock; returns `NotLocked` otherwise.
    fn read_all(&mut self) -> Result<Vec<u8>, TransportError>;

    /// Length of the backing file, in bytes. Used by the bisect path
    /// to know where the last record ends without pulling the whole
    /// file into memory. Requires a read or write lock; returns
    /// `NotLocked` otherwise. The default implementation reads the
    /// whole file and reports its length; concrete implementations
    /// should override with `fstat` or equivalent.
    #[allow(clippy::len_without_is_empty)]
    fn len(&mut self) -> Result<u64, TransportError> {
        Ok(self.read_all()?.len() as u64)
    }

    /// Read `len` bytes starting at `offset`. Used by the bisect path
    /// to avoid pulling the whole dirstate into memory just to probe a
    /// handful of locations. The default implementation reads the
    /// whole file via [`Self::read_all`] and slices; concrete
    /// implementations should override with a seek+read when possible.
    /// A read shorter than `len` is acceptable (e.g. when the requested
    /// range extends past EOF) — callers must tolerate short reads.
    fn read_at(&mut self, offset: u64, len: usize) -> Result<Vec<u8>, TransportError> {
        let all = self.read_all()?;
        let start = offset as usize;
        if start >= all.len() {
            return Ok(Vec::new());
        }
        let end = std::cmp::min(start + len, all.len());
        Ok(all[start..end].to_vec())
    }

    /// Replace the full contents of the backing file, truncating any
    /// trailing bytes from the previous version. Requires a write
    /// lock; returns `NotLocked` if no lock is held, and a generic
    /// error if only a read lock is held.
    fn write_all(&mut self, bytes: &[u8]) -> Result<(), TransportError>;

    /// Temporarily upgrade a read lock to a write lock. On success the
    /// transport's [`Transport::lock_state`] becomes
    /// `Some(LockState::Write)`. On failure (another reader holds the
    /// file) the read lock is preserved and the method returns
    /// `Ok(false)`. Returns `NotLocked` if no lock is held, or
    /// `AlreadyLocked` if a write lock is already held.
    ///
    /// The default implementation refuses the upgrade — backends that
    /// can't atomically switch lock modes (e.g. in-memory tests, the
    /// Python-file adapter) keep the read lock.
    fn upgrade_to_write_lock(&mut self) -> Result<bool, TransportError> {
        match self.lock_state() {
            None => Err(TransportError::NotLocked),
            Some(LockState::Write) => Err(TransportError::AlreadyLocked),
            Some(LockState::Read) => Ok(false),
        }
    }

    /// Inverse of [`Self::upgrade_to_write_lock`]. Downgrade a write
    /// lock previously obtained via `upgrade_to_write_lock` back to a
    /// read lock. Returns `NotLocked` if no lock is held, or a
    /// generic error if the current lock is not a write lock.
    ///
    /// The default implementation is a no-op when the lock is already
    /// a read lock and errors otherwise.
    fn downgrade_to_read_lock(&mut self) -> Result<(), TransportError> {
        match self.lock_state() {
            None => Err(TransportError::NotLocked),
            Some(LockState::Read) => Ok(()),
            Some(LockState::Write) => Err(TransportError::Other(
                "downgrade_to_read_lock not supported by this transport".into(),
            )),
        }
    }

    /// Force the current contents to durable storage. Implementations
    /// that have no meaningful fsync (in-memory tests, mocked
    /// backends) are free to make this a no-op; real filesystem
    /// implementations should call `fdatasync(2)` or the platform
    /// equivalent.
    fn fdatasync(&mut self) -> Result<(), TransportError>;

    /// Return the stat info for an absolute path in the working-tree
    /// filesystem that the dirstate is tracking (not the dirstate
    /// file itself).  `NoSuchFile` when the path is gone from disk.
    /// Required by `DirState::update_entry` / `process_entry`, which
    /// otherwise would couple the pure crate to `std::fs`.
    fn lstat(&self, abspath: &[u8]) -> Result<StatInfo, TransportError>;

    /// Return the target of the symlink at `abspath`.  `NoSuchFile`
    /// when the path is gone; a generic error when the path is not a
    /// symlink.
    fn read_link(&self, abspath: &[u8]) -> Result<Vec<u8>, TransportError>;

    /// Whether the directory at `abspath` is a nested tree reference
    /// (i.e. contains a `.bzr/` control directory).  Mirrors the
    /// per-format `_directory_is_tree_reference` hook on breezy's
    /// `WorkingTree`: the file format decides whether tree references
    /// can exist at all, and a concrete directory qualifies iff it
    /// carries its own `.bzr/`.  Consumers use this during
    /// `iter_changes` to flip the on-disk `directory` kind to
    /// `tree-reference` before handing the entry to
    /// [`DirState::process_entry`].
    ///
    /// Formats that don't support tree references should implement
    /// this as an unconditional `Ok(false)`.
    fn is_tree_reference_dir(&self, abspath: &[u8]) -> Result<bool, TransportError>;

    /// List the immediate children of directory `abspath`.  Used by
    /// the pure-crate `iter_changes` walker.  Returns a vector of
    /// per-child entries; the implementation does not guarantee any
    /// particular order — the walker sorts.
    ///
    /// Each entry carries the child's utf8 basename, its kind
    /// (`"file"`, `"directory"`, `"symlink"`, or `"tree-reference"`),
    /// its [`StatInfo`] (from an `lstat`), and the absolute path of
    /// the child on disk.  The walker re-uses the stat to avoid a
    /// second syscall inside `process_entry`.
    ///
    /// `NoSuchFile` when `abspath` does not exist or is not a
    /// directory.
    fn list_dir(&self, abspath: &[u8]) -> Result<Vec<DirEntryInfo>, TransportError>;
}
