use std::path::PathBuf;

use async_trait::async_trait;
use dagger_sdk::{Container, ContainerWithDirectoryOptsBuilder, HostDirectoryOptsBuilder};

use crate::{
    dagger_middleware::DynMiddleware,
    rust_service::architecture::{Architecture, Os},
    Context, MainAction, PullRequestAction,
};

#[derive(Clone)]
pub enum NodeServiceStage {
    BeforeDeps(DynMiddleware),
    AfterDeps(DynMiddleware),
    BeforeBase(DynMiddleware),
    AfterBase(DynMiddleware),
    BeforeBuild(DynMiddleware),
    AfterBuild(DynMiddleware),
    BeforePackage(DynMiddleware),
    AfterPackage(DynMiddleware),
    BeforeRelease(DynMiddleware),
    AfterRelease(DynMiddleware),
}

#[derive(Clone)]
pub struct NodeService {
    client: dagger_sdk::Query,
    service: String,
    base_image: Option<Container>,
    final_image: Option<Container>,
    stages: Vec<NodeServiceStage>,
    source: Option<PathBuf>,
    arch: Option<Architecture>,
    os: Option<Os>,
}

impl NodeService {
    pub fn new(value: dagger_sdk::Query, service: impl Into<String>) -> Self {
        Self {
            client: value,
            service: service.into(),
            base_image: None,
            final_image: None,
            stages: Vec::new(),
            source: None,
            arch: None,
            os: None,
        }
    }

    pub fn with_base_image(&mut self, base: dagger_sdk::Container) -> &mut Self {
        self.base_image = Some(base);

        self
    }

    pub fn with_service(&mut self, service: impl Into<String>) -> &mut Self {
        self.service = service.into();

        self
    }

    pub fn with_stage(&mut self, stage: NodeServiceStage) -> &mut Self {
        self.stages.push(stage);

        self
    }

    pub fn with_source(&mut self, path: impl Into<PathBuf>) -> &mut Self {
        self.source = Some(path.into());

        self
    }

    pub fn with_arch(&mut self, arch: Architecture) -> &mut Self {
        self.arch = Some(arch);

        self
    }

    pub fn with_os(&mut self, os: Os) -> &mut Self {
        self.os = Some(os);

        self
    }

    fn get_src(&self) -> PathBuf {
        self.source
            .clone()
            .unwrap_or(std::env::current_dir().unwrap())
    }

    fn get_arch(&self) -> Architecture {
        self.arch
            .clone()
            .unwrap_or_else(|| match std::env::consts::ARCH {
                "x86" | "x86_64" | "amd64" => Architecture::Amd64,
                "arm" => Architecture::Arm64,
                arch => panic!("unsupported architecture: {arch}"),
            })
    }

    fn get_os(&self) -> Os {
        self.os
            .clone()
            .unwrap_or_else(|| match std::env::consts::OS {
                "linux" => Os::Linux,
                "macos" => Os::MacOS,
                os => panic!("unsupported os: {os}"),
            })
    }

    pub async fn build_base(&self) -> eyre::Result<Container> {
        let src = self.client.host().directory_opts(
            self.get_src().to_string_lossy(),
            HostDirectoryOptsBuilder::default()
                .exclude(vec!["node_modules/", ".git/", ".cuddle/"])
                .build()?,
        );
        let pkg_files = self.client.host().directory_opts(
            self.get_src().to_string_lossy(),
            HostDirectoryOptsBuilder::default()
                .include(vec!["package.json", "yarn.lock"])
                .build()?,
        );
        let deps =
            "apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev vips-dev git postgresql-dev"
                .split_whitespace();

        let base_image = match self.base_image.clone() {
            Some(image) => image,
            None => self
                .client
                .container()
                .from("node:20-alpine")
                .with_exec(vec!["apk", "update"])
                .with_exec(deps.collect()),
        }
        .with_env_variable("NODE_ENV", "production");

        let base_yarn_image = base_image
            .with_workdir("/opt/")
            .with_directory(".", pkg_files)
            .with_exec(vec!["yarn", "global", "add", "node-gyp"])
            .with_exec(vec![
                "yarn",
                "config",
                "set",
                "network-timeout",
                "600000",
                "-g",
            ])
            .with_exec(vec!["yarn", "install", "--production"]);

        let base_build = base_yarn_image
            .with_env_variable(
                "PATH",
                format!(
                    "/opt/node_modules/.bin:{}",
                    base_yarn_image.env_variable("PATH").await?
                ),
            )
            .with_workdir("/opt/app")
            .with_directory(".", src)
            .with_exec(vec!["yarn", "build"]);

        Ok(base_build)
    }

    pub async fn build_release(&self) -> eyre::Result<Container> {
        let base = self.build_base().await?;

        let final_build_image = match self.final_image.clone() {
            Some(c) => c,
            None => self
                .client
                .container()
                .from("node:20-alpine")
                .with_exec(vec![
                    "apk",
                    "add",
                    "--no-cache",
                    "vips-dev",
                    "postgresql-dev",
                ]),
        }
        .with_env_variable("NODE_ENV", "production");

        let final_image = final_build_image
            .with_workdir("/opt/")
            .with_directory("/opt/node_modules", base.directory("/opt/node_modules"))
            .with_workdir("/opt/app")
            .with_directory_opts(
                "/opt/app",
                base.directory("/opt/app"),
                ContainerWithDirectoryOptsBuilder::default()
                    .owner("node:node")
                    .build()?,
            )
            .with_env_variable(
                "PATH",
                format!(
                    "/opt/node_modules/.bin:{}",
                    final_build_image.env_variable("PATH").await?
                ),
            )
            .with_user("node")
            .with_exposed_port(1337)
            .with_entrypoint(vec!["yarn", "start"]);

        Ok(final_image)
    }
}

#[async_trait]
impl PullRequestAction for NodeService {
    async fn execute_pull_request(&self, _ctx: &mut Context) -> eyre::Result<()> {
        let release = self.build_release().await?;

        release.sync().await?;

        Ok(())
    }
}

#[async_trait]
impl MainAction for NodeService {
    async fn execute_main(&self, _ctx: &mut Context) -> eyre::Result<()> {
        let container = self.build_release().await?;
        let timestamp = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_secs();

        container
            .publish(format!(
                "docker.io/kasperhermansen/{}:main-{}",
                self.service, timestamp,
            ))
            .await?;

        let update_deployments_docker_image =
            "docker.io/kasperhermansen/update-deployment:1701123940";
        let dep = self
            .client
            .container()
            .from(update_deployments_docker_image);

        let dep = match std::env::var("SSH_AUTH_SOCK").ok() {
            Some(sock) => dep
                .with_unix_socket("/tmp/ssh_sock", self.client.host().unix_socket(sock))
                .with_env_variable("SSH_AUTH_SOCK", "/tmp/ssh_sock")
                .with_exec(vec![
                    "update-deployment",
                    "--repo",
                    &format!(
                        "git@git.kjuulh.io:kjuulh/{}-deployment.git",
                        self.service
                    ),
                    "--service",
                    &self.service,
                    "--image",
                    &format!("kasperhermansen/{}:main-{}", self.service, timestamp),
                ]),
            _ => dep
                .with_env_variable("GIT_USERNAME", "kjuulh")
                .with_env_variable(
                    "GIT_PASSWORD",
                    std::env::var("GIT_PASSWORD").expect("GIT_PASSWORD to be set"),
                )
                .with_exec(vec![
                    "update-deployment",
                    "--repo",
                    &format!(
                        "https://git.kjuulh.io/kjuulh/{}-deployment.git",
                        self.service
                    ),
                    "--service",
                    &self.service,
                    "--image",
                    &format!("kasperhermansen/{}:main-{}", self.service, timestamp),
                ]),
        };

        dep.sync().await?;

        Ok(())
    }
}
