// SPDX-FileCopyrightText: 2024-2026 The n5i Project
//
// SPDX-License-Identifier: AGPL-3.0-or-later

pub mod networkpolicy;

use std::collections::{BTreeMap, HashSet};

use crate::internal::NetworkPolicyIngress;
use crate::internal::networkpolicy::NetworkPolicy;
use crate::metadata::{Dependency, Metadata, Runtime, SvcPorts};
use crate::utils::{StringLike, StringOrNumber};
use itertools::Itertools;
use k8s_openapi::api::rbac::v1::PolicyRule;
use serde::{Deserialize, Serialize};
use slugify::slugify;

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Runnable {
    Deployment(Box<Deployment>),
    StatefulSet(Box<StatefulSet>),
    Once(Box<Job>),
    OnAppUpdate(Box<Job>),
    Cron(Box<CronJob>),
}

impl Runnable {
    pub fn into_container(self) -> Container {
        match self {
            Runnable::Deployment(dep) => dep.container,
            Runnable::StatefulSet(set) => set.container,
            Runnable::Once(job) => job.container,
            Runnable::OnAppUpdate(job) => job.container,
            Runnable::Cron(job) => job.container,
        }
    }

    pub fn get_container(&self) -> &Container {
        match self {
            Runnable::Deployment(dep) => &dep.container,
            Runnable::StatefulSet(set) => &set.container,
            Runnable::Once(job) => &job.container,
            Runnable::OnAppUpdate(job) => &job.container,
            Runnable::Cron(job) => &job.container,
        }
    }
}

#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub enum Volume {
    Host(HostVolume),
    Pvc(PvcVolume),
    Secret(SecretMount),
    EmptyDir(EmptyDirVolume),
}

impl Volume {
    pub fn get_name(&self) -> &str {
        match self {
            Volume::Host(vol) => &vol.name,
            Volume::Pvc(vol) => &vol.name,
            Volume::Secret(vol) => &vol.name,
            Volume::EmptyDir(vol) => &vol.name,
        }
    }

    pub fn get_slug(&self) -> String {
        match self {
            Volume::Host(vol) => {
                format!("host-{}", slugify!(&vol.name))
            }
            Volume::Pvc(vol) => {
                let prefix = vol
                    .from_app
                    .as_ref()
                    .map(|a| slugify!(a))
                    .or_else(|| vol.from_ns.as_ref().map(|ns| slugify!(ns)))
                    .unwrap_or_else(|| "own".to_string());
                format!("pvc-{}-{}", prefix, slugify!(&vol.name))
            }
            Volume::Secret(vol) => {
                format!("secret-{}", slugify!(&vol.name))
            }
            Volume::EmptyDir(vol) => {
                format!("emptydir-{}", slugify!(&vol.name))
            }
        }
    }

    pub fn get_target_slug(&self) -> String {
        match self {
            Volume::Host(vol) => {
                format!("host-{}", slugify!(&vol.name))
            }
            Volume::Pvc(vol) => {
                format!("pvc-own-{}", slugify!(&vol.name))
            }
            Volume::Secret(vol) => {
                format!("secret-{}", slugify!(&vol.name))
            }
            Volume::EmptyDir(vol) => {
                format!("emptydir-{}", slugify!(&vol.name))
            }
        }
    }

    pub fn get_sub_path(&self) -> Option<&String> {
        match self {
            Volume::Host(_) => None,
            Volume::Pvc(vol) => vol.sub_path.as_ref(),
            Volume::Secret(_) => None,
            Volume::EmptyDir(_) => None,
        }
    }

    pub fn get_mount_path(&self) -> &str {
        match self {
            Volume::Host(vol) => &vol.mount_path,
            Volume::Pvc(vol) => &vol.mount_path,
            Volume::Secret(vol) => &vol.mount_path,
            Volume::EmptyDir(vol) => &vol.mount_path,
        }
    }

    pub fn is_readonly(&self) -> bool {
        match self {
            Volume::Host(vol) => vol.is_readonly,
            Volume::Pvc(vol) => vol.is_readonly,
            Volume::Secret(_) => true,
            Volume::EmptyDir(vol) => vol.is_readonly,
        }
    }
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct HostVolume {
    pub name: String,
    pub host_path: String,
    pub mount_path: String,
    pub is_readonly: bool,
}

#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct EmptyDirVolume {
    pub name: String,
    pub mount_path: String,
    pub is_readonly: bool,
}

#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct PvcVolume {
    pub name: String,
    pub mount_path: String,
    pub sub_path: Option<String>,
    pub is_readonly: bool,
    pub from_app: Option<String>,
    pub from_ns: Option<String>,
}

#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct SecretMount {
    pub name: String,
    pub mount_path: String,
}

#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct SecretRef {
    pub secret: String,
    pub key: String,
}

#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub enum EnvVar {
    StringLike(StringLike),
    SecretRef(SecretRef),
}

#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub struct Ports {
    pub udp: BTreeMap<u16, u16>,
    pub tcp: BTreeMap<u16, u16>,
}

impl Ports {
    pub fn extend(&mut self, other: Self) {
        self.udp.extend(other.udp);
        self.tcp.extend(other.tcp);
    }
}

pub fn port_config_for_pod_or_ingress(ports: Ports) -> Vec<networkpolicy::PortConfig> {
    let mut port_configs = Vec::new();
    for (_public_port, target_port) in ports.tcp {
        port_configs.push(networkpolicy::PortConfig {
            port: target_port,
            protocol: networkpolicy::Protocol::Tcp,
        });
    }
    for (_public_port, target_port) in ports.udp {
        port_configs.push(networkpolicy::PortConfig {
            port: target_port,
            protocol: networkpolicy::Protocol::Udp,
        });
    }
    port_configs
}

pub fn port_config_for_service_egress_only(ports: Ports) -> Vec<networkpolicy::PortConfig> {
    let mut port_configs = Vec::new();
    for (public_port, _target_port) in ports.tcp {
        port_configs.push(networkpolicy::PortConfig {
            port: public_port,
            protocol: networkpolicy::Protocol::Tcp,
        });
    }
    for (public_port, _target_port) in ports.udp {
        port_configs.push(networkpolicy::PortConfig {
            port: public_port,
            protocol: networkpolicy::Protocol::Udp,
        });
    }
    port_configs
}

impl Ports {
    pub fn keys(&self) -> Vec<u16> {
        self.tcp
            .keys()
            .copied()
            .chain(self.udp.clone().keys().copied())
            .sorted()
            .dedup()
            .collect()
    }

    pub fn values(&self) -> Vec<u16> {
        self.tcp
            .values()
            .copied()
            .chain(self.udp.clone().values().copied())
            .sorted()
            .dedup()
            .collect()
    }

    pub fn is_empty(&self) -> bool {
        self.tcp.is_empty() && self.udp.is_empty()
    }

    pub fn append(&mut self, other: &Ports) {
        self.tcp.extend(other.tcp.clone());
        self.udp.extend(other.udp.clone());
    }
}

#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct Service {
    pub target_container: String,
    pub r#type: ServiceType,
    pub ports: Ports,
    // Force a fixed cluster IP. Not recommended
    pub cluster_ip: Option<String>,
    pub network_policy: Option<NetworkPolicyIngress>,
    #[serde(default)]
    pub traefik_h2c: bool,
}

#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub enum ServiceType {
    LoadBalancer,
    ClusterIp,
}

#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct Container {
    pub cap_add: Vec<String>,
    pub cap_drop: Vec<String>,
    pub command: Option<Vec<String>>,
    pub entrypoint: Option<Vec<String>>,
    pub environment: BTreeMap<String, EnvVar>,
    pub hostname: Option<String>,
    pub image: String,
    pub privileged: bool,
    pub restart: Option<String>,
    pub stop_grace_period: Option<String>,
    pub uid: Option<u32>,
    pub gid: Option<u32>,
    pub volumes: Vec<Volume>,
    pub working_dir: Option<String>,
    pub shm_size: Option<StringOrNumber>,
    pub ulimits: Option<serde_json::Value>,
    pub host_network: bool,
    /// Ports this container exposes to other containers
    pub exposes: SvcPorts,
    pub fs_group: Option<u32>,
    pub service_account: Option<ServiceAccount>,
    pub network_policy: Option<NetworkPolicy>,
    pub has_permissions: HashSet<String>,
    pub additional_labels: BTreeMap<String, String>,
    #[serde(default)]
    pub sysctls: BTreeMap<String, String>,
    pub read_only_rootfs: Option<bool>,
    pub tty: Option<bool>,
    pub stdin_open: Option<bool>,
    pub host_pid: Option<bool>,
    pub dns_policy: Option<String>,
}

#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct CronJob {
    pub container: Container,
    /// The cron schedule at which to execute this service (only if type is CronJob)
    pub schedule: String,
}

#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct Job {
    pub container: Container,
}

#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct Deployment {
    pub container: Container,
    // TODO: Find a better place for middlewares
    #[serde(default)]
    pub middlewares: BTreeMap<String, k8s_crds_traefik::MiddlewareSpec>,
}

#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct StatefulSet {
    pub container: Container,
    // TODO: Find a better place for middlewares
    #[serde(default)]
    pub middlewares: BTreeMap<String, k8s_crds_traefik::MiddlewareSpec>,
}

#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq)]
pub struct ServiceAccountInner {
    pub rules: Vec<PolicyRule>,
    pub builtin_roles: Vec<String>,
}

#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq)]
pub struct ServiceAccount {
    pub cluster_rules: Vec<PolicyRule>,
    pub builtin_cluster_roles: Vec<String>,
    pub inner: ServiceAccountInner,
    pub other_ns: BTreeMap<String, ServiceAccountInner>,
}

#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct Secret {
    pub name: String,
    pub data: BTreeMap<String, Box<[u8]>>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PluginType {
    Context,
    Source,
    Runtime,
    CustomResource,
    Ingress,
    DnsProvider,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PluginInfo {
    pub display_name: String,
    pub r#type: PluginType,
    pub endpoint: String,
}

#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct CustomResource {
    pub app: String,
    pub plugin_id: String,
    pub definition: serde_yaml::Value,
}

#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct InternalAppRepresentation {
    pub metadata: Metadata,
    pub containers: BTreeMap<String, Runnable>,
    pub services: BTreeMap<String, Service>,
    pub ingress: Vec<Ingress>,
    pub plugins: BTreeMap<String, PluginInfo>,
    pub secrets: Vec<Secret>,
    pub custom_resources: Vec<CustomResource>,
    pub app_network_policy: Option<NetworkPolicy>,
    pub other: Vec<serde_yaml::Value>,
}

#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub enum IngressType {
    // Fallback ingress for direct HTTP requests
    HttpFallback,
    // Default ingress for HTTPS requests
    #[default]
    Https,
    // Ingress for TCP requests (SNI routing)
    TlsTcp,
}

#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct Ingress {
    pub target_service: Option<String>,
    pub target_ns: Option<String>,
    pub target_app: Option<String>,
    pub path_prefix: Option<String>,
    pub target_port: Option<u16>,
    pub r#type: IngressType,
    // These properties are ignored for TlsTcp ingress
    pub enable_compression: bool,
    pub strip_prefix: bool,
    pub auth_exclude: bool,
    pub component: Option<String>,
    #[serde(default)]
    pub use_https: bool,
    #[serde(default)]
    pub tls_insecure: bool,
}

impl InternalAppRepresentation {
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        metadata: Metadata,
        containers: BTreeMap<String, Runnable>,
        services: BTreeMap<String, Service>,
        ingress: Vec<Ingress>,
        secrets: Vec<Secret>,
        plugins: BTreeMap<String, PluginInfo>,
        custom_resources: Vec<CustomResource>,
        app_network_policy: Option<NetworkPolicy>,
        other: Vec<serde_yaml::Value>,
    ) -> Self {
        let mut tmp = Self {
            metadata,
            containers,
            services,
            ingress,
            plugins,
            secrets,
            custom_resources,
            app_network_policy,
            other,
        };
        tmp.finalize();
        tmp
    }

    // Some basic validations that are not particularly relevant but prevent some malformed apps
    pub fn finalize(&mut self) {
        self.metadata.supports_ingress = !self.ingress.is_empty();
        self.metadata.can_be_protected =
            !self.ingress.is_empty() && self.ingress.iter().any(|i| !i.auth_exclude);
        self.metadata.dependencies.append(
            &mut self
                .custom_resources
                .iter()
                .map(|cr| Dependency::OneDependency(cr.app.clone()))
                .collect(),
        );
        if !self
            .metadata
            .allowed_scopes
            .contains(&self.metadata.default_scope)
        {
            self.metadata
                .allowed_scopes
                .push(self.metadata.default_scope);
        }
        self.metadata.is_snapshot_compatible = Some(self.supports_snapshots());
    }

    pub fn get_public_ports(&self) -> Vec<u16> {
        let mut ports = Vec::new();
        for service in self.services.values() {
            if service.r#type == ServiceType::LoadBalancer {
                let mut new_ports = service.ports.keys();
                ports.append(&mut new_ports)
            }
        }
        ports.sort();
        ports.dedup();
        ports
    }

    pub fn get_ingress(&self) -> &Vec<Ingress> {
        &self.ingress
    }

    pub fn into_ingress(self) -> Vec<Ingress> {
        self.ingress
    }

    pub fn get_metadata(&self) -> &Metadata {
        &self.metadata
    }

    pub fn get_metadata_mut(&mut self) -> &mut Metadata {
        &mut self.metadata
    }

    pub fn into_metadata(self) -> Metadata {
        self.metadata
    }

    pub fn get_plugins(&self) -> &BTreeMap<String, PluginInfo> {
        &self.plugins
    }

    pub fn into_plugins(self) -> BTreeMap<String, PluginInfo> {
        self.plugins
    }

    pub fn set_runtime(&mut self, runtime: Runtime) {
        self.metadata.runtime = runtime;
    }

    pub fn supports_snapshots(&self) -> bool {
        if let Some(supported) = self.metadata.is_snapshot_compatible {
            return supported;
        }
        if !self.other.is_empty() || !self.custom_resources.is_empty() {
            return false;
        }
        // Otherwise, check if all containers only mount host paths from /dev, /sys, /proc and /run,
        // and only mount PVCs from themselves
        for container in self.containers.values() {
            for volume in &container.get_container().volumes {
                match volume {
                    Volume::Host(host_vol) => {
                        if !(host_vol.host_path.starts_with("/dev")
                            || host_vol.host_path.starts_with("/sys")
                            || host_vol.host_path.starts_with("/proc")
                            || host_vol.host_path.starts_with("/run"))
                        {
                            return false;
                        }
                    }
                    Volume::Pvc(pvc_vol) => {
                        if pvc_vol.from_app.is_some() || pvc_vol.from_ns.is_some() {
                            return false;
                        }
                    }
                    Volume::Secret(_) => return false,
                    _ => {}
                }
            }
        }

        true
    }
}
