use convert_case::{Case, Casing};
use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, WORKDIR};
use dockerfile_builder::{Dockerfile, instruction_builder::EnvBuilder};
use fqdn::fqdn;
use harmony_macros::ingress_path;
use non_blank_string_rs::NonBlankString;
use serde_json::json;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr;

use async_trait::async_trait;
use log::{debug, info};
use serde::Serialize;

use crate::config::{REGISTRY_PROJECT, REGISTRY_URL};
use crate::modules::k8s::ingress::K8sIngressScore;
use crate::topology::HelmCommand;
use crate::{
    data::{Id, Version},
    interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome},
    inventory::Inventory,
    modules::k8s::deployment::K8sDeploymentScore,
    score::Score,
    topology::{K8sclient, Topology, Url},
};

use super::helm::chart::HelmChartScore;

#[derive(Debug, Clone, Serialize)]
pub struct LAMPScore {
    pub name: String,
    pub domain: Url,
    pub config: LAMPConfig,
    pub php_version: Version,
}

#[derive(Debug, Clone, Serialize)]
pub struct LAMPConfig {
    pub project_root: PathBuf,
    pub ssl_enabled: bool,
    pub database_size: Option<String>,
    pub namespace: String,
}

impl Default for LAMPConfig {
    fn default() -> Self {
        LAMPConfig {
            project_root: Path::new("./src").to_path_buf(),
            ssl_enabled: true,
            database_size: None,
            namespace: "harmony-lamp".to_string(),
        }
    }
}

impl<T: Topology + K8sclient + HelmCommand> Score<T> for LAMPScore {
    fn create_interpret(&self) -> Box<dyn Interpret<T>> {
        Box::new(LAMPInterpret {
            score: self.clone(),
        })
    }

    fn name(&self) -> String {
        "LampScore".to_string()
    }
}

#[derive(Debug)]
pub struct LAMPInterpret {
    score: LAMPScore,
}

#[async_trait]
impl<T: Topology + K8sclient + HelmCommand> Interpret<T> for LAMPInterpret {
    async fn execute(
        &self,
        inventory: &Inventory,
        topology: &T,
    ) -> Result<Outcome, InterpretError> {
        let image_name = match self.build_docker_image() {
            Ok(name) => name,
            Err(e) => {
                return Err(InterpretError::new(format!(
                    "Could not build LAMP docker image {e}"
                )));
            }
        };
        info!("LAMP docker image built {image_name}");

        let remote_name = match self.push_docker_image(&image_name) {
            Ok(remote_name) => remote_name,
            Err(e) => {
                return Err(InterpretError::new(format!(
                    "Could not push docker image {e}"
                )));
            }
        };
        info!("LAMP docker image pushed to {remote_name}");

        info!("Deploying database");
        self.deploy_database(inventory, topology).await?;

        let base_name = self.score.name.to_case(Case::Kebab);
        let secret_name = format!("{}-database-mariadb", base_name);

        let deployment_score = K8sDeploymentScore {
            name: <LAMPScore as Score<T>>::name(&self.score).to_case(Case::Kebab),
            image: remote_name,
            namespace: self.get_namespace().map(|nbs| nbs.to_string()),
            env_vars: json!([
            {
                "name": "MYSQL_PASSWORD",
                "valueFrom": {
                    "secretKeyRef": {
                        "name": secret_name,
                        "key": "mariadb-root-password"
                    }
                }
            },
            {
                "name": "MYSQL_HOST",
                "value": secret_name
            },
            ]),
        };

        info!("Deploying score {deployment_score:#?}");

        deployment_score
            .create_interpret()
            .execute(inventory, topology)
            .await?;

        info!("LAMP deployment_score {deployment_score:?}");

        let lamp_ingress = K8sIngressScore {
            name: fqdn!("lamp-ingress"),
            host: fqdn!("test"),
            backend_service: fqdn!(
                <LAMPScore as Score<T>>::name(&self.score)
                    .to_case(Case::Kebab)
                    .as_str()
            ),
            port: 8080,
            path: Some(ingress_path!("/")),
            path_type: None,
            namespace: self
                .get_namespace()
                .map(|nbs| fqdn!(nbs.to_string().as_str())),
        };

        lamp_ingress
            .create_interpret()
            .execute(inventory, topology)
            .await?;

        info!("LAMP lamp_ingress {lamp_ingress:?}");

        Ok(Outcome::success(
            "Successfully deployed LAMP Stack!".to_string(),
        ))
    }

    fn get_name(&self) -> InterpretName {
        todo!()
    }

    fn get_version(&self) -> Version {
        todo!()
    }

    fn get_status(&self) -> InterpretStatus {
        todo!()
    }

    fn get_children(&self) -> Vec<Id> {
        todo!()
    }
}

impl LAMPInterpret {
    async fn deploy_database<T: Topology + K8sclient + HelmCommand>(
        &self,
        inventory: &Inventory,
        topology: &T,
    ) -> Result<Outcome, InterpretError> {
        let mut values_overrides = HashMap::new();
        if let Some(database_size) = self.score.config.database_size.clone() {
            values_overrides.insert(
                NonBlankString::from_str("primary.persistence.size").unwrap(),
                database_size,
            );
            values_overrides.insert(
                NonBlankString::from_str("auth.rootPassword").unwrap(),
                "mariadb-changethis".to_string(),
            );
        }
        let score = HelmChartScore {
            namespace: self.get_namespace(),
            release_name: NonBlankString::from_str(&format!("{}-database", self.score.name))
                .unwrap(),
            chart_name: NonBlankString::from_str(
                "oci://registry-1.docker.io/bitnamicharts/mariadb",
            )
            .unwrap(),
            chart_version: None,
            values_overrides: Some(values_overrides),
            create_namespace: true,
            install_only: false,
            values_yaml: None,
            repository: None,
        };

        score.create_interpret().execute(inventory, topology).await
    }
    fn build_dockerfile(&self, score: &LAMPScore) -> Result<PathBuf, Box<dyn std::error::Error>> {
        let mut dockerfile = Dockerfile::new();

        // Use the PHP version from the score to determine the base image
        let php_version = score.php_version.to_string();
        let php_major_minor = php_version
            .split('.')
            .take(2)
            .collect::<Vec<&str>>()
            .join(".");

        // Base image selection - using official PHP image with Apache
        dockerfile.push(FROM::from(format!("php:{}-apache", php_major_minor)));

        // Set environment variables for PHP configuration
        dockerfile.push(ENV::from("PHP_MEMORY_LIMIT=256M"));
        dockerfile.push(ENV::from("PHP_MAX_EXECUTION_TIME=30"));
        dockerfile.push(
            EnvBuilder::builder()
                .key("PHP_ERROR_REPORTING")
                .value("\"E_ERROR | E_WARNING | E_PARSE\"")
                .build()
                .unwrap(),
        );

        // Install necessary PHP extensions and dependencies
        dockerfile.push(RUN::from(
            "apt-get update && \
             apt-get install -y --no-install-recommends \
                libfreetype6-dev \
                libjpeg62-turbo-dev \
                libpng-dev \
                libzip-dev \
                unzip \
                && apt-get clean \
                && rm -rf /var/lib/apt/lists/*",
        ));

        dockerfile.push(RUN::from(
            "docker-php-ext-configure gd --with-freetype --with-jpeg && \
             docker-php-ext-install -j$(nproc) \
                gd \
                mysqli \
                pdo_mysql \
                zip \
                opcache",
        ));

        dockerfile.push(RUN::from(r#"sed -i 's/VirtualHost \*:80/VirtualHost *:8080/' /etc/apache2/sites-available/000-default.conf && \
                sed -i 's/^Listen 80$/Listen 8080/' /etc/apache2/ports.conf"#));

        // Copy PHP configuration
        dockerfile.push(RUN::from("mkdir -p /usr/local/etc/php/conf.d/"));

        // Create and copy a custom PHP configuration
        let php_config = r#"
memory_limit = ${PHP_MEMORY_LIMIT}
max_execution_time = ${PHP_MAX_EXECUTION_TIME}
error_reporting = ${PHP_ERROR_REPORTING}
display_errors = Off
log_errors = On
error_log = /dev/stderr
date.timezone = UTC

; Opcache configuration for production
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=4000
opcache.revalidate_freq=2
opcache.fast_shutdown=1
"#;

        // Save this configuration to a temporary file within the project root
        let config_path = Path::new(&score.config.project_root).join("docker-php.ini");
        fs::write(&config_path, php_config)?;

        // Reference the file within the Docker context (where the build runs)
        dockerfile.push(COPY::from(
            "docker-php.ini /usr/local/etc/php/conf.d/docker-php.ini",
        ));

        // Security hardening
        dockerfile.push(RUN::from(
            "a2enmod headers && \
             a2enmod rewrite && \
             sed -i 's/ServerTokens OS/ServerTokens Prod/' /etc/apache2/conf-enabled/security.conf && \
             sed -i 's/ServerSignature On/ServerSignature Off/' /etc/apache2/conf-enabled/security.conf"
        ));

        // Set env vars
        dockerfile.push(RUN::from(
            "echo 'PassEnv MYSQL_PASSWORD' >> /etc/apache2/sites-available/000-default.conf \
             && echo 'PassEnv MYSQL_USER' >> /etc/apache2/sites-available/000-default.conf \
             && echo 'PassEnv MYSQL_HOST' >> /etc/apache2/sites-available/000-default.conf",
        ));

        // Create a dedicated user for running Apache
        dockerfile.push(RUN::from(
            "groupadd -g 1000 appuser && \
             useradd -u 1000 -g appuser -m -s /bin/bash appuser && \
             chown -R appuser:appuser /var/www/html",
        ));

        // Set the working directory
        dockerfile.push(WORKDIR::from("/var/www/html"));

        // Copy application code from the project root to the container
        // Note: In Dockerfile, the COPY context is relative to the build context
        // We'll handle the actual context in the build_docker_image method
        dockerfile.push(COPY::from(". /var/www/html"));

        // Fix permissions
        dockerfile.push(RUN::from("chown -R appuser:appuser /var/www/html"));

        // Expose Apache port
        dockerfile.push(EXPOSE::from("8080/tcp"));

        // Set the default command
        dockerfile.push(CMD::from("apache2-foreground"));

        // Save the Dockerfile to disk in the project root
        let dockerfile_path = Path::new(&score.config.project_root).join("Dockerfile");
        fs::write(&dockerfile_path, dockerfile.to_string())?;

        Ok(dockerfile_path)
    }

    fn check_output(
        &self,
        output: &std::process::Output,
        msg: &str,
    ) -> Result<(), Box<dyn std::error::Error>> {
        if !output.status.success() {
            return Err(format!("{msg}: {}", String::from_utf8_lossy(&output.stderr)).into());
        }
        Ok(())
    }

    fn push_docker_image(&self, image_name: &str) -> Result<String, Box<dyn std::error::Error>> {
        let full_tag = format!("{}/{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT, &image_name);
        let output = std::process::Command::new("docker")
            .args(["tag", image_name, &full_tag])
            .output()?;
        self.check_output(&output, "Tagging docker image failed")?;

        debug!(
            "docker tag output {} {}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );

        let output = std::process::Command::new("docker")
            .args(["push", &full_tag])
            .output()?;
        self.check_output(&output, "Pushing docker image failed")?;
        debug!(
            "docker push output {} {}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );

        Ok(full_tag)
    }

    pub fn build_docker_image(&self) -> Result<String, Box<dyn std::error::Error>> {
        info!("Generating Dockerfile");
        let dockerfile = self.build_dockerfile(&self.score)?;

        info!(
            "Building Docker image with file {} from root {}",
            dockerfile.to_string_lossy(),
            self.score.config.project_root.to_string_lossy()
        );
        let image_name = format!("{}-php-apache", self.score.name);
        let project_root = &self.score.config.project_root;

        let output = std::process::Command::new("docker")
            .args([
                "build",
                "--file",
                dockerfile.to_str().unwrap(),
                "-t",
                &image_name,
                project_root.to_str().unwrap(),
            ])
            .output()?;

        if !output.status.success() {
            return Err(format!(
                "Failed to build Docker image: {}",
                String::from_utf8_lossy(&output.stderr)
            )
            .into());
        }

        Ok(image_name)
    }

    fn get_namespace(&self) -> Option<NonBlankString> {
        Some(NonBlankString::from_str(&self.score.config.namespace).unwrap())
    }
}
