//! Pod service implementation
//!
//! Handles CRUD operations for Kubernetes Pods with multi-tenant support.

use crate::auth::{
    get_tenant_context, resolve_tenant_ids_from_context, resource_for_tenant, AuthService,
};
use crate::storage::Storage;
use chrono::Utc;
use creditservice_client::Client as CreditServiceClient;
use creditservice_client::ResourceType;
use k8shost_proto::{
    pod_service_server::PodService, CreatePodRequest, CreatePodResponse, DeletePodRequest,
    DeletePodResponse, GetPodRequest, GetPodResponse, ListPodsRequest, ListPodsResponse,
    UpdatePodRequest, UpdatePodResponse, WatchEvent, WatchPodsRequest,
};
use k8shost_types::PodStatus;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio_stream::wrappers::ReceiverStream;
use tonic::{Request, Response, Status};
use uuid::Uuid;

const ACTION_POD_CREATE: &str = "k8s:pods:create";
const ACTION_POD_READ: &str = "k8s:pods:read";
const ACTION_POD_LIST: &str = "k8s:pods:list";
const ACTION_POD_UPDATE: &str = "k8s:pods:update";
const ACTION_POD_DELETE: &str = "k8s:pods:delete";

/// Pod service implementation with storage backend
#[derive(Clone)]
pub struct PodServiceImpl {
    storage: Arc<Storage>,
    /// CreditService client (optional, for admission control)
    credit_service: Option<Arc<RwLock<CreditServiceClient>>>,
    auth: Arc<AuthService>,
}

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

    pub async fn new_with_credit_service(
        storage: Arc<Storage>,
        auth: Arc<AuthService>,
        endpoint: Option<&str>,
    ) -> Self {
        // Initialize CreditService client if endpoint is configured
        let credit_service = match endpoint {
            Some(endpoint) if !endpoint.trim().is_empty() => {
                match CreditServiceClient::connect(endpoint).await {
                    Ok(client) => {
                        tracing::info!("CreditService admission control enabled: {}", endpoint);
                        Some(Arc::new(RwLock::new(client)))
                    }
                    Err(e) => {
                        tracing::warn!(
                            "Failed to connect to CreditService (admission control disabled): {}",
                            e
                        );
                        None
                    }
                }
            }
            _ => {
                tracing::info!("CreditService endpoint not configured, admission control disabled");
                None
            }
        };

        Self {
            storage,
            credit_service,
            auth,
        }
    }

    /// Calculate estimated cost for a Pod based on resource requests
    fn calculate_pod_cost(pod: &k8shost_types::Pod) -> i64 {
        let mut total_cpu_millicores = 0i64;
        let mut total_memory_mb = 0i64;

        for container in &pod.spec.containers {
            if let Some(ref resources) = container.resources {
                // Parse CPU requests (e.g., "100m" = 100 millicores, "1" = 1000 millicores)
                if let Some(cpu_str) = resources.requests.get("cpu") {
                    if let Some(millicores) = Self::parse_cpu(cpu_str) {
                        total_cpu_millicores += millicores;
                    }
                }

                // Parse memory requests (e.g., "128Mi", "1Gi")
                if let Some(mem_str) = resources.requests.get("memory") {
                    if let Some(mb) = Self::parse_memory(mem_str) {
                        total_memory_mb += mb;
                    }
                }
            }
        }

        // Default minimums if not specified
        if total_cpu_millicores == 0 {
            total_cpu_millicores = 100; // 0.1 vCPU default
        }
        if total_memory_mb == 0 {
            total_memory_mb = 128; // 128Mi default
        }

        // Pricing: 10 credits per vCPU (1000 millicores) + 5 credits per GB
        let cpu_cost = (total_cpu_millicores * 10) / 1000;
        let memory_gb = (total_memory_mb + 1023) / 1024; // Round up
        let memory_cost = memory_gb * 5;

        cpu_cost + memory_cost
    }

    /// Parse CPU string to millicores (e.g., "100m" -> 100, "1" -> 1000)
    fn parse_cpu(cpu_str: &str) -> Option<i64> {
        if let Some(stripped) = cpu_str.strip_suffix('m') {
            stripped.parse::<i64>().ok()
        } else {
            // Full CPU cores
            cpu_str
                .parse::<f64>()
                .ok()
                .map(|cores| (cores * 1000.0) as i64)
        }
    }

    /// Parse memory string to MB (e.g., "128Mi" -> 128, "1Gi" -> 1024)
    fn parse_memory(mem_str: &str) -> Option<i64> {
        if let Some(stripped) = mem_str.strip_suffix("Mi") {
            stripped.parse::<i64>().ok()
        } else if let Some(stripped) = mem_str.strip_suffix("Gi") {
            stripped.parse::<i64>().ok().map(|gb| gb * 1024)
        } else if let Some(stripped) = mem_str.strip_suffix("M") {
            stripped.parse::<i64>().ok()
        } else if let Some(stripped) = mem_str.strip_suffix("G") {
            stripped.parse::<i64>().ok().map(|gb| gb * 1000)
        } else {
            // Try parsing as bytes
            mem_str
                .parse::<i64>()
                .ok()
                .map(|bytes| bytes / (1024 * 1024))
        }
    }

    /// Convert k8shost_types::Pod to proto Pod
    pub fn to_proto_pod(pod: &k8shost_types::Pod) -> k8shost_proto::Pod {
        // Convert metadata
        let metadata = Some(k8shost_proto::ObjectMeta {
            name: pod.metadata.name.clone(),
            namespace: pod.metadata.namespace.clone(),
            uid: pod.metadata.uid.clone(),
            resource_version: pod.metadata.resource_version.clone(),
            creation_timestamp: pod.metadata.creation_timestamp.map(|ts| ts.to_rfc3339()),
            labels: pod.metadata.labels.clone(),
            annotations: pod.metadata.annotations.clone(),
            org_id: pod.metadata.org_id.clone(),
            project_id: pod.metadata.project_id.clone(),
        });

        // Convert spec
        let spec = Some(k8shost_proto::PodSpec {
            containers: pod
                .spec
                .containers
                .iter()
                .map(|c| k8shost_proto::Container {
                    name: c.name.clone(),
                    image: c.image.clone(),
                    command: c.command.clone(),
                    args: c.args.clone(),
                    ports: c
                        .ports
                        .iter()
                        .map(|p| k8shost_proto::ContainerPort {
                            name: p.name.clone(),
                            container_port: p.container_port,
                            protocol: p.protocol.clone(),
                        })
                        .collect(),
                    env: c
                        .env
                        .iter()
                        .map(|e| k8shost_proto::EnvVar {
                            name: e.name.clone(),
                            value: e.value.clone(),
                        })
                        .collect(),
                })
                .collect(),
            restart_policy: pod.spec.restart_policy.clone(),
            node_name: pod.spec.node_name.clone(),
        });

        // Convert status
        let status = pod.status.as_ref().map(|s| k8shost_proto::PodStatus {
            phase: s.phase.clone(),
            pod_ip: s.pod_ip.clone(),
            host_ip: s.host_ip.clone(),
            conditions: s
                .conditions
                .iter()
                .map(|c| k8shost_proto::PodCondition {
                    r#type: c.r#type.clone(),
                    status: c.status.clone(),
                    reason: c.reason.clone(),
                    message: c.message.clone(),
                })
                .collect(),
        });

        k8shost_proto::Pod {
            metadata,
            spec,
            status,
        }
    }

    /// Convert proto Pod to k8shost_types::Pod
    pub fn from_proto_pod(proto: &k8shost_proto::Pod) -> Result<k8shost_types::Pod, Status> {
        let metadata = proto
            .metadata
            .as_ref()
            .ok_or_else(|| Status::invalid_argument("metadata is required"))?;

        let spec = proto
            .spec
            .as_ref()
            .ok_or_else(|| Status::invalid_argument("spec is required"))?;

        // Convert metadata
        let meta = k8shost_types::ObjectMeta {
            name: metadata.name.clone(),
            namespace: metadata.namespace.clone(),
            uid: metadata.uid.clone(),
            resource_version: metadata.resource_version.clone(),
            creation_timestamp: metadata
                .creation_timestamp
                .as_ref()
                .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
                .map(|dt| dt.with_timezone(&Utc)),
            labels: metadata.labels.clone(),
            annotations: metadata.annotations.clone(),
            org_id: metadata.org_id.clone(),
            project_id: metadata.project_id.clone(),
        };

        // Convert spec
        let pod_spec = k8shost_types::PodSpec {
            containers: spec
                .containers
                .iter()
                .map(|c| k8shost_types::Container {
                    name: c.name.clone(),
                    image: c.image.clone(),
                    command: c.command.clone(),
                    args: c.args.clone(),
                    ports: c
                        .ports
                        .iter()
                        .map(|p| k8shost_types::ContainerPort {
                            name: p.name.clone(),
                            container_port: p.container_port,
                            protocol: p.protocol.clone(),
                        })
                        .collect(),
                    env: c
                        .env
                        .iter()
                        .map(|e| k8shost_types::EnvVar {
                            name: e.name.clone(),
                            value: e.value.clone(),
                        })
                        .collect(),
                    resources: None, // TODO: Add resource requirements conversion
                })
                .collect(),
            restart_policy: spec.restart_policy.clone(),
            node_name: spec.node_name.clone(),
        };

        // Convert status
        let status = proto.status.as_ref().map(|s| k8shost_types::PodStatus {
            phase: s.phase.clone(),
            pod_ip: s.pod_ip.clone(),
            host_ip: s.host_ip.clone(),
            conditions: s
                .conditions
                .iter()
                .map(|c| k8shost_types::PodCondition {
                    r#type: c.r#type.clone(),
                    status: c.status.clone(),
                    reason: c.reason.clone(),
                    message: c.message.clone(),
                })
                .collect(),
        });

        Ok(k8shost_types::Pod {
            metadata: meta,
            spec: pod_spec,
            status,
        })
    }
}

#[tonic::async_trait]
impl PodService for PodServiceImpl {
    async fn create_pod(
        &self,
        request: Request<CreatePodRequest>,
    ) -> Result<Response<CreatePodResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();
        let proto_pod = req
            .pod
            .ok_or_else(|| Status::invalid_argument("pod is required"))?;

        // Convert proto to internal type
        let mut pod = Self::from_proto_pod(&proto_pod)?;

        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            pod.metadata.org_id.as_deref().unwrap_or(""),
            pod.metadata.project_id.as_deref().unwrap_or(""),
        )?;
        pod.metadata.org_id = Some(org_id.clone());
        pod.metadata.project_id = Some(project_id.clone());
        self.auth
            .authorize(
                &tenant,
                ACTION_POD_CREATE,
                &resource_for_tenant("pod", "*", &org_id, &project_id),
            )
            .await?;
        if pod.metadata.namespace.is_none() {
            return Err(Status::invalid_argument(
                "namespace is required in metadata",
            ));
        }

        // Assign UID if not present
        if pod.metadata.uid.is_none() {
            pod.metadata.uid = Some(Uuid::new_v4().to_string());
        }

        // Set creation timestamp
        if pod.metadata.creation_timestamp.is_none() {
            pod.metadata.creation_timestamp = Some(Utc::now());
        }

        // Set initial resource version
        if pod.metadata.resource_version.is_none() {
            pod.metadata.resource_version = Some("1".to_string());
        }

        // Initialize status if not present
        if pod.status.is_none() {
            pod.status = Some(PodStatus {
                phase: Some("Pending".to_string()),
                pod_ip: None,
                host_ip: None,
                conditions: vec![],
            });
        }

        // CreditService Admission Control (2-phase commit)
        let reservation_id: Option<String> = if let Some(ref credit_svc) = self.credit_service {
            let mut client = credit_svc.write().await;

            // Calculate estimated cost based on Pod resource requests
            let estimated_cost = Self::calculate_pod_cost(&pod);

            let project_id = pod.metadata.project_id.as_ref().ok_or_else(|| {
                Status::invalid_argument("project_id required for admission control")
            })?;

            // Phase 0: Check quota
            match client
                .check_quota(project_id, ResourceType::K8sNode, 1, estimated_cost)
                .await
            {
                Ok(resp) if !resp.allowed => {
                    let reason = if resp.reason.is_empty() {
                        "Insufficient quota or balance".to_string()
                    } else {
                        resp.reason
                    };
                    tracing::warn!(
                        project_id = %project_id,
                        reason = %reason,
                        "Pod creation denied by CreditService"
                    );
                    return Err(Status::resource_exhausted(format!(
                        "Admission denied: {}",
                        reason
                    )));
                }
                Err(e) => {
                    tracing::warn!("CreditService check_quota failed (allowing request): {}", e);
                    // Continue without reservation if CreditService is unavailable
                }
                Ok(_) => {}
            }

            // Phase 1: Reserve credits
            match client
                .reserve_credits(
                    project_id,
                    estimated_cost,
                    format!("Pod {} creation", pod.metadata.name),
                    "PodInstance",
                    300, // 5 minute TTL
                )
                .await
            {
                Ok(reservation) => {
                    tracing::info!(
                        reservation_id = %reservation.id,
                        amount = estimated_cost,
                        "Credits reserved for Pod creation"
                    );
                    Some(reservation.id)
                }
                Err(e) => {
                    tracing::warn!("CreditService reserve_credits failed: {}", e);
                    return Err(Status::resource_exhausted(format!(
                        "Failed to reserve credits: {}",
                        e
                    )));
                }
            }
        } else {
            None
        };

        // Store the pod
        match self.storage.put_pod(&pod).await {
            Ok(_) => {}
            Err(e) => {
                // Rollback: Release reservation on failure
                if let (Some(ref credit_svc), Some(ref res_id)) =
                    (&self.credit_service, &reservation_id)
                {
                    let mut client = credit_svc.write().await;
                    if let Err(release_err) = client
                        .release_reservation(res_id, format!("Pod storage failed: {}", e))
                        .await
                    {
                        tracing::warn!("Failed to release reservation {}: {}", res_id, release_err);
                    } else {
                        tracing::info!(reservation_id = %res_id, "Released reservation after Pod storage failure");
                    }
                }
                return Err(e);
            }
        }

        // Phase 2: Commit reservation on success
        if let (Some(ref credit_svc), Some(ref res_id)) = (&self.credit_service, &reservation_id) {
            let mut client = credit_svc.write().await;
            let actual_cost = Self::calculate_pod_cost(&pod);
            let pod_uid = pod.metadata.uid.as_ref().unwrap_or(&pod.metadata.name);

            if let Err(e) = client
                .commit_reservation(res_id, actual_cost, pod_uid)
                .await
            {
                tracing::warn!("Failed to commit reservation {}: {}", res_id, e);
                // Pod is already created, so we don't fail here - billing will reconcile
            } else {
                tracing::info!(reservation_id = %res_id, pod_uid = %pod_uid, "Committed credits for Pod");
            }
        }

        // NOTE: In production, the following steps would occur next:
        // 1. Scheduler (S5) assigns pod to a node based on resource requirements
        // 2. Kubelet on the assigned node detects the pod assignment
        // 3. Kubelet invokes CNI plugin (cni::invoke_cni_add) to set up networking
        // 4. Kubelet pulls container images and starts containers
        // 5. Pod status is updated to "Running" with pod_ip from CNI result
        //
        // For MVP S6.1, CNI integration is demonstrated via integration tests.

        // Convert back to proto and return
        let proto_pod = Self::to_proto_pod(&pod);
        Ok(Response::new(CreatePodResponse {
            pod: Some(proto_pod),
        }))
    }

    async fn get_pod(
        &self,
        request: Request<GetPodRequest>,
    ) -> Result<Response<GetPodResponse>, Status> {
        // Extract tenant context from authenticated request
        let tenant_context = get_tenant_context(&request)?;
        let req = request.into_inner();
        let pod_key = format!("{}/{}", req.namespace, req.name);
        self.auth
            .authorize(
                &tenant_context,
                ACTION_POD_READ,
                &resource_for_tenant(
                    "pod",
                    pod_key,
                    &tenant_context.org_id,
                    &tenant_context.project_id,
                ),
            )
            .await?;

        let pod = self
            .storage
            .get_pod(
                &tenant_context.org_id,
                &tenant_context.project_id,
                &req.namespace,
                &req.name,
            )
            .await?;

        if let Some(pod) = pod {
            let proto_pod = Self::to_proto_pod(&pod);
            Ok(Response::new(GetPodResponse {
                pod: Some(proto_pod),
            }))
        } else {
            Err(Status::not_found(format!("Pod {} not found", req.name)))
        }
    }

    async fn list_pods(
        &self,
        request: Request<ListPodsRequest>,
    ) -> Result<Response<ListPodsResponse>, Status> {
        // Extract tenant context from authenticated request
        let tenant_context = get_tenant_context(&request)?;
        self.auth
            .authorize(
                &tenant_context,
                ACTION_POD_LIST,
                &resource_for_tenant(
                    "pod",
                    "*",
                    &tenant_context.org_id,
                    &tenant_context.project_id,
                ),
            )
            .await?;
        let req = request.into_inner();

        let namespace = req.namespace.as_deref();
        let label_selector = if req.label_selector.is_empty() {
            None
        } else {
            Some(&req.label_selector)
        };

        let pods = self
            .storage
            .list_pods(
                &tenant_context.org_id,
                &tenant_context.project_id,
                namespace,
                label_selector,
            )
            .await?;

        let items: Vec<k8shost_proto::Pod> = pods.iter().map(Self::to_proto_pod).collect();

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

    async fn update_pod(
        &self,
        request: Request<UpdatePodRequest>,
    ) -> Result<Response<UpdatePodResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();
        let proto_pod = req
            .pod
            .ok_or_else(|| Status::invalid_argument("pod is required"))?;

        let mut pod = Self::from_proto_pod(&proto_pod)?;

        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            pod.metadata.org_id.as_deref().unwrap_or(""),
            pod.metadata.project_id.as_deref().unwrap_or(""),
        )?;
        pod.metadata.org_id = Some(org_id.clone());
        pod.metadata.project_id = Some(project_id.clone());
        if pod.metadata.namespace.is_none() {
            return Err(Status::invalid_argument("namespace is required"));
        }
        let pod_key = format!(
            "{}/{}",
            pod.metadata.namespace.as_deref().unwrap_or("default"),
            pod.metadata.name
        );
        self.auth
            .authorize(
                &tenant,
                ACTION_POD_UPDATE,
                &resource_for_tenant("pod", pod_key, &org_id, &project_id),
            )
            .await?;

        // Increment resource version
        let current_version = pod
            .metadata
            .resource_version
            .as_ref()
            .and_then(|v| v.parse::<u64>().ok())
            .unwrap_or(0);
        pod.metadata.resource_version = Some((current_version + 1).to_string());

        // Update the pod
        self.storage.put_pod(&pod).await?;

        let proto_pod = Self::to_proto_pod(&pod);
        Ok(Response::new(UpdatePodResponse {
            pod: Some(proto_pod),
        }))
    }

    async fn delete_pod(
        &self,
        request: Request<DeletePodRequest>,
    ) -> Result<Response<DeletePodResponse>, Status> {
        // Extract tenant context from authenticated request
        let tenant_context = get_tenant_context(&request)?;
        let req = request.into_inner();
        let pod_key = format!("{}/{}", req.namespace, req.name);
        self.auth
            .authorize(
                &tenant_context,
                ACTION_POD_DELETE,
                &resource_for_tenant(
                    "pod",
                    pod_key,
                    &tenant_context.org_id,
                    &tenant_context.project_id,
                ),
            )
            .await?;

        let existed = self
            .storage
            .delete_pod(
                &tenant_context.org_id,
                &tenant_context.project_id,
                &req.namespace,
                &req.name,
            )
            .await?;

        Ok(Response::new(DeletePodResponse { success: existed }))
    }

    type WatchPodsStream = ReceiverStream<Result<WatchEvent, Status>>;

    async fn watch_pods(
        &self,
        request: Request<WatchPodsRequest>,
    ) -> Result<Response<Self::WatchPodsStream>, Status> {
        let tenant_context = get_tenant_context(&request)?;
        self.auth
            .authorize(
                &tenant_context,
                ACTION_POD_LIST,
                &resource_for_tenant(
                    "pod",
                    "*",
                    &tenant_context.org_id,
                    &tenant_context.project_id,
                ),
            )
            .await?;
        let _req = request.into_inner();

        // TODO: Implement proper watch mechanism with FlareDB change notifications.
        // Return unimplemented for now to avoid emitting invalid watch events.
        Err(Status::unimplemented("watch_pods is not implemented yet"))
    }
}
