// Copyright 2018-2026 the Deno authors. MIT license.

use std::borrow::Cow;
use std::cell::RefCell;
use std::fmt::Formatter;
use std::io;
use std::path::Path;
#[cfg(unix)]
use std::process::Stdio as StdStdio;
use std::rc::Rc;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;

use deno_core::BufMutView;
use deno_core::BufView;
use deno_core::CancelHandle;
use deno_core::CancelTryFuture;
use deno_core::Canceled;
use deno_core::OpState;
use deno_core::ResourceHandleFd;
use deno_core::ResourceId;
use deno_core::error::ResourceError;
use deno_error::JsErrorBox;
use deno_permissions::PermissionCheckError;
#[cfg(windows)]
use deno_subprocess_windows::Stdio as StdStdio;
use tokio::task::JoinError;

#[derive(Debug, deno_error::JsError)]
pub enum FsError {
  #[class(inherit)]
  Io(io::Error),
  #[class("Busy")]
  FileBusy,
  #[class(not_supported)]
  NotSupported,
  #[class(inherit)]
  PermissionCheck(PermissionCheckError),
  #[class(inherit)]
  JoinError(JoinError),
}

impl std::fmt::Display for FsError {
  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
    match self {
      FsError::Io(err) => std::fmt::Display::fmt(err, f),
      FsError::FileBusy => f.write_str("file busy"),
      FsError::NotSupported => f.write_str("not supported"),
      FsError::PermissionCheck(err) => std::fmt::Display::fmt(err, f),
      FsError::JoinError(err) => std::fmt::Display::fmt(err, f),
    }
  }
}

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

impl FsError {
  pub fn kind(&self) -> io::ErrorKind {
    match self {
      Self::Io(err) => err.kind(),
      Self::FileBusy => io::ErrorKind::Other,
      Self::NotSupported => io::ErrorKind::Other,
      Self::PermissionCheck(e) => e.kind(),
      Self::JoinError(_) => io::ErrorKind::Other,
    }
  }

  pub fn into_io_error(self) -> io::Error {
    match self {
      FsError::Io(err) => err,
      FsError::FileBusy => io::Error::new(self.kind(), "file busy"),
      FsError::NotSupported => io::Error::new(self.kind(), "not supported"),
      FsError::PermissionCheck(err) => err.into_io_error(),
      FsError::JoinError(ref err) => {
        io::Error::new(self.kind(), format!("join error: {err}"))
      }
    }
  }
}

impl From<io::Error> for FsError {
  fn from(err: io::Error) -> Self {
    Self::Io(err)
  }
}

impl From<io::ErrorKind> for FsError {
  fn from(err: io::ErrorKind) -> Self {
    Self::Io(err.into())
  }
}

impl From<PermissionCheckError> for FsError {
  fn from(err: PermissionCheckError) -> Self {
    Self::PermissionCheck(err)
  }
}

impl From<JoinError> for FsError {
  fn from(err: JoinError) -> Self {
    Self::JoinError(err)
  }
}

impl From<Canceled> for FsError {
  fn from(err: Canceled) -> Self {
    Self::Io(err.into())
  }
}

pub type FsResult<T> = Result<T, FsError>;

pub struct FsStat {
  pub is_file: bool,
  pub is_directory: bool,
  pub is_symlink: bool,
  pub size: u64,

  pub mtime: Option<i64>,
  pub atime: Option<i64>,
  pub birthtime: Option<i64>,
  pub ctime: Option<i64>,

  pub dev: u64,
  pub ino: Option<u64>,
  pub mode: u32,
  pub nlink: Option<u64>,
  pub uid: u32,
  pub gid: u32,
  pub rdev: u64,
  pub blksize: u64,
  pub blocks: Option<u64>,
  pub is_block_device: bool,
  pub is_char_device: bool,
  pub is_fifo: bool,
  pub is_socket: bool,
}

/// File system statistics as returned by `statfs(2)` (or `GetDiskFreeSpaceW`
/// on Windows). Mirrors the result of Node's `fs.statfs`.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct FsStatFs {
  pub typ: u64,
  pub bsize: u64,
  pub blocks: u64,
  pub bfree: u64,
  pub bavail: u64,
  pub files: u64,
  pub ffree: u64,
}

impl FsStat {
  pub fn from_std(metadata: std::fs::Metadata) -> Self {
    macro_rules! unix_some_or_none {
      ($member:ident) => {{
        #[cfg(unix)]
        {
          use std::os::unix::fs::MetadataExt;
          Some(metadata.$member())
        }
        #[cfg(not(unix))]
        {
          None
        }
      }};
    }

    macro_rules! unix_or_zero {
      ($member:ident) => {{
        #[cfg(unix)]
        {
          use std::os::unix::fs::MetadataExt;
          metadata.$member()
        }
        #[cfg(not(unix))]
        {
          0
        }
      }};
    }

    macro_rules! unix_or_false {
      ($member:ident) => {{
        #[cfg(unix)]
        {
          use std::os::unix::fs::FileTypeExt;
          metadata.file_type().$member()
        }
        #[cfg(not(unix))]
        {
          false
        }
      }};
    }

    #[inline(always)]
    fn to_msec(maybe_time: Result<SystemTime, io::Error>) -> Option<i64> {
      match maybe_time {
        Ok(time) => {
          let ms = match time.duration_since(UNIX_EPOCH) {
            Ok(d) => d.as_millis() as i64,
            // Pre-epoch: negate the duration
            Err(e) => -(e.duration().as_millis() as i64),
          };
          Some(ms)
        }
        Err(_) => None,
      }
    }

    #[inline(always)]
    fn get_ctime(ctime_secs: i64) -> Option<i64> {
      if ctime_secs != 0 {
        // ctime is seconds since epoch; convert to milliseconds
        Some(ctime_secs * 1000)
      } else {
        None
      }
    }

    Self {
      is_file: metadata.is_file(),
      is_directory: metadata.is_dir(),
      is_symlink: metadata.file_type().is_symlink(),
      size: metadata.len(),

      mtime: to_msec(metadata.modified()),
      atime: to_msec(metadata.accessed()),
      birthtime: to_msec(metadata.created()),
      ctime: get_ctime(unix_or_zero!(ctime)),

      dev: unix_or_zero!(dev),
      ino: unix_some_or_none!(ino),
      mode: unix_or_zero!(mode),
      nlink: unix_some_or_none!(nlink),
      uid: unix_or_zero!(uid),
      gid: unix_or_zero!(gid),
      rdev: unix_or_zero!(rdev),
      blksize: unix_or_zero!(blksize),
      blocks: unix_some_or_none!(blocks),
      is_block_device: unix_or_false!(is_block_device),
      is_char_device: unix_or_false!(is_char_device),
      is_fifo: unix_or_false!(is_fifo),
      is_socket: unix_or_false!(is_socket),
    }
  }
}

#[async_trait::async_trait(?Send)]
pub trait File {
  /// Provides the path of the file, which is used for checking
  /// metadata permission updates.
  fn maybe_path(&self) -> Option<&Path>;

  fn read_sync(self: Rc<Self>, buf: &mut [u8]) -> FsResult<usize>;
  async fn read(self: Rc<Self>, limit: usize) -> FsResult<BufView> {
    let buf = BufMutView::new(limit);
    let (nread, mut buf) = self.read_byob(buf).await?;
    buf.truncate(nread);
    Ok(buf.into_view())
  }
  async fn read_byob(
    self: Rc<Self>,
    buf: BufMutView,
  ) -> FsResult<(usize, BufMutView)>;

  fn write_sync(self: Rc<Self>, buf: &[u8]) -> FsResult<usize>;
  async fn write(
    self: Rc<Self>,
    buf: BufView,
  ) -> FsResult<deno_core::WriteOutcome>;

  fn write_all_sync(self: Rc<Self>, buf: &[u8]) -> FsResult<()>;
  async fn write_all(self: Rc<Self>, buf: BufView) -> FsResult<()>;

  fn read_all_sync(self: Rc<Self>) -> FsResult<Cow<'static, [u8]>>;
  async fn read_all_async(self: Rc<Self>) -> FsResult<Cow<'static, [u8]>>;

  fn chmod_sync(self: Rc<Self>, pathmode: u32) -> FsResult<()>;
  async fn chmod_async(self: Rc<Self>, mode: u32) -> FsResult<()>;

  fn chown_sync(
    self: Rc<Self>,
    uid: Option<u32>,
    gid: Option<u32>,
  ) -> FsResult<()>;
  async fn chown_async(
    self: Rc<Self>,
    uid: Option<u32>,
    gid: Option<u32>,
  ) -> FsResult<()>;

  fn seek_sync(self: Rc<Self>, pos: io::SeekFrom) -> FsResult<u64>;
  async fn seek_async(self: Rc<Self>, pos: io::SeekFrom) -> FsResult<u64>;

  fn datasync_sync(self: Rc<Self>) -> FsResult<()>;
  async fn datasync_async(self: Rc<Self>) -> FsResult<()>;

  fn sync_sync(self: Rc<Self>) -> FsResult<()>;
  async fn sync_async(self: Rc<Self>) -> FsResult<()>;

  fn stat_sync(self: Rc<Self>) -> FsResult<FsStat>;
  async fn stat_async(self: Rc<Self>) -> FsResult<FsStat>;

  fn lock_sync(self: Rc<Self>, exclusive: bool) -> FsResult<()>;
  async fn lock_async(self: Rc<Self>, exclusive: bool) -> FsResult<()>;

  fn try_lock_sync(self: Rc<Self>, exclusive: bool) -> FsResult<bool>;
  async fn try_lock_async(self: Rc<Self>, exclusive: bool) -> FsResult<bool>;

  fn unlock_sync(self: Rc<Self>) -> FsResult<()>;
  async fn unlock_async(self: Rc<Self>) -> FsResult<()>;

  fn truncate_sync(self: Rc<Self>, len: u64) -> FsResult<()>;
  async fn truncate_async(self: Rc<Self>, len: u64) -> FsResult<()>;

  fn utime_sync(
    self: Rc<Self>,
    atime_secs: i64,
    atime_nanos: u32,
    mtime_secs: i64,
    mtime_nanos: u32,
  ) -> FsResult<()>;
  async fn utime_async(
    self: Rc<Self>,
    atime_secs: i64,
    atime_nanos: u32,
    mtime_secs: i64,
    mtime_nanos: u32,
  ) -> FsResult<()>;

  /// Read at a position without moving the file cursor (pread).
  fn read_at_sync(
    self: Rc<Self>,
    buf: &mut [u8],
    position: u64,
  ) -> FsResult<usize>;
  /// Async pread -- runs on a blocking thread.
  async fn read_at_async(
    self: Rc<Self>,
    buf: BufMutView,
    position: u64,
  ) -> FsResult<(usize, BufMutView)>;
  /// Write at a position without moving the file cursor (pwrite).
  fn write_at_sync(
    self: Rc<Self>,
    buf: &[u8],
    position: u64,
  ) -> FsResult<usize>;

  // lower level functionality
  fn as_stdio(self: Rc<Self>) -> FsResult<StdStdio>;
  fn backing_fd(self: Rc<Self>) -> Option<ResourceHandleFd>;
  fn try_clone_inner(self: Rc<Self>) -> FsResult<Rc<dyn File>>;
}

pub struct FileResource {
  name: String,
  file: Rc<dyn File>,
  /// Cancels pending read operations when the resource is closed.
  /// Used so streams backed by a file (especially pipes / FIFOs whose
  /// reads can block indefinitely) can be cancelled — see
  /// <https://github.com/denoland/deno/issues/21186>.
  cancel_handle: RefCell<Rc<CancelHandle>>,
}

impl FileResource {
  pub fn new(file: Rc<dyn File>, name: String) -> Self {
    Self {
      name,
      file,
      cancel_handle: RefCell::new(CancelHandle::new_rc()),
    }
  }

  fn cancel_handle(&self) -> Rc<CancelHandle> {
    self.cancel_handle.borrow().clone()
  }

  fn cancel_read_ops(&self) {
    self.cancel_handle.replace(CancelHandle::new_rc()).cancel();
  }

  fn with_resource<F, R>(
    state: &OpState,
    rid: ResourceId,
    f: F,
  ) -> Result<R, JsErrorBox>
  where
    F: FnOnce(Rc<FileResource>) -> Result<R, JsErrorBox>,
  {
    let resource = state
      .resource_table
      .get::<FileResource>(rid)
      .map_err(JsErrorBox::from_err)?;
    f(resource)
  }

  pub fn get_file(
    state: &OpState,
    rid: ResourceId,
  ) -> Result<Rc<dyn File>, ResourceError> {
    let resource = state.resource_table.get::<FileResource>(rid)?;
    Ok(resource.file())
  }

  pub fn with_file<F, R>(
    state: &OpState,
    rid: ResourceId,
    f: F,
  ) -> Result<R, JsErrorBox>
  where
    F: FnOnce(Rc<dyn File>) -> Result<R, JsErrorBox>,
  {
    Self::with_resource(state, rid, |r| f(r.file.clone()))
  }

  pub fn file(&self) -> Rc<dyn File> {
    self.file.clone()
  }
}

impl deno_core::Resource for FileResource {
  fn name(&self) -> Cow<'_, str> {
    Cow::Borrowed(&self.name)
  }

  fn read(self: Rc<Self>, limit: usize) -> deno_core::AsyncResult<BufView> {
    let cancel_handle = self.cancel_handle();
    Box::pin(async move {
      self
        .file
        .clone()
        .read(limit)
        .try_or_cancel(cancel_handle)
        .await
        .map_err(JsErrorBox::from_err)
    })
  }

  fn read_byob(
    self: Rc<Self>,
    buf: BufMutView,
  ) -> deno_core::AsyncResult<(usize, BufMutView)> {
    let cancel_handle = self.cancel_handle();
    Box::pin(async move {
      self
        .file
        .clone()
        .read_byob(buf)
        .try_or_cancel(cancel_handle)
        .await
        .map_err(JsErrorBox::from_err)
    })
  }

  fn write(
    self: Rc<Self>,
    buf: BufView,
  ) -> deno_core::AsyncResult<deno_core::WriteOutcome> {
    Box::pin(async move {
      self
        .file
        .clone()
        .write(buf)
        .await
        .map_err(JsErrorBox::from_err)
    })
  }

  fn write_all(self: Rc<Self>, buf: BufView) -> deno_core::AsyncResult<()> {
    Box::pin(async move {
      self
        .file
        .clone()
        .write_all(buf)
        .await
        .map_err(JsErrorBox::from_err)
    })
  }

  fn read_byob_sync(
    self: Rc<Self>,
    data: &mut [u8],
  ) -> Result<usize, JsErrorBox> {
    self
      .file
      .clone()
      .read_sync(data)
      .map_err(JsErrorBox::from_err)
  }

  fn write_sync(self: Rc<Self>, data: &[u8]) -> Result<usize, JsErrorBox> {
    self
      .file
      .clone()
      .write_sync(data)
      .map_err(JsErrorBox::from_err)
  }

  fn backing_fd(self: Rc<Self>) -> Option<ResourceHandleFd> {
    self.file.clone().backing_fd()
  }

  fn close(self: Rc<Self>) {
    // Cancel any pending read operations. Reads on pipes / FIFOs can
    // block indefinitely; without this, a `ReadableStream` backed by
    // such a file could never have its `cancel()` promise resolve while
    // a read is outstanding.
    self.cancel_read_ops();
  }

  fn cancel_read_ops(self: Rc<Self>) {
    FileResource::cancel_read_ops(&self);
  }
}
