use std::fs;
use std::path::PathBuf;
use std::process;
use std::sync::Arc;

use async_trait::async_trait;
use bollard::query_parameters::PushImageOptionsBuilder;
use bollard::{Docker, body_full};
use dockerfile_builder::Dockerfile;
use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR};
use dockerfile_builder::instruction_builder::CopyBuilder;
use futures_util::StreamExt;
use log::{debug, error, info};
use serde::Serialize;
use tar::Archive;

use crate::config::{REGISTRY_PROJECT, REGISTRY_URL};
use crate::{
    score::Score,
    topology::{Topology, Url},
};

use super::{Application, ApplicationFeature, ApplicationInterpret, HelmPackage, OCICompliant};

#[derive(Debug, Serialize, Clone)]
pub struct ApplicationScore<A: Application + Serialize, T: Topology + Clone + Serialize>
where
    Arc<A>: Serialize + Clone,
{
    pub features: Vec<Box<dyn ApplicationFeature<T>>>,
    pub application: Arc<A>,
}

impl<
    A: Application + Serialize + Clone + 'static,
    T: Topology + std::fmt::Debug + Clone + Serialize + 'static,
> Score<T> for ApplicationScore<A, T>
where
    Arc<A>: Serialize,
{
    fn create_interpret(&self) -> Box<dyn crate::interpret::Interpret<T>> {
        Box::new(ApplicationInterpret {
            features: self.features.clone(),
            application: self.application.clone(),
        })
    }

    fn name(&self) -> String {
        format!("Application: {}", self.application.name())
    }
}

#[derive(Debug, Clone, Serialize)]
pub enum RustWebFramework {
    Leptos,
}

#[derive(Debug, Clone, Serialize)]
pub struct RustWebapp {
    pub name: String,
    pub domain: Url,
    /// The path to the root of the Rust project to be containerized.
    pub project_root: PathBuf,
    pub framework: Option<RustWebFramework>,
}

impl Application for RustWebapp {
    fn name(&self) -> String {
        self.name.clone()
    }
}

#[async_trait]
impl HelmPackage for RustWebapp {
    async fn build_push_helm_package(&self, image_url: &str) -> Result<String, String> {
        info!("Starting Helm chart build and push for '{}'", self.name);

        // 1. Create the Helm chart files on disk.
        let chart_dir = self
            .create_helm_chart_files(image_url)
            .map_err(|e| format!("Failed to create Helm chart files: {}", e))?;
        info!("Successfully created Helm chart files in {:?}", chart_dir);

        // 2. Package the chart into a .tgz archive.
        let packaged_chart_path = self
            .package_helm_chart(&chart_dir)
            .map_err(|e| format!("Failed to package Helm chart: {}", e))?;
        info!(
            "Successfully packaged Helm chart: {}",
            packaged_chart_path.to_string_lossy()
        );

        // 3. Push the packaged chart to the OCI registry.
        let oci_chart_url = self
            .push_helm_chart(&packaged_chart_path)
            .map_err(|e| format!("Failed to push Helm chart: {}", e))?;
        info!("Successfully pushed Helm chart to: {}", oci_chart_url);

        Ok(oci_chart_url)
    }
}

#[async_trait]
impl OCICompliant for RustWebapp {
    /// Builds a Docker image for the Rust web application using a multi-stage build,
    /// pushes it to the configured OCI registry, and returns the full image tag.
    async fn build_push_oci_image(&self) -> Result<String, String> {
        // This function orchestrates the build and push process.
        // It's async to match the trait definition, though the underlying docker commands are blocking.
        info!("Starting OCI image build and push for '{}'", self.name);

        // 1. Build the local image by calling the synchronous helper function.
        let local_image_name = self.local_image_name();
        self.build_docker_image(&local_image_name)
            .await
            .map_err(|e| format!("Failed to build Docker image: {}", e))?;
        info!(
            "Successfully built local Docker image: {}",
            local_image_name
        );

        let remote_image_name = self.image_name();
        // 2. Push the image to the registry.
        self.push_docker_image(&local_image_name, &remote_image_name)
            .await
            .map_err(|e| format!("Failed to push Docker image: {}", e))?;
        info!("Successfully pushed Docker image to: {}", remote_image_name);

        Ok(remote_image_name)
    }

    fn local_image_name(&self) -> String {
        self.name.clone()
    }

    fn image_name(&self) -> String {
        format!(
            "{}/{}/{}",
            *REGISTRY_URL,
            *REGISTRY_PROJECT,
            &self.local_image_name()
        )
    }
}

/// Implementation of helper methods for building and pushing the Docker image.
impl RustWebapp {
    /// Generates a multi-stage Dockerfile for a Rust application.
    fn build_dockerfile(&self) -> Result<PathBuf, Box<dyn std::error::Error>> {
        let mut dockerfile = Dockerfile::new();

        self.build_builder_image(&mut dockerfile);

        // Save the Dockerfile to a uniquely named file in the project root to avoid conflicts.
        let dockerfile_path = self.project_root.join("Dockerfile.harmony");
        fs::write(&dockerfile_path, dockerfile.to_string())?;

        Ok(dockerfile_path)
    }

    /// Builds the Docker image using the generated Dockerfile.
    pub async fn build_docker_image(
        &self,
        image_name: &str,
    ) -> Result<String, Box<dyn std::error::Error>> {
        info!("Generating Dockerfile for '{}'", self.name);
        let _dockerfile_path = self.build_dockerfile()?;

        let docker = Docker::connect_with_socket_defaults().unwrap();

        let build_image_options = bollard::query_parameters::BuildImageOptionsBuilder::default()
            .dockerfile("Dockerfile.harmony")
            .t(image_name)
            .q(false)
            .version(bollard::query_parameters::BuilderVersion::BuilderV1)
            .platform("linux/x86_64");

        let mut temp_tar_builder = tar::Builder::new(Vec::new());
        let _ = temp_tar_builder
            .append_dir_all("", self.project_root.clone())
            .unwrap();
        let archive = temp_tar_builder
            .into_inner()
            .expect("couldn't finish creating tar");
        let archived_files = Archive::new(archive.as_slice())
            .entries()
            .unwrap()
            .map(|entry| entry.unwrap().path().unwrap().into_owned())
            .collect::<Vec<_>>();

        debug!("files in docker tar: {:#?}", archived_files);

        let mut image_build_stream = docker.build_image(
            build_image_options.build(),
            None,
            Some(body_full(archive.into())),
        );

        while let Some(msg) = image_build_stream.next().await {
            println!("Message: {msg:?}");
        }

        Ok(image_name.to_string())
    }

    /// Tags and pushes a Docker image to the configured remote registry.
    async fn push_docker_image(
        &self,
        image_name: &str,
        full_tag: &str,
    ) -> Result<String, Box<dyn std::error::Error>> {
        info!("Pushing docker image {full_tag}");

        let docker = Docker::connect_with_socket_defaults().unwrap();

        // let push_options = PushImageOptionsBuilder::new().tag(tag);

        let mut push_image_stream =
            docker.push_image(full_tag, Some(PushImageOptionsBuilder::new().build()), None);

        while let Some(msg) = push_image_stream.next().await {
            println!("Message: {msg:?}");
        }

        Ok(full_tag.to_string())
    }

    /// Checks the output of a process command for success.
    fn check_output(
        &self,
        output: &process::Output,
        msg: &str,
    ) -> Result<(), Box<dyn std::error::Error>> {
        if !output.status.success() {
            let error_message = format!("{}: {}", msg, String::from_utf8_lossy(&output.stderr));
            return Err(error_message.into());
        }
        Ok(())
    }

    fn build_builder_image(&self, dockerfile: &mut Dockerfile) {
        match self.framework {
            Some(RustWebFramework::Leptos) => {
                // --- Stage 1: Builder for Leptos ---
                dockerfile.push(FROM::from("rust:bookworm as builder"));

                // Install dependencies, cargo-binstall, and clean up in one layer
                dockerfile.push(RUN::from(
                    "apt-get update && \
                     apt-get install -y --no-install-recommends clang wget && \
                     wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz && \
                     tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz && \
                     cp cargo-binstall /usr/local/cargo/bin && \
                     rm cargo-binstall-x86_64-unknown-linux-musl.tgz cargo-binstall && \
                     apt-get clean && \
                     rm -rf /var/lib/apt/lists/*"
                ));

                // Install cargo-leptos
                dockerfile.push(RUN::from("cargo binstall cargo-leptos -y"));

                // Add the WASM target
                dockerfile.push(RUN::from("rustup target add wasm32-unknown-unknown"));

                // Set up workdir, copy source, and build
                dockerfile.push(WORKDIR::from("/app"));
                dockerfile.push(COPY::from(". ."));
                dockerfile.push(RUN::from("cargo leptos build --release -vv"));
                // --- Stage 2: Final Image ---
                dockerfile.push(FROM::from("debian:bookworm-slim"));

                // Create a non-root user for security.
                dockerfile.push(RUN::from(
                    "groupadd -r appgroup && useradd -r -s /bin/false -g appgroup appuser",
                ));

                dockerfile.push(ENV::from("LEPTOS_SITE_ADDR=0.0.0.0:3000"));
                dockerfile.push(EXPOSE::from("3000/tcp"));
                dockerfile.push(WORKDIR::from("/home/appuser"));

                // Copy static files
                dockerfile.push(
                    CopyBuilder::builder()
                        .from("builder")
                        .src("/app/target/site/pkg")
                        .dest("/home/appuser/pkg")
                        .build()
                        .unwrap(),
                );
                // Copy the compiled binary from the builder stage.
                error!(
                    "FIXME Should not be using score name here, instead should use name from Cargo.toml"
                );
                let binary_path_in_builder = format!("/app/target/release/{}", self.name);
                let binary_path_in_final = format!("/home/appuser/{}", self.name);
                dockerfile.push(
                    CopyBuilder::builder()
                        .from("builder")
                        .src(binary_path_in_builder)
                        .dest(&binary_path_in_final)
                        .build()
                        .unwrap(),
                );

                // Run as the non-root user.
                dockerfile.push(USER::from("appuser"));

                // Set the command to run the application.
                dockerfile.push(CMD::from(binary_path_in_final));
            }
            None => {
                // --- Stage 1: Builder for a generic Rust app ---
                dockerfile.push(FROM::from("rust:latest as builder"));

                // Install the wasm32 target as required.
                dockerfile.push(RUN::from("rustup target add wasm32-unknown-unknown"));
                dockerfile.push(WORKDIR::from("/app"));

                // Copy the source code and build the application.
                dockerfile.push(COPY::from(". ."));
                dockerfile.push(RUN::from("cargo build --release --locked"));
                // --- Stage 2: Final Image ---
                dockerfile.push(FROM::from("debian:bookworm-slim"));

                // Create a non-root user for security.
                dockerfile.push(RUN::from(
                    "groupadd -r appgroup && useradd -r -s /bin/false -g appgroup appuser",
                ));

                // Copy only the compiled binary from the builder stage.
                error!(
                    "FIXME Should not be using score name here, instead should use name from Cargo.toml"
                );
                let binary_path_in_builder = format!("/app/target/release/{}", self.name);
                let binary_path_in_final = format!("/usr/local/bin/{}", self.name);
                dockerfile.push(
                    CopyBuilder::builder()
                        .from("builder")
                        .src(binary_path_in_builder)
                        .dest(&binary_path_in_final)
                        .build()
                        .unwrap(),
                );

                // Run as the non-root user.
                dockerfile.push(USER::from("appuser"));

                // Set the command to run the application.
                dockerfile.push(CMD::from(binary_path_in_final));
            }
        }
    }

    /// Creates all necessary files for a basic Helm chart.
    fn create_helm_chart_files(
        &self,
        image_url: &str,
    ) -> Result<PathBuf, Box<dyn std::error::Error>> {
        let chart_name = format!("{}-chart", self.name);
        let chart_dir = self
            .project_root
            .join(".harmony_generated")
            .join("helm")
            .join(&chart_name);
        let templates_dir = chart_dir.join("templates");
        fs::create_dir_all(&templates_dir)?;

        let (image_repo, image_tag) = image_url.rsplit_once(':').unwrap_or((image_url, "latest"));

        // Create Chart.yaml
        let chart_yaml = format!(
            r#"
apiVersion: v2
name: {}
description: A Helm chart for the {} web application.
type: application
version: 0.1.0
appVersion: "{}"
"#,
            chart_name, self.name, image_tag
        );
        fs::write(chart_dir.join("Chart.yaml"), chart_yaml)?;

        // Create values.yaml
        let values_yaml = format!(
            r#"
# Default values for {}.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

replicaCount: 1

image:
  repository: {}
  pullPolicy: IfNotPresent
  # Overridden by the chart's appVersion
  tag: "{}"

service:
  type: ClusterIP
  port: 3000

ingress:
  enabled: true
  # Annotations for cert-manager to handle SSL.
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    # Add other annotations like nginx ingress class if needed
    # kubernetes.io/ingress.class: nginx
  hosts:
    - host: chart-example.local
      paths:
        - path: /
          pathType: ImplementationSpecific
  tls:
   - secretName: {}-tls
     hosts:
       - chart-example.local

"#,
            chart_name, image_repo, image_tag, self.name
        );
        fs::write(chart_dir.join("values.yaml"), values_yaml)?;

        // Create templates/_helpers.tpl
        let helpers_tpl = r#"
{{/*
Expand the name of the chart.
*/}}
{{- define "chart.name" -}}
{{- default .Chart.Name $.Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
*/}}
{{- define "chart.fullname" -}}
{{- $name := default .Chart.Name $.Values.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
"#;
        fs::write(templates_dir.join("_helpers.tpl"), helpers_tpl)?;

        // Create templates/service.yaml
        let service_yaml = r#"
apiVersion: v1
kind: Service
metadata:
  name: {{ include "chart.fullname" . }}
spec:
  type: {{ $.Values.service.type }}
  ports:
    - name: main
      port: {{ $.Values.service.port | default 3000 }}
      targetPort: {{ $.Values.service.port | default 3000 }}
      protocol: TCP
  selector:
    app: {{ include "chart.name" . }}
"#;
        fs::write(templates_dir.join("service.yaml"), service_yaml)?;

        // Create templates/deployment.yaml
        let deployment_yaml = r#"
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "chart.fullname" . }}
spec:
  replicas: {{ $.Values.replicaCount }}
  selector:
    matchLabels:
      app: {{ include "chart.name" . }}
  template:
    metadata:
      labels:
        app: {{ include "chart.name" . }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ $.Values.image.repository }}:{{ $.Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ $.Values.image.pullPolicy }}
          ports:
            - name: main
              containerPort: {{ $.Values.service.port | default 3000 }}
              protocol: TCP
"#;
        fs::write(templates_dir.join("deployment.yaml"), deployment_yaml)?;

        // Create templates/ingress.yaml
        let ingress_yaml = r#"
{{- if $.Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "chart.fullname" . }}
  annotations:
    {{- toYaml $.Values.ingress.annotations | nindent 4 }}
spec:
  {{- if $.Values.ingress.tls }}
  tls:
    {{- range $.Values.ingress.tls }}
    - hosts:
        {{- range .hosts }}
        - {{ . | quote }}
        {{- end }}
      secretName: {{ .secretName }}
    {{- end }}
  {{- end }}
  rules:
    {{- range $.Values.ingress.hosts }}
    - host: {{ .host | quote }}
      http:
        paths:
          {{- range .paths }}
          - path: {{ .path }}
            pathType: {{ .pathType }}
            backend:
              service:
                name: {{ include "chart.fullname" $ }}
                port:
                  number: {{ $.Values.service.port | default 3000 }}
          {{- end }}
    {{- end }}
{{- end }}
"#;
        fs::write(templates_dir.join("ingress.yaml"), ingress_yaml)?;

        Ok(chart_dir)
    }

    /// Packages a Helm chart directory into a .tgz file.
    fn package_helm_chart(
        &self,
        chart_dir: &PathBuf,
    ) -> Result<PathBuf, Box<dyn std::error::Error>> {
        let chart_dirname = chart_dir.file_name().expect("Should find a chart dirname");
        info!(
            "Launching `helm package {}` cli with CWD {}",
            chart_dirname.to_string_lossy(),
            &self
                .project_root
                .join(".harmony_generated")
                .join("helm")
                .to_string_lossy()
        );
        let output = process::Command::new("helm")
            .args(["package", chart_dirname.to_str().unwrap()])
            .current_dir(&self.project_root.join(".harmony_generated").join("helm")) // Run package from the parent dir
            .output()?;

        self.check_output(&output, "Failed to package Helm chart")?;

        // Helm prints the path of the created chart to stdout.
        let tgz_name = String::from_utf8(output.stdout)?
            .trim()
            .split_whitespace()
            .last()
            .unwrap_or_default()
            .to_string();
        if tgz_name.is_empty() {
            return Err("Could not determine packaged chart filename.".into());
        }

        // The output from helm is relative, so we join it with the execution directory.
        Ok(self
            .project_root
            .join(".harmony_generated")
            .join("helm")
            .join(tgz_name))
    }

    /// Pushes a packaged Helm chart to an OCI registry.
    fn push_helm_chart(
        &self,
        packaged_chart_path: &PathBuf,
    ) -> Result<String, Box<dyn std::error::Error>> {
        // The chart name is the file stem of the .tgz file
        let chart_file_name = packaged_chart_path.file_stem().unwrap().to_str().unwrap();
        let oci_push_url = format!("oci://{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT);
        let oci_pull_url = format!("{oci_push_url}/{}-chart", self.name);

        info!(
            "Pushing Helm chart {} to {}",
            packaged_chart_path.to_string_lossy(),
            oci_push_url
        );

        let output = process::Command::new("helm")
            .args(["push", packaged_chart_path.to_str().unwrap(), &oci_push_url])
            .output()?;

        self.check_output(&output, "Pushing Helm chart failed")?;

        // The final URL includes the version tag, which is part of the file name
        let version = chart_file_name.rsplit_once('-').unwrap().1;
        debug!("pull url {oci_pull_url}");
        debug!("push url {oci_push_url}");
        Ok(format!("{}:{}", oci_pull_url, version))
    }
}
