//! Controller for the `App` CRD — manages Deployment + Service.

use crate::context::ReconcileState;
use crate::controller::helpers::{
    child_meta, env_var_to_json, merge_env, owner_ref, query_deployment_status, std_labels,
};
use crate::crd::{App, AppSpec};
use serde_json::{Value, json};
use std::sync::Arc;
use tracing::info;

/// Reconcile an App resource: create/update Deployment + Service.
pub async fn reconcile(app: Arc<App>, ctx: Arc<ReconcileState>) -> Result<(), kube::Error> {
    let ns = app.metadata.namespace.as_deref().unwrap_or("default");
    let name = app.metadata.name.as_deref().unwrap_or("");
    let spec = &app.spec;
    let client = &ctx.client;

    let or = owner_ref(&app.metadata, &app.api_version, &app.kind);
    let labels = std_labels();

    // ---- Deployment ----
    let deployment = build_deployment(ns, name, spec, &or, &labels);
    apply_deployment(client, ns, name, &deployment).await?;

    // ---- Service ----
    let service = build_service(ns, name, &or, &labels);
    apply_service(client, ns, name, &service).await?;

    // ---- Status patch ----
    let (ready_replicas, phase) = query_deployment_status(client, ns, name).await?;
    let status = json!({
        "status": {
            "readyReplicas": ready_replicas,
            "phase": phase
        }
    });
    patch_status::<App>(client, ns, name, &status).await?;

    Ok(())
}

fn build_deployment(
    ns: &str,
    name: &str,
    spec: &AppSpec,
    or: &crate::crd::OwnerReference,
    labels: &std::collections::BTreeMap<String, String>,
) -> Value {
    let env = merge_env(&[], &spec.env);
    let image = if spec.image.is_empty() {
        "myapp/app:latest".to_string()
    } else {
        spec.image.clone()
    };
    let pull = if spec.image_pull_policy.is_empty() {
        "IfNotPresent".to_string()
    } else {
        spec.image_pull_policy.clone()
    };

    let resources = build_resources(&spec.resources);
    let liveness = spec.liveness_probe.as_ref().map(|p| {
        json!({
            "httpGet": { "path": p.path, "port": p.port },
            "initialDelaySeconds": p.initial_delay_seconds,
            "periodSeconds": 10,
        })
    });
    let readiness = spec.readiness_probe.as_ref().map(|p| {
        json!({
            "httpGet": { "path": p.path, "port": p.port },
            "initialDelaySeconds": p.initial_delay_seconds,
            "periodSeconds": 5,
        })
    });

    json!({
        "metadata": child_meta(name, ns, or, labels.clone()),
        "spec": {
            "replicas": spec.replicas,
            "selector": { "matchLabels": labels },
            "strategy": {
                "type": "RollingUpdate",
                "rollingUpdate": { "maxSurge": 1, "maxUnavailable": 0 }
            },
            "template": {
                "metadata": { "labels": labels.clone() },
                "spec": {
                    "containers": [{
                        "name": "app",
                        "image": image,
                        "ports": [{ "containerPort": 8080 }],
                        "env": env.iter().map(env_var_to_json).collect::<Vec<_>>(),
                        "imagePullPolicy": pull,
                        "resources": resources,
                        "livenessProbe": liveness,
                        "readinessProbe": readiness,
                    }]
                }
            }
        }
    })
}

fn build_service(
    ns: &str,
    name: &str,
    or: &crate::crd::OwnerReference,
    labels: &std::collections::BTreeMap<String, String>,
) -> Value {
    json!({
        "metadata": child_meta(name, ns, or, labels.clone()),
        "spec": {
            "ports": [{ "port": 80, "targetPort": 8080, "name": "http" }],
            "selector": labels.clone(),
            "type": "ClusterIP"
        }
    })
}

pub(crate) fn build_resources(res: &Option<crate::crd::ResourceRequirements>) -> Value {
    match res {
        Some(r) => {
            let mut out = serde_json::Map::new();
            if let Some(ref req) = r.requests {
                let mut req_map = serde_json::Map::new();
                if let Some(ref cpu) = req.cpu {
                    req_map.insert("cpu".to_string(), json!(cpu));
                }
                if let Some(ref mem) = req.memory {
                    req_map.insert("memory".to_string(), json!(mem));
                }
                if !req_map.is_empty() {
                    out.insert("requests".to_string(), Value::Object(req_map));
                }
            }
            if let Some(ref lim) = r.limits {
                let mut lim_map = serde_json::Map::new();
                if let Some(ref cpu) = lim.cpu {
                    lim_map.insert("cpu".to_string(), json!(cpu));
                }
                if let Some(ref mem) = lim.memory {
                    lim_map.insert("memory".to_string(), json!(mem));
                }
                if !lim_map.is_empty() {
                    out.insert("limits".to_string(), Value::Object(lim_map));
                }
            }
            if out.is_empty() {
                json!({})
            } else {
                Value::Object(out)
            }
        }
        None => json!({}),
    }
}

pub(crate) async fn apply_deployment(
    client: &kube::Client,
    ns: &str,
    name: &str,
    body: &Value,
) -> Result<(), kube::Error> {
    let api: kube::Api<crate::crd::JsonResource> = kube::Api::namespaced(client.clone(), ns);
    let jr = crate::crd::JsonResource::new(Default::default(), body.clone());
    match api.get(name).await {
        Ok(_) => {
            info!(name, ns, "updating app deployment");
            let _ = api
                .replace(name, &kube::api::PostParams::default(), &jr)
                .await?;
        }
        Err(kube::Error::Api(e)) if e.code == 404 => {
            info!(name, ns, "creating app deployment");
            let _ = api.create(&kube::api::PostParams::default(), &jr).await?;
        }
        Err(e) => return Err(e),
    }
    Ok(())
}

pub(crate) async fn apply_service(
    client: &kube::Client,
    ns: &str,
    name: &str,
    body: &Value,
) -> Result<(), kube::Error> {
    let api: kube::Api<crate::crd::JsonResource> = kube::Api::namespaced(client.clone(), ns);
    let jr = crate::crd::JsonResource::new(Default::default(), body.clone());
    match api.get(name).await {
        Ok(_) => {
            let _ = api
                .replace(name, &kube::api::PostParams::default(), &jr)
                .await?;
        }
        Err(kube::Error::Api(e)) if e.code == 404 => {
            let _ = api.create(&kube::api::PostParams::default(), &jr).await?;
        }
        Err(e) => return Err(e),
    }
    Ok(())
}

pub(crate) async fn patch_status<T: Clone + serde::de::DeserializeOwned + std::fmt::Debug>(
    client: &kube::Client,
    ns: &str,
    name: &str,
    body: &Value,
) -> Result<(), kube::Error> {
    let api: kube::Api<crate::crd::JsonResource> = kube::Api::namespaced(client.clone(), ns);
    let _ = api
        .patch_status(
            name,
            &kube::api::PatchParams::default(),
            &kube::api::Patch::Merge(body),
        )
        .await?;
    Ok(())
}
