use crate::shapes::{FilePath, GlobPath, PortablePath};
use crate::workspace::*;
use crate::{config_enum, config_struct, config_unit_enum};
use moon_common::Id;
use rustc_hash::FxHashMap;
use schematic::{Config, ConfigEnum, PathSegment, ValidateError, env, validate};
use semver::VersionReq;

// We can't use serde based types in the enum below to handle validation,
// as serde fails to parse correctly. So we must manually validate here.
fn validate_projects<D, C>(
    projects: &PartialWorkspaceProjects,
    _data: &D,
    _ctx: &C,
    _finalize: bool,
) -> Result<(), ValidateError> {
    match projects {
        PartialWorkspaceProjects::Both(cfg) => {
            if let Some(globs) = &cfg.globs {
                for (i, g) in globs.iter().enumerate() {
                    GlobPath::parse_relative(g).map_err(|error| {
                        ValidateError::with_segments(
                            error.to_string(),
                            [PathSegment::Key("globs".to_owned()), PathSegment::Index(i)],
                        )
                    })?;
                }
            }

            if let Some(sources) = &cfg.sources {
                for (k, v) in sources {
                    FilePath::parse_relative(v).map_err(|error| {
                        ValidateError::with_segments(
                            error.to_string(),
                            [
                                PathSegment::Key("sources".to_owned()),
                                PathSegment::Key(k.to_string()),
                            ],
                        )
                    })?;
                }
            }
        }
        PartialWorkspaceProjects::Globs(globs) => {
            for (i, g) in globs.iter().enumerate() {
                GlobPath::parse_relative(g).map_err(|error| {
                    ValidateError::with_segments(error.to_string(), [PathSegment::Index(i)])
                })?;
            }
        }
        PartialWorkspaceProjects::Sources(sources) => {
            for (k, v) in sources {
                FilePath::parse_relative(v).map_err(|error| {
                    ValidateError::with_segments(
                        error.to_string(),
                        [PathSegment::Key(k.to_string())],
                    )
                })?;
            }
        }
    };

    Ok(())
}

config_unit_enum!(
    /// The project identifier format for glob located projects.
    #[derive(ConfigEnum)]
    pub enum WorkspaceProjectGlobFormat {
        /// The project directory name.
        #[default]
        DirName,

        /// The relative path from the workspace root to the project root.
        SourcePath,
    }
);

config_struct!(
    /// Configures projects in the workspace, using both globs and explicit source paths.
    #[derive(Config)]
    pub struct WorkspaceProjectsConfig {
        /// A list of glob patterns in which to locate project directories.
        /// Can be suffixed with a `moon.*` config file to only find distinct projects.
        pub globs: Vec<String>,

        /// The project identifier format for glob located projects.
        /// @since 2.0.0
        pub glob_format: WorkspaceProjectGlobFormat,

        /// A map of project identifiers to relative file paths to each project directory.
        pub sources: FxHashMap<Id, String>,
    }
);

config_enum!(
    /// Configures projects in the workspace.
    #[derive(Config)]
    #[serde(
        untagged,
        expecting = "expected a list of globs, a map of projects, or both"
    )]
    pub enum WorkspaceProjects {
        /// Using both glob patterns and file source paths.
        #[setting(nested)]
        Both(WorkspaceProjectsConfig),

        /// Using glob patterns. Suffix with `moon.*` to be distinct.
        Globs(Vec<String>),

        /// Using a map of identifiers to file source paths.
        #[setting(default)]
        Sources(FxHashMap<Id, String>),
    }
);

config_struct!(
    /// Configures all aspects of the moon workspace.
    /// Docs: https://moonrepo.dev/docs/config/workspace
    #[derive(Config)]
    pub struct WorkspaceConfig {
        #[setting(default = "./cache/schemas/workspace.json", rename = "$schema")]
        pub schema: String,

        /// Configures code ownership rules for generating a `CODEOWNERS` file.
        /// @since 1.8.0
        #[setting(nested)]
        pub codeowners: CodeownersConfig,

        /// Configures boundaries and constraints between projects.
        #[setting(nested)]
        pub constraints: ConstraintsConfig,

        /// The default/main project within the workspace. When a task is
        /// ran without a project, the default will be used.
        /// @since 2.0.0
        pub default_project: Option<Id>,

        /// Configures Docker integration for the workspace.
        /// @since 1.27.0
        #[setting(nested)]
        pub docker: DockerConfig,

        /// Configures experiments across the entire moon workspace.
        /// @since 1.11.0
        #[setting(nested)]
        pub experiments: ExperimentsConfig,

        /// Extends one or many workspace configuration file.
        /// Supports a relative file path or a secure URL.
        /// @since 1.12.0
        #[setting(extend, validate = validate::extends_from)]
        pub extends: Option<schematic::ExtendsFrom>,

        /// Configures the generator for scaffolding from templates.
        #[setting(nested)]
        pub generator: GeneratorConfig,

        /// Configures aspects of the content hashing engine.
        #[setting(nested)]
        pub hasher: HasherConfig,

        /// Configures how and where notifications are sent.
        #[setting(nested)]
        pub notifier: NotifierConfig,

        /// Configures aspects of the action pipeline.
        #[setting(nested)]
        pub pipeline: PipelineConfig,

        /// Configures all projects within the workspace to create a project graph.
        /// Accepts a list of globs, a mapping of projects to relative file paths,
        /// or both values.
        #[setting(nested, validate = validate_projects)]
        pub projects: WorkspaceProjects,

        /// Configures aspects of the remote service.
        #[setting(nested)]
        pub remote: RemoteConfig,

        /// Collects anonymous usage information, and checks for new moon versions.
        #[setting(default = true, env = "MOON_TELEMETRY", parse_env = env::parse_bool)]
        pub telemetry: bool,

        /// Configures the version control system (VCS). Also known as
        /// source code management (SCM).
        #[setting(nested)]
        pub vcs: VcsConfig,

        /// Requires a specific version of the `moon` binary.
        pub version_constraint: Option<VersionReq>,
    }
);
