//! Background reconciler for Deployment resources.

use crate::services::deployment::{
    deployment_template_hash, DeploymentServiceImpl, DEPLOYMENT_NAME_ANNOTATION,
    DEPLOYMENT_UID_ANNOTATION, TEMPLATE_HASH_ANNOTATION,
};
use crate::storage::Storage;
use chrono::Utc;
use k8shost_types::{Deployment, DeploymentStatus, Pod, PodStatus};
use std::collections::BTreeSet;
use std::sync::Arc;
use std::time::Duration;
use tokio::time::sleep;
use tonic::Status;
use tracing::{debug, info, warn};
use uuid::Uuid;

pub struct DeploymentController {
    storage: Arc<Storage>,
    interval: Duration,
}

#[derive(Debug)]
struct DeploymentPlan {
    creates: Vec<Pod>,
    deletes: Vec<String>,
    status: DeploymentStatus,
}

impl DeploymentController {
    pub fn new(storage: Arc<Storage>) -> Self {
        Self {
            storage,
            interval: Duration::from_secs(5),
        }
    }

    pub async fn run(self: Arc<Self>) {
        info!(
            "Deployment controller started ({}s interval)",
            self.interval.as_secs()
        );

        loop {
            if let Err(error) = self.reconcile_all().await {
                warn!(error = %error, "deployment reconciliation failed");
            }
            sleep(self.interval).await;
        }
    }

    async fn reconcile_all(&self) -> anyhow::Result<()> {
        let mut deployments = self.storage.list_all_deployments().await?;
        deployments.sort_by(|lhs, rhs| {
            lhs.metadata
                .org_id
                .cmp(&rhs.metadata.org_id)
                .then_with(|| lhs.metadata.project_id.cmp(&rhs.metadata.project_id))
                .then_with(|| lhs.metadata.namespace.cmp(&rhs.metadata.namespace))
                .then_with(|| lhs.metadata.name.cmp(&rhs.metadata.name))
        });

        for deployment in deployments {
            if let Err(error) = self.reconcile_deployment(deployment).await {
                warn!(error = %error, "failed to reconcile deployment");
            }
        }

        Ok(())
    }

    async fn reconcile_deployment(&self, deployment: Deployment) -> anyhow::Result<()> {
        let Some(org_id) = deployment.metadata.org_id.as_deref() else {
            warn!(deployment = %deployment.metadata.name, "deployment missing org_id");
            return Ok(());
        };
        let Some(project_id) = deployment.metadata.project_id.as_deref() else {
            warn!(deployment = %deployment.metadata.name, "deployment missing project_id");
            return Ok(());
        };
        let Some(namespace) = deployment.metadata.namespace.as_deref() else {
            warn!(deployment = %deployment.metadata.name, "deployment missing namespace");
            return Ok(());
        };

        let pods = self
            .storage
            .list_pods(org_id, project_id, Some(namespace), None)
            .await?;
        let plan = plan_deployment_reconciliation(&deployment, &pods)?;

        for pod_name in &plan.deletes {
            let deleted = self
                .storage
                .delete_pod(org_id, project_id, namespace, pod_name)
                .await?;
            if deleted {
                debug!(
                    deployment = %deployment.metadata.name,
                    pod = %pod_name,
                    "deleted deployment-managed pod"
                );
            }
        }

        for pod in &plan.creates {
            self.storage.put_pod(pod).await?;
            debug!(
                deployment = %deployment.metadata.name,
                pod = %pod.metadata.name,
                "created deployment-managed pod"
            );
        }

        let status_changed = !deployment_status_eq(deployment.status.as_ref(), Some(&plan.status));
        if !plan.creates.is_empty() || !plan.deletes.is_empty() || status_changed {
            let mut updated = deployment.clone();
            updated.status = Some(plan.status);
            updated.metadata.resource_version = Some(next_resource_version(
                deployment.metadata.resource_version.as_deref(),
            ));
            self.storage.put_deployment(&updated).await?;
        }

        Ok(())
    }
}

fn plan_deployment_reconciliation(
    deployment: &Deployment,
    pods: &[Pod],
) -> Result<DeploymentPlan, Status> {
    DeploymentServiceImpl::validate_spec(deployment)?;
    let template_hash = deployment_template_hash(deployment)?;
    let desired_replicas = deployment.spec.replicas.unwrap_or(1).max(0) as usize;
    let mut owned = pods
        .iter()
        .filter(|pod| DeploymentServiceImpl::pod_is_owned_by_deployment(deployment, pod))
        .cloned()
        .collect::<Vec<_>>();
    owned.sort_by(|lhs, rhs| lhs.metadata.name.cmp(&rhs.metadata.name));

    let mut current = Vec::new();
    let mut stale = Vec::new();
    for pod in owned {
        let pod_hash = pod
            .metadata
            .annotations
            .get(TEMPLATE_HASH_ANNOTATION)
            .map(String::as_str);
        if pod_hash == Some(template_hash.as_str()) {
            current.push(pod);
        } else {
            stale.push(pod);
        }
    }

    let mut deletes = stale
        .iter()
        .map(|pod| pod.metadata.name.clone())
        .collect::<Vec<_>>();
    let mut survivors = current;
    if survivors.len() > desired_replicas {
        let excess = survivors.split_off(desired_replicas);
        deletes.extend(excess.into_iter().map(|pod| pod.metadata.name));
    }

    let used_ordinals = pods
        .iter()
        .filter(|pod| DeploymentServiceImpl::pod_is_owned_by_deployment(deployment, pod))
        .filter_map(|pod| parse_pod_ordinal(&deployment.metadata.name, &pod.metadata.name))
        .collect::<BTreeSet<_>>();

    let ready_replicas = survivors.iter().filter(|pod| pod_is_ready(pod)).count() as i32;
    let available_replicas = ready_replicas;

    let mut creates = Vec::new();
    let mut used_ordinals = used_ordinals;
    while survivors.len() + creates.len() < desired_replicas {
        let ordinal = next_available_ordinal(&used_ordinals);
        used_ordinals.insert(ordinal);
        creates.push(build_pod(deployment, &template_hash, ordinal)?);
    }

    Ok(DeploymentPlan {
        creates,
        deletes,
        status: DeploymentStatus {
            replicas: Some(desired_replicas as i32),
            ready_replicas: Some(ready_replicas),
            available_replicas: Some(available_replicas),
        },
    })
}

fn build_pod(deployment: &Deployment, template_hash: &str, ordinal: usize) -> Result<Pod, Status> {
    let Some(namespace) = deployment.metadata.namespace.clone() else {
        return Err(Status::invalid_argument("deployment namespace is required"));
    };
    let Some(org_id) = deployment.metadata.org_id.clone() else {
        return Err(Status::invalid_argument("deployment org_id is required"));
    };
    let Some(project_id) = deployment.metadata.project_id.clone() else {
        return Err(Status::invalid_argument(
            "deployment project_id is required",
        ));
    };

    let mut metadata = deployment.spec.template.metadata.clone();
    metadata.name = format!("{}-{}", deployment.metadata.name, ordinal);
    metadata.namespace = Some(namespace);
    metadata.org_id = Some(org_id);
    metadata.project_id = Some(project_id);
    metadata.uid = Some(Uuid::new_v4().to_string());
    metadata.resource_version = Some("1".to_string());
    metadata.creation_timestamp = Some(Utc::now());
    metadata.annotations.insert(
        DEPLOYMENT_NAME_ANNOTATION.to_string(),
        deployment.metadata.name.clone(),
    );
    if let Some(uid) = deployment.metadata.uid.as_ref() {
        metadata
            .annotations
            .insert(DEPLOYMENT_UID_ANNOTATION.to_string(), uid.clone());
    }
    metadata.annotations.insert(
        TEMPLATE_HASH_ANNOTATION.to_string(),
        template_hash.to_string(),
    );

    let mut spec = deployment.spec.template.spec.clone();
    spec.node_name = None;

    Ok(Pod {
        metadata,
        spec,
        status: Some(PodStatus {
            phase: Some("Pending".to_string()),
            pod_ip: None,
            host_ip: None,
            conditions: Vec::new(),
        }),
    })
}

fn pod_is_ready(pod: &Pod) -> bool {
    pod.status
        .as_ref()
        .and_then(|status| status.phase.as_deref())
        == Some("Running")
}

fn parse_pod_ordinal(deployment_name: &str, pod_name: &str) -> Option<usize> {
    let suffix = pod_name.strip_prefix(&format!("{}-", deployment_name))?;
    suffix.parse::<usize>().ok()
}

fn next_available_ordinal(used: &BTreeSet<usize>) -> usize {
    let mut ordinal = 1usize;
    while used.contains(&ordinal) {
        ordinal += 1;
    }
    ordinal
}

fn deployment_status_eq(lhs: Option<&DeploymentStatus>, rhs: Option<&DeploymentStatus>) -> bool {
    match (lhs, rhs) {
        (None, None) => true,
        (Some(lhs), Some(rhs)) => {
            lhs.replicas == rhs.replicas
                && lhs.ready_replicas == rhs.ready_replicas
                && lhs.available_replicas == rhs.available_replicas
        }
        _ => false,
    }
}

fn next_resource_version(current: Option<&str>) -> String {
    let current = current
        .and_then(|version| version.parse::<u64>().ok())
        .unwrap_or(0);
    (current + 1).to_string()
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashMap;

    fn test_deployment() -> Deployment {
        Deployment {
            metadata: k8shost_types::ObjectMeta {
                name: "web".to_string(),
                namespace: Some("default".to_string()),
                uid: Some("deployment-uid".to_string()),
                resource_version: Some("1".to_string()),
                creation_timestamp: None,
                labels: HashMap::new(),
                annotations: HashMap::new(),
                org_id: Some("test-org".to_string()),
                project_id: Some("test-project".to_string()),
            },
            spec: k8shost_types::DeploymentSpec {
                replicas: Some(2),
                selector: k8shost_types::LabelSelector {
                    match_labels: HashMap::from([("app".to_string(), "web".to_string())]),
                },
                template: k8shost_types::PodTemplateSpec {
                    metadata: k8shost_types::ObjectMeta {
                        name: "".to_string(),
                        namespace: Some("default".to_string()),
                        uid: None,
                        resource_version: None,
                        creation_timestamp: None,
                        labels: HashMap::from([("app".to_string(), "web".to_string())]),
                        annotations: HashMap::new(),
                        org_id: None,
                        project_id: None,
                    },
                    spec: k8shost_types::PodSpec {
                        containers: vec![k8shost_types::Container {
                            name: "web".to_string(),
                            image: "nginx:latest".to_string(),
                            command: Vec::new(),
                            args: Vec::new(),
                            ports: Vec::new(),
                            env: Vec::new(),
                            resources: None,
                        }],
                        restart_policy: Some("Always".to_string()),
                        node_name: None,
                    },
                },
            },
            status: None,
        }
    }

    fn owned_pod(name: &str, hash: &str, phase: &str) -> Pod {
        Pod {
            metadata: k8shost_types::ObjectMeta {
                name: name.to_string(),
                namespace: Some("default".to_string()),
                uid: None,
                resource_version: None,
                creation_timestamp: None,
                labels: HashMap::from([("app".to_string(), "web".to_string())]),
                annotations: HashMap::from([
                    (DEPLOYMENT_NAME_ANNOTATION.to_string(), "web".to_string()),
                    (
                        DEPLOYMENT_UID_ANNOTATION.to_string(),
                        "deployment-uid".to_string(),
                    ),
                    (TEMPLATE_HASH_ANNOTATION.to_string(), hash.to_string()),
                ]),
                org_id: Some("test-org".to_string()),
                project_id: Some("test-project".to_string()),
            },
            spec: k8shost_types::PodSpec {
                containers: Vec::new(),
                restart_policy: None,
                node_name: None,
            },
            status: Some(PodStatus {
                phase: Some(phase.to_string()),
                pod_ip: None,
                host_ip: None,
                conditions: Vec::new(),
            }),
        }
    }

    #[test]
    fn plan_creates_missing_replicas() {
        let deployment = test_deployment();
        let plan = plan_deployment_reconciliation(&deployment, &[]).unwrap();

        assert_eq!(plan.creates.len(), 2);
        assert!(plan.deletes.is_empty());
        assert_eq!(plan.status.replicas, Some(2));
    }

    #[test]
    fn plan_scales_down_extra_pods() {
        let mut deployment = test_deployment();
        deployment.spec.replicas = Some(1);
        let hash = deployment_template_hash(&deployment).unwrap();
        let pods = vec![
            owned_pod("web-1", &hash, "Running"),
            owned_pod("web-2", &hash, "Running"),
        ];

        let plan = plan_deployment_reconciliation(&deployment, &pods).unwrap();

        assert!(plan.creates.is_empty());
        assert_eq!(plan.deletes, vec!["web-2".to_string()]);
        assert_eq!(plan.status.ready_replicas, Some(1));
    }

    #[test]
    fn plan_replaces_stale_template_pods() {
        let deployment = test_deployment();
        let pods = vec![
            owned_pod("web-1", "stale", "Running"),
            owned_pod("web-2", "stale", "Running"),
        ];

        let plan = plan_deployment_reconciliation(&deployment, &pods).unwrap();

        assert_eq!(plan.creates.len(), 2);
        assert_eq!(plan.deletes.len(), 2);
        assert_eq!(plan.status.ready_replicas, Some(0));
    }
}
