use std::{
    env, fs,
    path::{Path, PathBuf},
    process::Command,
};

use dependency_graph::{DependencyGraph, Node, Step};
use serde::Deserialize;

use crate::{
    env::BuildEnv,
    error::{Error, PortError},
};

use super::ygglibc;

#[derive(Debug, Clone, Copy)]
pub enum BuildStep {
    Fetch,
    Compile,
    Install,
}

#[derive(Debug, Deserialize)]
pub struct PortBuildScripts {
    pub fetch: Option<PathBuf>,
    pub compile: Option<PathBuf>,
    pub install: Option<PathBuf>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct PortDependencies {
    pub build: Option<Vec<String>>,
    pub runtime: Option<Vec<String>>,
    pub c_compiler: Option<bool>,
}

#[derive(Debug, Deserialize)]
pub struct PortManifest {
    pub description: String,
    pub version: String,
    pub dependencies: Option<PortDependencies>,
    pub scripts: Option<PortBuildScripts>,
}

#[derive(Debug)]
pub struct NormalPort {
    pub package_name: String,
    pub manifest: PortManifest,
    pub manifest_dir: PathBuf,
    pub source_dir: PathBuf,
    pub build_dir: PathBuf,
    pub all_dependencies: Vec<String>,
}

#[derive(Debug)]
pub enum Port {
    Meta(MetaPort),
    Normal(NormalPort),
}

#[derive(Debug)]
pub enum MetaPort {
    Libc,
}

impl Node for Port {
    type DependencyType = String;

    fn matches(&self, dependency: &Self::DependencyType) -> bool {
        match self {
            Self::Meta(MetaPort::Libc) => dependency == "meta-libc",
            Self::Normal(port) => port.package_name == *dependency,
        }
    }

    fn dependencies(&self) -> &[Self::DependencyType] {
        // build + runtime dependencies
        match self {
            Self::Meta(MetaPort::Libc) => &[],
            Self::Normal(port) => &port.all_dependencies,
        }
    }
}

impl NormalPort {
    fn default_c_dependencies(name: &str) -> Vec<String> {
        let mut d = vec![];
        d.push("meta-llvm".into());
        d.push("meta-libc".into());
        if name != "compiler-rt" {
            d.push("compiler-rt".into());
        }
        if name != "compiler-rt" && name != "openlibm" {
            d.push("openlibm".into());
        }
        d
    }

    pub fn load(env: &BuildEnv, name: &str) -> Result<Self, PortError> {
        let manifest_dir = env.workspace_root.join("ports").join(name);
        let source_dir = env
            .workspace_root
            .join("userspace/target/ports")
            .join(name)
            .join("source");
        let build_dir = env
            .workspace_root
            .join("userspace/target")
            .join(env.arch.user_triple())
            .join("ports")
            .join(name)
            .join("build");
        let manifest_path = manifest_dir.join("port.toml");
        let manifest_text =
            fs::read_to_string(&manifest_path).map_err(|e| PortError::ManifestLoadError(e))?;
        let manifest: PortManifest =
            toml::from_str(&manifest_text).map_err(PortError::ManifestParseError)?;
        let all_dependencies = match manifest.dependencies.as_ref() {
            Some(deps) => {
                let mut d = vec![];
                if deps.c_compiler.unwrap_or(true) {
                    d.extend_from_slice(&Self::default_c_dependencies(name));
                }
                if let Some(build) = deps.build.as_ref() {
                    d.extend(build.iter().cloned());
                }
                if let Some(runtime) = deps.runtime.as_ref() {
                    d.extend(runtime.iter().cloned());
                }
                d
            }
            None => Self::default_c_dependencies(name),
        };

        Ok(Self {
            package_name: name.into(),
            manifest_dir,
            source_dir,
            build_dir,
            manifest,
            all_dependencies,
        })
    }

    fn run_script<P: AsRef<Path>>(
        &self,
        env: &BuildEnv,
        script: P,
        step: BuildStep,
    ) -> Result<(), PortError> {
        // Script arguments:
        // * manifest-dir
        // * source-dir
        // * build-dir
        // Script env:
        // * Y_WORKSPACE_ROOT=...
        // * Y_HOST_PREFIX=...
        // * Y_SYSROOT=...
        // * Y_PORT_VERSION=...
        // * Y_ARCH=...
        // * Y_TRIPLE=...
        let mut command = Command::new(script.as_ref());

        let ports_prefix = env.workspace_root.join("userspace/target/ports-prefix");

        command.arg(&self.manifest_dir);
        command.arg(&self.source_dir);
        command.arg(&self.build_dir);

        command.env("Y_WORKSPACE_ROOT", &env.workspace_root);
        command.env("Y_SYSROOT", &env.ports_sysroot);
        command.env("Y_HOST_PREFIX", &ports_prefix);
        command.env("Y_PORT_VERSION", &self.manifest.version);
        command.env(
            "Y_PORT_UTILS",
            &env.workspace_root.join("ports/meta-port-scripts"),
        );
        command.env("Y_ARCH", env.arch.name());
        command.env("Y_TRIPLE", env.arch.user_triple());

        if self.package_name != "meta-llvm" && self.package_name != "llvm" {
            let path_var = env::var("PATH").unwrap();
            let prefix_path = ports_prefix.join("bin");
            command.env("PATH", format!("{}:{path_var}", prefix_path.display()));
        }

        let status = command
            .status()
            .map_err(|e| PortError::ScriptExecuteError(step, e))?;
        if status.success() {
            Ok(())
        } else {
            Err(PortError::ScriptFailed(step, status))
        }
    }

    fn lookup_script<F: Fn(&PortBuildScripts) -> Option<PathBuf>>(
        &self,
        mapper: F,
        fallback: &str,
        required: bool,
    ) -> Result<PathBuf, PortError> {
        let script = if let Some(script) = self.manifest.scripts.as_ref().and_then(mapper) {
            // Explicit fetch script
            if !script.exists() {
                return Err(PortError::ScriptNotFound(script));
            }
            script
        } else {
            let default = self.manifest_dir.join(fallback);
            if required && !default.exists() {
                return Err(PortError::ScriptNotFound(fallback.into()));
            }
            default
        };

        Ok(script)
    }

    pub fn run_step(&self, env: &BuildEnv, step: BuildStep) -> Result<(), PortError> {
        match step {
            BuildStep::Fetch => {
                let script =
                    self.lookup_script(|scripts| scripts.fetch.clone(), "fetch.sh", false)?;

                if !script.exists() {
                    return Ok(());
                }

                self.run_script(env, script, step)
            }
            BuildStep::Compile => {
                let script =
                    self.lookup_script(|scripts| scripts.compile.clone(), "compile.sh", true)?;
                self.run_script(env, script, step)
            }
            BuildStep::Install => {
                let script =
                    self.lookup_script(|scripts| scripts.install.clone(), "install.sh", true)?;
                self.run_script(env, script, step)
            }
        }
    }
}

impl Port {
    pub fn load(env: &BuildEnv, name: &str) -> Result<Self, PortError> {
        match name {
            "meta-libc" => Ok(Self::Meta(MetaPort::Libc)),
            _ => NormalPort::load(env, name).map(Self::Normal),
        }
    }

    pub fn name(&self) -> &str {
        match self {
            Self::Meta(MetaPort::Libc) => "meta-libc",
            Self::Normal(port) => &port.package_name,
        }
    }

    pub fn run_step(&self, env: &BuildEnv, step: BuildStep) -> Result<(), PortError> {
        log::info!("{:?}: {step:?}", self.name());
        match (self, step) {
            (Self::Meta(MetaPort::Libc), BuildStep::Fetch) => Ok(()),
            (Self::Meta(MetaPort::Libc), BuildStep::Compile) => {
                ygglibc::build_ygglibc(env).map_err(|e| PortError::LibcBuildError(step, e.into()))
            }
            (Self::Meta(MetaPort::Libc), BuildStep::Install) => {
                ygglibc::install_ygglibc(env).map_err(|e| PortError::LibcBuildError(step, e.into()))
            }
            (Self::Normal(port), step) => port.run_step(env, step),
        }
    }
}

fn run_step(env: &BuildEnv, ports: &[&Port], step: BuildStep) -> Result<(), Error> {
    for port in ports.iter().copied() {
        port.run_step(env, step)
            .map_err(|e| Error::PortError(port.name().into(), e))?;
    }
    Ok(())
}

pub fn build_and_install(env: &BuildEnv, ports: Vec<Port>) -> Result<(), Error> {
    let graph = DependencyGraph::from(&ports[..]);
    let mut to_build = vec![];
    let mut unresolved = false;
    for entry in graph {
        match entry {
            Step::Resolved(port) => to_build.push(port),
            Step::Unresolved(name) => {
                log::error!("Unresolved port: {name:?}");
                unresolved = true;
            }
        }
    }
    if unresolved {
        return Err(Error::PortUnresolvedDependencies);
    }

    run_step(env, &to_build, BuildStep::Fetch)?;
    for port in to_build {
        port.run_step(env, BuildStep::Compile)
            .map_err(|e| Error::PortError(port.name().into(), e))?;
        port.run_step(env, BuildStep::Install)
            .map_err(|e| Error::PortError(port.name().into(), e))?;
    }

    Ok(())
}
