use std::io;

use ini::Ini;
use turbopath::AbsoluteSystemPath;

pub const NPMRC_FILENAME: &str = ".npmrc";

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("Encountered error reading .npmrc: {0}")]
    Io(#[from] std::io::Error),
    #[error("Encountered error parsing .npmrc: {0}")]
    Ini(#[from] ini::Error),
}

/// Representation of .npmrc used by both npm and pnpm to configure behavior
/// The representation is intentionally incomplete and is only intended to
/// contain settings that affect the package graph.
#[derive(Debug, PartialEq, Eq, Clone, Default)]
pub struct NpmRc {
    /// Used by pnpm to determine whether dependencies
    /// declared without an explicit workspace protocol
    /// can target a workspace package.
    pub link_workspace_packages: Option<bool>,
    /// Used by pnpm to determine whether a single lockfile is used at the root
    /// or each workspace gets its own lockfile. Defaults to true.
    /// When false, pnpm creates a pnpm-lock.yaml in each workspace directory.
    pub shared_workspace_lockfile: Option<bool>,
}

impl NpmRc {
    pub fn from_file(repo_root: &AbsoluteSystemPath) -> Result<Self, Error> {
        let npmrc_path = repo_root.join_component(NPMRC_FILENAME);
        let npmrc = npmrc_path
            .read_existing_to_string()?
            .map(|contents| Self::from_reader(contents.as_bytes()))
            .transpose()?
            .unwrap_or_default();
        Ok(npmrc)
    }

    pub fn from_reader(mut reader: impl io::Read) -> Result<Self, Error> {
        let ini = Ini::read_from(&mut reader)?;
        Ok(Self::from_ini(ini))
    }

    // Private to avoid leaking the underlying ini parsing library we use
    fn from_ini(ini: Ini) -> Self {
        let link_workspace_packages = ini
            .get_from::<&str>(None, "link-workspace-packages")
            .and_then(parse_link_workspace_packages);

        let shared_workspace_lockfile = ini
            .get_from::<&str>(None, "shared-workspace-lockfile")
            .and_then(parse_bool);

        Self {
            link_workspace_packages,
            shared_workspace_lockfile,
        }
    }
}

fn parse_link_workspace_packages(value: &str) -> Option<bool> {
    match value {
        // "deep" changes the underlying linking strategy used by pnpm, but it still results
        // in workspace packages being used over npm packages
        "true" | "deep" => Some(true),
        "false" => Some(false),
        _ => None,
    }
}

fn parse_bool(value: &str) -> Option<bool> {
    match value {
        "true" => Some(true),
        "false" => Some(false),
        _ => None,
    }
}

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

    #[test]
    fn test_parse_empty_npmrc() {
        let empty = NpmRc::from_reader(b"".as_slice()).unwrap();
        assert_eq!(
            empty,
            NpmRc {
                link_workspace_packages: None,
                shared_workspace_lockfile: None,
            }
        );
    }

    #[test]
    fn test_example_pnpm_npmrc() {
        let contents = b"auto-install-peers=true
        enable-pre-post-scripts=true
        link-workspace-packages=false
        "
        .as_slice();
        let example = NpmRc::from_reader(contents).unwrap();
        assert_eq!(
            example,
            NpmRc {
                link_workspace_packages: Some(false),
                shared_workspace_lockfile: None,
            }
        );
    }

    #[test]
    fn test_shared_workspace_lockfile_false() {
        let contents = b"shared-workspace-lockfile=false".as_slice();
        let result = NpmRc::from_reader(contents).unwrap();
        assert_eq!(
            result,
            NpmRc {
                link_workspace_packages: None,
                shared_workspace_lockfile: Some(false),
            }
        );
    }

    #[test]
    fn test_shared_workspace_lockfile_true() {
        let contents = b"shared-workspace-lockfile=true".as_slice();
        let result = NpmRc::from_reader(contents).unwrap();
        assert_eq!(
            result,
            NpmRc {
                link_workspace_packages: None,
                shared_workspace_lockfile: Some(true),
            }
        );
    }
}
