use std::{
    cell::{Cell, RefCell},
    fmt,
    fs::{self, OpenOptions},
    io::{self, Read},
    os::unix::{self, fs::PermissionsExt},
    path::{self, Path, PathBuf},
};

use crate::{error::NarError, parser};

/// Decoder that can extract the contents of NAR files.
pub struct Decoder<R: Read> {
    inner: DecoderInner<R>,
}

// This struct uses `Cell` and `RefCell` so that we can modify it
// behind a read-only reference.  This is necessary if we want to
// implement the standard `Iterator` interface.
//
// We need to track the `pos` in the stream so that we can properly
// skip bytes when the user doesn't consume a file.
/// Internal state for the `Decoder`.  This is only exposed because
/// it's referenced in a type.
pub struct DecoderInner<R> {
    pos: Cell<u64>,
    reader: RefCell<R>,
}

/// Iterator over the entries in the archive.
pub struct Entries<'a, R: Read> {
    decoder: &'a Decoder<R>,

    // What we're currently doing.
    current_activity: CurrentActivity,
}

/// A single entry in a NAR file.
#[derive(Debug)]
pub struct Entry<'a, R> {
    /// The path to the entry in the archive. The top-level entry
    /// doesn't have a path, so the first entry will have `path =
    /// None`.  This `path` is the full path in the archive with all
    /// parent paths combined. I.e. this is "some/subdir/my-file", and
    /// not just "my-file".
    pub path: Option<PathBuf>,

    /// The contents of the entry.
    pub content: Content<'a, R>,
}

/// The content of an [`Entry`] emitted by [`Decoder`].
pub enum Content<'a, R> {
    /// A directory.  Its children will follow as separate
    /// [`File`](Self::File) entries.
    Directory,

    /// A symlink with a given path. The NAR format imposes no
    /// constraints on `target`, so this symlink could point to
    /// anywhere.
    Symlink { target: PathBuf },

    /// A file, either plain or executable, with the given contents.
    /// The `data` field is a struct implementing [`std::io::Read`],
    /// so it can be read like any file.  You *must* either read
    /// `data` before calling [`Entries::next`] on the iterator, or
    /// not read it all.  Attempting to read `data` after calling
    /// [`Entries::next`] is undefined behaviour, and will almost
    /// certainly return garbage data.
    File {
        executable: bool,
        size: u64,
        offset: u64,

        /// May be used to extract the contents of this file.
        data: io::Take<&'a DecoderInner<R>>,
    },
}

impl<'a, R> Content<'a, R> {
    /// Returns `true` if the content is [`Directory`].
    ///
    /// [`Directory`]: Content::Directory
    pub fn is_directory(&self) -> bool {
        matches!(self, Self::Directory)
    }

    /// Returns `true` if the content is [`Symlink`].
    ///
    /// [`Symlink`]: Content::Symlink
    pub fn is_symlink(&self) -> bool {
        matches!(self, Self::Symlink { .. })
    }

    /// Returns `true` if the content is [`File`].
    ///
    /// [`File`]: Content::File
    pub fn is_file(&self) -> bool {
        matches!(self, Self::File { .. })
    }
}

#[derive(Debug)]
enum CurrentActivity {
    Finished,
    ParsingTopLevel,
    ParsingContent { next: u64, path: PathBuf },
    ParsingDirectoryEntries { path: PathBuf },
}

impl<R: Read> Decoder<R> {
    //! Create a new decoder over the given [`Read`](std::io::Read)er.
    //! Consider wrapping it in a [`std::io::BufReader`] for
    //! performance.
    pub fn new(reader: R) -> Result<Self, NarError> {
        let inner = DecoderInner {
            pos: Cell::new(0),
            reader: RefCell::new(reader),
        };
        parser::expect_str(&inner, "nix-archive-1")?;
        Ok(Self { inner })
    }

    /// Construct an iterator over the entries in this archive.
    ///
    /// You must consider each entry within an archive in sequence. If
    /// entries are processed out of sequence (from what the iterator
    /// returns), then the contents read for each entry may be
    /// corrupted.
    pub fn entries(&self) -> Result<Entries<R>, NarError> {
        Entries::new(self)
    }

    /// Unpacks the contents of the NAR file to the given destination
    /// (which must not already exist).
    ///
    /// This operation will not not write files outside of the path
    /// specified by `dst`. Files in the archive which have a `..` in
    /// their path are skipped during the unpacking process.
    ///
    /// No attempt is made to validate the targets of symlinks.
    ///
    /// If the NAR file is invalid, this function returns an error.
    /// For instance, this happens if an [`Entry`]'s parent doesn't
    /// exist or is not a directory.
    pub fn unpack<P: AsRef<Path>>(&self, dst: P) -> Result<(), NarError> {
        let dst = dst.as_ref();
        if fs::symlink_metadata(dst).is_ok() {
            return Err(NarError::UnpackError(format!(
                "Unpack destination already exists: {}. Delete it first.",
                dst.display()
            )));
        }
        for entry in self.entries()? {
            let entry = entry?;
            match &entry.path {
                None => {}
                Some(path) => {
                    if path
                        .components()
                        .any(|c| !matches!(c, path::Component::Normal(_)))
                    {
                        continue;
                    }
                }
            }
            let dst_path = entry
                .path
                .map(|path| dst.join(path))
                .unwrap_or_else(|| dst.to_path_buf());
            macro_rules! assert_parent_is_dir {
                ($dst_path:ident) => {
                    if let Some(parent) = dst_path.parent() {
                        if !parent.is_dir() {
                            return Err(NarError::UnpackError(format!(
                                "Entry {} has a parent which is not a directory",
                                dst_path.display()
                            )));
                        }
                    }
                };
            }
            match entry.content {
                Content::Directory => {
                    fs::create_dir(dst_path)?;
                }
                Content::Symlink { target } => {
                    assert_parent_is_dir!(dst_path);
                    unix::fs::symlink(target, dst_path)?;
                }
                Content::File {
                    executable,
                    size: _,
                    offset: _,
                    mut data,
                } => {
                    assert_parent_is_dir!(dst_path);
                    let mut file = OpenOptions::new()
                        .read(true)
                        .write(true)
                        .create_new(true)
                        .open(dst_path)?;
                    io::copy(&mut data, &mut file)?;
                    let mut perms = file.metadata()?.permissions();
                    perms.set_mode(if executable { 0o555 } else { 0o444 });
                    file.set_permissions(perms)?;
                }
            }
        }
        Ok(())
    }
}

impl<'a, R: Read> Read for &'a DecoderInner<R> {
    fn read(&mut self, into: &mut [u8]) -> io::Result<usize> {
        let i = self.reader.borrow_mut().read(into)?;
        self.pos.set(self.pos.get() + i as u64);
        Ok(i)
    }
}

impl<'a, R: Read> Entries<'a, R> {
    fn new(decoder: &'a Decoder<R>) -> Result<Self, NarError> {
        let decoder_pos = decoder.inner.pos.get();
        // The 24 comes from the `nar-archive-1` (8 for the length +
        // 12 rounded up to 16 for the text) that starts every NAR
        // file.
        if decoder_pos != 24 {
            return Err(NarError::ApiError(format!(
                "Can only call `entries` on a new `Decoder`. This one is at position {decoder_pos}."
            )));
        }
        Ok(Self {
            decoder,
            current_activity: CurrentActivity::ParsingTopLevel,
        })
    }

    fn handle_parse_regular(
        &mut self,
        path: Option<PathBuf>,
        executable: bool,
        size: u64,
    ) -> Entry<'a, R> {
        let size_rounded_up = (size + 7) & !7;
        self.current_activity = CurrentActivity::ParsingContent {
            next: self.decoder.inner.pos.get() + size_rounded_up,
            path: path.clone().unwrap_or_else(PathBuf::new),
        };
        Entry {
            path,
            content: Content::File {
                executable,
                size,
                offset: self.decoder.inner.pos.get(),
                data: self.decoder.inner.take(size),
            },
        }
    }

    fn next_or_err(&mut self) -> Result<Option<Entry<'a, R>>, NarError> {
        use parser::{Node as N, ParseResult as PR};
        use CurrentActivity as CA;
        match self.current_activity {
            CA::Finished => Ok(None),
            CA::ParsingTopLevel => match parser::parse_next(&self.decoder.inner)? {
                PR::Node(N::Regular { executable, size }) => {
                    Ok(Some(self.handle_parse_regular(None, executable, size)))
                }
                PR::Node(N::Symlink { target }) => {
                    self.current_activity = CA::Finished;
                    Ok(Some(Entry {
                        path: None,
                        content: Content::Symlink {
                            target: target.into(),
                        },
                    }))
                }
                PR::Node(N::Directory) => {
                    self.current_activity = CA::ParsingDirectoryEntries {
                        path: PathBuf::new(),
                    };
                    Ok(Some(Entry {
                        path: None,
                        content: Content::Directory,
                    }))
                }
                PR::DirectoryEntry(path, _) => Err(NarError::ParseError(format!(
                    "got unexpected directory entry at top-level: '{}'",
                    path.display()
                ))),
                PR::ParenClose => {
                    self.current_activity = CA::Finished;
                    Ok(None)
                }
            },
            CA::ParsingDirectoryEntries { path: ref dir_path } => {
                let dir_path = dir_path.to_path_buf();
                match parser::parse_next(&self.decoder.inner)? {
                    PR::Node(
                        node @ (N::Regular { .. } | N::Symlink { .. } | N::Directory),
                    ) => Err(NarError::ParseError(format!(
                        "got unexpected {} node at while parsing directory '{}'",
                        node.variant_name(),
                        dir_path.display()
                    ))),
                    PR::DirectoryEntry(path, node) => {
                        let path = dir_path.join(path);
                        match node {
                            N::Regular { executable, size } => Ok(Some(
                                self.handle_parse_regular(Some(path), executable, size),
                            )),
                            N::Symlink { target } => {
                                // Skip the closing parentheses of the directory entry
                                parser::parse_paren_close(&self.decoder.inner)?;
                                Ok(Some(Entry {
                                    path: Some(path),
                                    content: Content::Symlink {
                                        target: target.into(),
                                    },
                                }))
                            }
                            N::Directory => {
                                self.current_activity =
                                    CA::ParsingDirectoryEntries { path: path.clone() };
                                Ok(Some(Entry {
                                    path: Some(path),
                                    content: Content::Directory,
                                }))
                            }
                        }
                    }
                    PR::ParenClose => {
                        if let Some(parent) = dir_path.parent() {
                            // Skip the closing parentheses of the directory entry
                            parser::parse_paren_close(&self.decoder.inner)?;
                            self.current_activity = CA::ParsingDirectoryEntries {
                                path: parent.to_path_buf(),
                            };
                            self.next_or_err()
                        } else {
                            self.current_activity = CA::Finished;
                            Ok(None)
                        }
                    }
                }
            }
            CA::ParsingContent { next, ref path } => {
                // Skip any remaining bytes in the current file.
                skip_bytes(&self.decoder.inner, next - self.decoder.inner.pos.get())?;
                // Skip the closing parentheses of the node
                parser::parse_paren_close(&self.decoder.inner)?;
                if let Some(parent) = path.parent() {
                    // Skip the closing parentheses of the directory entry
                    parser::parse_paren_close(&self.decoder.inner)?;
                    self.current_activity = CA::ParsingDirectoryEntries {
                        path: parent.to_path_buf(),
                    };
                    self.next_or_err()
                } else {
                    self.current_activity = CA::Finished;
                    Ok(None)
                }
            }
        }
    }
}

impl<'a, R: Read> Iterator for Entries<'a, R> {
    type Item = Result<Entry<'a, R>, NarError>;

    fn next(&mut self) -> Option<Self::Item> {
        match self.next_or_err() {
            Err(err) => {
                self.current_activity = CurrentActivity::Finished;
                Some(Err(err))
            }
            Ok(None) => None,
            Ok(Some(res)) => Some(Ok(res)),
        }
    }
}

impl<R> fmt::Debug for Content<'_, R> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Content::Directory => f.write_str("Directory"),
            Content::Symlink { target } => {
                f.debug_struct("Symlink").field("target", target).finish()
            }
            Content::File {
                executable,
                size,
                offset,
                data: _,
            } => f
                .debug_struct("File")
                .field("executable", executable)
                .field("size", size)
                .field("offset", offset)
                .finish(),
        }
    }
}

fn skip_bytes<R: Read>(
    mut decoder_inner: &DecoderInner<R>,
    mut bytes_to_skip: u64,
) -> Result<(), NarError> {
    if bytes_to_skip > 0 {
        use std::cmp;
        while bytes_to_skip > 0 {
            let mut buf = [0u8; 4096 * 8];
            let n = cmp::min(bytes_to_skip, buf.len() as u64);
            match decoder_inner
                .read(&mut buf[..n as usize])
                .map_err(Into::<NarError>::into)?
            {
                0 => {
                    return Err(NarError::ParseError(
                        "unexpected EOF during skip".to_string(),
                    ));
                }
                n => {
                    bytes_to_skip -= n as u64;
                }
            }
        }
    }
    Ok(())
}

impl<'a, R> Entry<'a, R> {
    pub fn abs_path(&self) -> PathBuf {
        let mut p = Path::new("/").to_path_buf();
        if let Some(ref path) = self.path {
            p.push(path);
        }
        p
    }
}
