//! Deployment service implementation.
//!
//! Provides CRUD for Deployment resources and treats them as first-class
//! tenant-scoped objects persisted in FlareDB.

use crate::auth::{
    get_tenant_context, resolve_tenant_ids_from_context, resource_for_tenant, AuthService,
};
use crate::storage::Storage;
use chrono::Utc;
use k8shost_proto::{
    deployment_service_server::DeploymentService, CreateDeploymentRequest,
    CreateDeploymentResponse, DeleteDeploymentRequest, DeleteDeploymentResponse,
    GetDeploymentRequest, GetDeploymentResponse, ListDeploymentsRequest, ListDeploymentsResponse,
    UpdateDeploymentRequest, UpdateDeploymentResponse,
};
use sha2::{Digest, Sha256};
use std::sync::Arc;
use tonic::{Request, Response, Status};
use uuid::Uuid;

const ACTION_DEPLOYMENT_CREATE: &str = "k8s:deployments:create";
const ACTION_DEPLOYMENT_READ: &str = "k8s:deployments:read";
const ACTION_DEPLOYMENT_LIST: &str = "k8s:deployments:list";
const ACTION_DEPLOYMENT_UPDATE: &str = "k8s:deployments:update";
const ACTION_DEPLOYMENT_DELETE: &str = "k8s:deployments:delete";

pub(crate) const DEPLOYMENT_NAME_ANNOTATION: &str = "k8shost.ultracloud.io/deployment-name";
pub(crate) const DEPLOYMENT_UID_ANNOTATION: &str = "k8shost.ultracloud.io/deployment-uid";
pub(crate) const TEMPLATE_HASH_ANNOTATION: &str = "k8shost.ultracloud.io/template-hash";

#[derive(Clone)]
pub struct DeploymentServiceImpl {
    storage: Arc<Storage>,
    auth: Arc<AuthService>,
}

impl DeploymentServiceImpl {
    pub fn new(storage: Arc<Storage>, auth: Arc<AuthService>) -> Self {
        Self { storage, auth }
    }

    pub fn to_proto_deployment(
        deployment: &k8shost_types::Deployment,
    ) -> k8shost_proto::Deployment {
        k8shost_proto::Deployment {
            metadata: Some(to_proto_meta(&deployment.metadata)),
            spec: Some(k8shost_proto::DeploymentSpec {
                replicas: deployment.spec.replicas,
                selector: Some(k8shost_proto::LabelSelector {
                    match_labels: deployment.spec.selector.match_labels.clone(),
                }),
                template: Some(k8shost_proto::PodTemplateSpec {
                    metadata: Some(to_proto_meta(&deployment.spec.template.metadata)),
                    spec: Some(to_proto_pod_spec(&deployment.spec.template.spec)),
                }),
            }),
            status: deployment
                .status
                .as_ref()
                .map(|status| k8shost_proto::DeploymentStatus {
                    replicas: status.replicas,
                    ready_replicas: status.ready_replicas,
                    available_replicas: status.available_replicas,
                }),
        }
    }

    pub fn from_proto_deployment(
        deployment: &k8shost_proto::Deployment,
    ) -> Result<k8shost_types::Deployment, Status> {
        let metadata = deployment
            .metadata
            .as_ref()
            .ok_or_else(|| Status::invalid_argument("metadata is required"))?;
        let spec = deployment
            .spec
            .as_ref()
            .ok_or_else(|| Status::invalid_argument("spec is required"))?;
        let selector = spec
            .selector
            .as_ref()
            .ok_or_else(|| Status::invalid_argument("selector is required"))?;
        let template = spec
            .template
            .as_ref()
            .ok_or_else(|| Status::invalid_argument("template is required"))?;
        let template_meta = template
            .metadata
            .as_ref()
            .ok_or_else(|| Status::invalid_argument("template.metadata is required"))?;
        let template_spec = template
            .spec
            .as_ref()
            .ok_or_else(|| Status::invalid_argument("template.spec is required"))?;

        Ok(k8shost_types::Deployment {
            metadata: from_proto_meta(metadata)?,
            spec: k8shost_types::DeploymentSpec {
                replicas: spec.replicas,
                selector: k8shost_types::LabelSelector {
                    match_labels: selector.match_labels.clone(),
                },
                template: k8shost_types::PodTemplateSpec {
                    metadata: from_proto_meta(template_meta)?,
                    spec: from_proto_pod_spec(template_spec),
                },
            },
            status: deployment
                .status
                .as_ref()
                .map(|status| k8shost_types::DeploymentStatus {
                    replicas: status.replicas,
                    ready_replicas: status.ready_replicas,
                    available_replicas: status.available_replicas,
                }),
        })
    }

    pub(crate) fn validate_spec(deployment: &k8shost_types::Deployment) -> Result<(), Status> {
        if deployment.metadata.namespace.is_none() {
            return Err(Status::invalid_argument("namespace is required"));
        }
        if deployment.spec.replicas.unwrap_or(1) < 0 {
            return Err(Status::invalid_argument("replicas must be >= 0"));
        }
        if deployment.spec.selector.match_labels.is_empty() {
            return Err(Status::invalid_argument(
                "selector.match_labels must not be empty",
            ));
        }
        if deployment.spec.template.spec.containers.is_empty() {
            return Err(Status::invalid_argument(
                "template.spec.containers must not be empty",
            ));
        }
        let template_labels = &deployment.spec.template.metadata.labels;
        let selector_matches_template = deployment
            .spec
            .selector
            .match_labels
            .iter()
            .all(|(key, value)| template_labels.get(key) == Some(value));
        if !selector_matches_template {
            return Err(Status::invalid_argument(
                "selector.match_labels must be present in template.metadata.labels",
            ));
        }
        Ok(())
    }

    pub(crate) fn pod_is_owned_by_deployment(
        deployment: &k8shost_types::Deployment,
        pod: &k8shost_types::Pod,
    ) -> bool {
        let annotations = &pod.metadata.annotations;
        if let Some(uid) = deployment.metadata.uid.as_deref() {
            if annotations
                .get(DEPLOYMENT_UID_ANNOTATION)
                .map(String::as_str)
                == Some(uid)
            {
                return true;
            }
        }
        annotations
            .get(DEPLOYMENT_NAME_ANNOTATION)
            .map(String::as_str)
            == Some(deployment.metadata.name.as_str())
    }
}

pub(crate) fn deployment_template_hash(
    deployment: &k8shost_types::Deployment,
) -> Result<String, Status> {
    let payload = serde_json::to_vec(&deployment.spec.template)
        .map_err(|e| Status::internal(format!("failed to hash deployment template: {}", e)))?;
    let mut hasher = Sha256::new();
    hasher.update(payload);
    Ok(format!("{:x}", hasher.finalize()))
}

fn default_status() -> k8shost_types::DeploymentStatus {
    k8shost_types::DeploymentStatus {
        replicas: Some(0),
        ready_replicas: Some(0),
        available_replicas: Some(0),
    }
}

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()
}

fn to_proto_meta(meta: &k8shost_types::ObjectMeta) -> k8shost_proto::ObjectMeta {
    k8shost_proto::ObjectMeta {
        name: meta.name.clone(),
        namespace: meta.namespace.clone(),
        uid: meta.uid.clone(),
        resource_version: meta.resource_version.clone(),
        creation_timestamp: meta.creation_timestamp.map(|ts| ts.to_rfc3339()),
        labels: meta.labels.clone(),
        annotations: meta.annotations.clone(),
        org_id: meta.org_id.clone(),
        project_id: meta.project_id.clone(),
    }
}

fn from_proto_meta(meta: &k8shost_proto::ObjectMeta) -> Result<k8shost_types::ObjectMeta, Status> {
    Ok(k8shost_types::ObjectMeta {
        name: meta.name.clone(),
        namespace: meta.namespace.clone(),
        uid: meta.uid.clone(),
        resource_version: meta.resource_version.clone(),
        creation_timestamp: meta
            .creation_timestamp
            .as_ref()
            .and_then(|ts| chrono::DateTime::parse_from_rfc3339(ts).ok())
            .map(|ts| ts.with_timezone(&Utc)),
        labels: meta.labels.clone(),
        annotations: meta.annotations.clone(),
        org_id: meta.org_id.clone(),
        project_id: meta.project_id.clone(),
    })
}

fn to_proto_pod_spec(spec: &k8shost_types::PodSpec) -> k8shost_proto::PodSpec {
    k8shost_proto::PodSpec {
        containers: spec
            .containers
            .iter()
            .map(|container| k8shost_proto::Container {
                name: container.name.clone(),
                image: container.image.clone(),
                command: container.command.clone(),
                args: container.args.clone(),
                ports: container
                    .ports
                    .iter()
                    .map(|port| k8shost_proto::ContainerPort {
                        name: port.name.clone(),
                        container_port: port.container_port,
                        protocol: port.protocol.clone(),
                    })
                    .collect(),
                env: container
                    .env
                    .iter()
                    .map(|env| k8shost_proto::EnvVar {
                        name: env.name.clone(),
                        value: env.value.clone(),
                    })
                    .collect(),
            })
            .collect(),
        restart_policy: spec.restart_policy.clone(),
        node_name: spec.node_name.clone(),
    }
}

fn from_proto_pod_spec(spec: &k8shost_proto::PodSpec) -> k8shost_types::PodSpec {
    k8shost_types::PodSpec {
        containers: spec
            .containers
            .iter()
            .map(|container| k8shost_types::Container {
                name: container.name.clone(),
                image: container.image.clone(),
                command: container.command.clone(),
                args: container.args.clone(),
                ports: container
                    .ports
                    .iter()
                    .map(|port| k8shost_types::ContainerPort {
                        name: port.name.clone(),
                        container_port: port.container_port,
                        protocol: port.protocol.clone(),
                    })
                    .collect(),
                env: container
                    .env
                    .iter()
                    .map(|env| k8shost_types::EnvVar {
                        name: env.name.clone(),
                        value: env.value.clone(),
                    })
                    .collect(),
                resources: None,
            })
            .collect(),
        restart_policy: spec.restart_policy.clone(),
        node_name: spec.node_name.clone(),
    }
}

#[tonic::async_trait]
impl DeploymentService for DeploymentServiceImpl {
    async fn create_deployment(
        &self,
        request: Request<CreateDeploymentRequest>,
    ) -> Result<Response<CreateDeploymentResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();
        let proto_deployment = req
            .deployment
            .ok_or_else(|| Status::invalid_argument("deployment is required"))?;
        let mut deployment = Self::from_proto_deployment(&proto_deployment)?;

        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            deployment.metadata.org_id.as_deref().unwrap_or(""),
            deployment.metadata.project_id.as_deref().unwrap_or(""),
        )?;
        deployment.metadata.org_id = Some(org_id.clone());
        deployment.metadata.project_id = Some(project_id.clone());
        Self::validate_spec(&deployment)?;

        let deployment_key = format!(
            "{}/{}",
            deployment
                .metadata
                .namespace
                .as_deref()
                .unwrap_or("default"),
            deployment.metadata.name
        );
        self.auth
            .authorize(
                &tenant,
                ACTION_DEPLOYMENT_CREATE,
                &resource_for_tenant("deployment", deployment_key, &org_id, &project_id),
            )
            .await?;

        let namespace = deployment
            .metadata
            .namespace
            .as_deref()
            .ok_or_else(|| Status::invalid_argument("namespace is required"))?;
        if self
            .storage
            .get_deployment(&org_id, &project_id, namespace, &deployment.metadata.name)
            .await?
            .is_some()
        {
            return Err(Status::already_exists(format!(
                "Deployment {} already exists",
                deployment.metadata.name
            )));
        }

        if deployment.metadata.uid.is_none() {
            deployment.metadata.uid = Some(Uuid::new_v4().to_string());
        }
        if deployment.metadata.creation_timestamp.is_none() {
            deployment.metadata.creation_timestamp = Some(Utc::now());
        }
        deployment.metadata.resource_version = Some("1".to_string());
        deployment.status = Some(default_status());

        self.storage.put_deployment(&deployment).await?;

        Ok(Response::new(CreateDeploymentResponse {
            deployment: Some(Self::to_proto_deployment(&deployment)),
        }))
    }

    async fn get_deployment(
        &self,
        request: Request<GetDeploymentRequest>,
    ) -> Result<Response<GetDeploymentResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();
        let deployment_key = format!("{}/{}", req.namespace, req.name);
        self.auth
            .authorize(
                &tenant,
                ACTION_DEPLOYMENT_READ,
                &resource_for_tenant(
                    "deployment",
                    deployment_key,
                    &tenant.org_id,
                    &tenant.project_id,
                ),
            )
            .await?;

        let deployment = self
            .storage
            .get_deployment(
                &tenant.org_id,
                &tenant.project_id,
                &req.namespace,
                &req.name,
            )
            .await?;

        match deployment {
            Some(deployment) => Ok(Response::new(GetDeploymentResponse {
                deployment: Some(Self::to_proto_deployment(&deployment)),
            })),
            None => Err(Status::not_found(format!(
                "Deployment {} not found",
                req.name
            ))),
        }
    }

    async fn list_deployments(
        &self,
        request: Request<ListDeploymentsRequest>,
    ) -> Result<Response<ListDeploymentsResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        self.auth
            .authorize(
                &tenant,
                ACTION_DEPLOYMENT_LIST,
                &resource_for_tenant("deployment", "*", &tenant.org_id, &tenant.project_id),
            )
            .await?;
        let req = request.into_inner();

        let items = self
            .storage
            .list_deployments(&tenant.org_id, &tenant.project_id, req.namespace.as_deref())
            .await?
            .iter()
            .map(Self::to_proto_deployment)
            .collect();

        Ok(Response::new(ListDeploymentsResponse { items }))
    }

    async fn update_deployment(
        &self,
        request: Request<UpdateDeploymentRequest>,
    ) -> Result<Response<UpdateDeploymentResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();
        let proto_deployment = req
            .deployment
            .ok_or_else(|| Status::invalid_argument("deployment is required"))?;
        let mut deployment = Self::from_proto_deployment(&proto_deployment)?;

        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            deployment.metadata.org_id.as_deref().unwrap_or(""),
            deployment.metadata.project_id.as_deref().unwrap_or(""),
        )?;
        deployment.metadata.org_id = Some(org_id.clone());
        deployment.metadata.project_id = Some(project_id.clone());
        Self::validate_spec(&deployment)?;

        let namespace = deployment
            .metadata
            .namespace
            .as_deref()
            .ok_or_else(|| Status::invalid_argument("namespace is required"))?;
        let deployment_key = format!("{}/{}", namespace, deployment.metadata.name);
        self.auth
            .authorize(
                &tenant,
                ACTION_DEPLOYMENT_UPDATE,
                &resource_for_tenant("deployment", deployment_key, &org_id, &project_id),
            )
            .await?;

        let current = self
            .storage
            .get_deployment(&org_id, &project_id, namespace, &deployment.metadata.name)
            .await?
            .ok_or_else(|| {
                Status::not_found(format!("Deployment {} not found", deployment.metadata.name))
            })?;

        deployment.metadata.uid = current.metadata.uid.clone();
        deployment.metadata.creation_timestamp = current.metadata.creation_timestamp;
        deployment.metadata.resource_version = Some(next_resource_version(
            current.metadata.resource_version.as_deref(),
        ));
        deployment.status = current.status.clone();

        self.storage.put_deployment(&deployment).await?;

        Ok(Response::new(UpdateDeploymentResponse {
            deployment: Some(Self::to_proto_deployment(&deployment)),
        }))
    }

    async fn delete_deployment(
        &self,
        request: Request<DeleteDeploymentRequest>,
    ) -> Result<Response<DeleteDeploymentResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();
        let deployment_key = format!("{}/{}", req.namespace, req.name);
        self.auth
            .authorize(
                &tenant,
                ACTION_DEPLOYMENT_DELETE,
                &resource_for_tenant(
                    "deployment",
                    deployment_key,
                    &tenant.org_id,
                    &tenant.project_id,
                ),
            )
            .await?;

        let existing = self
            .storage
            .get_deployment(
                &tenant.org_id,
                &tenant.project_id,
                &req.namespace,
                &req.name,
            )
            .await?;
        let Some(deployment) = existing else {
            return Ok(Response::new(DeleteDeploymentResponse { success: false }));
        };

        let success = self
            .storage
            .delete_deployment(
                &tenant.org_id,
                &tenant.project_id,
                &req.namespace,
                &req.name,
            )
            .await?;

        let pods = self
            .storage
            .list_pods(
                &tenant.org_id,
                &tenant.project_id,
                Some(&req.namespace),
                None,
            )
            .await?;
        for pod in pods
            .into_iter()
            .filter(|pod| Self::pod_is_owned_by_deployment(&deployment, pod))
        {
            let namespace = pod.metadata.namespace.as_deref().unwrap_or("default");
            let _ = self
                .storage
                .delete_pod(
                    &tenant.org_id,
                    &tenant.project_id,
                    namespace,
                    &pod.metadata.name,
                )
                .await?;
        }

        Ok(Response::new(DeleteDeploymentResponse { success }))
    }
}

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

    fn test_deployment() -> k8shost_types::Deployment {
        k8shost_types::Deployment {
            metadata: k8shost_types::ObjectMeta {
                name: "web".to_string(),
                namespace: Some("default".to_string()),
                uid: Some("deploy-uid".to_string()),
                resource_version: Some("4".to_string()),
                creation_timestamp: Some(Utc::now()),
                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: Some(default_status()),
        }
    }

    #[test]
    fn deployment_round_trip_proto_conversion() {
        let deployment = test_deployment();
        let proto = DeploymentServiceImpl::to_proto_deployment(&deployment);
        let round_trip = DeploymentServiceImpl::from_proto_deployment(&proto).unwrap();

        assert_eq!(round_trip.metadata.name, deployment.metadata.name);
        assert_eq!(round_trip.spec.replicas, Some(2));
        assert_eq!(
            round_trip
                .spec
                .selector
                .match_labels
                .get("app")
                .map(String::as_str),
            Some("web")
        );
    }

    #[test]
    fn validate_spec_requires_selector_labels_on_template() {
        let mut deployment = test_deployment();
        deployment.spec.template.metadata.labels.clear();

        let error = DeploymentServiceImpl::validate_spec(&deployment).unwrap_err();
        assert_eq!(error.code(), tonic::Code::InvalidArgument);
    }

    #[test]
    fn template_hash_changes_when_template_changes() {
        let deployment = test_deployment();
        let mut changed = test_deployment();
        changed.spec.template.spec.containers[0].image = "caddy:latest".to_string();

        let original_hash = deployment_template_hash(&deployment).unwrap();
        let changed_hash = deployment_template_hash(&changed).unwrap();

        assert_ne!(original_hash, changed_hash);
    }

    #[test]
    fn owned_pod_detection_matches_uid_annotation() {
        let deployment = test_deployment();
        let mut pod = k8shost_types::Pod {
            metadata: k8shost_types::ObjectMeta {
                name: "web-1".to_string(),
                namespace: Some("default".to_string()),
                uid: None,
                resource_version: None,
                creation_timestamp: None,
                labels: HashMap::new(),
                annotations: HashMap::from([(
                    DEPLOYMENT_UID_ANNOTATION.to_string(),
                    "deploy-uid".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: None,
        };

        assert!(DeploymentServiceImpl::pod_is_owned_by_deployment(
            &deployment,
            &pod
        ));

        pod.metadata.annotations.clear();
        assert!(!DeploymentServiceImpl::pod_is_owned_by_deployment(
            &deployment,
            &pod
        ));
    }
}
