//! HealthCheck service implementation

use std::sync::Arc;

use crate::metadata::LbMetadataStore;
use base64::Engine as _;
use fiberlb_api::{
    health_check_service_server::HealthCheckService, CreateHealthCheckRequest,
    CreateHealthCheckResponse, DeleteHealthCheckRequest, DeleteHealthCheckResponse,
    GetHealthCheckRequest, GetHealthCheckResponse, HealthCheck as ProtoHealthCheck,
    HealthCheckType as ProtoHealthCheckType, HttpHealthConfig as ProtoHttpHealthConfig,
    ListHealthChecksRequest, ListHealthChecksResponse, UpdateHealthCheckRequest,
    UpdateHealthCheckResponse,
};
use fiberlb_types::{HealthCheck, HealthCheckId, HealthCheckType, HttpHealthConfig, PoolId};
use iam_service_auth::{get_tenant_context, resource_for_tenant, AuthService};
use tonic::{Request, Response, Status};
use uuid::Uuid;

/// HealthCheck service implementation
pub struct HealthCheckServiceImpl {
    metadata: Arc<LbMetadataStore>,
    auth: Arc<AuthService>,
}

impl HealthCheckServiceImpl {
    /// Create a new HealthCheckServiceImpl
    pub fn new(metadata: Arc<LbMetadataStore>, auth: Arc<AuthService>) -> Self {
        Self { metadata, auth }
    }
}

const ACTION_HEALTHCHECKS_CREATE: &str = "network:healthchecks:create";
const ACTION_HEALTHCHECKS_READ: &str = "network:healthchecks:read";
const ACTION_HEALTHCHECKS_LIST: &str = "network:healthchecks:list";
const ACTION_HEALTHCHECKS_UPDATE: &str = "network:healthchecks:update";
const ACTION_HEALTHCHECKS_DELETE: &str = "network:healthchecks:delete";

/// Convert domain HealthCheck to proto
fn health_check_to_proto(hc: &HealthCheck) -> ProtoHealthCheck {
    ProtoHealthCheck {
        id: hc.id.to_string(),
        name: hc.name.clone(),
        pool_id: hc.pool_id.to_string(),
        r#type: match hc.check_type {
            HealthCheckType::Tcp => ProtoHealthCheckType::Tcp.into(),
            HealthCheckType::Http => ProtoHealthCheckType::Http.into(),
            HealthCheckType::Https => ProtoHealthCheckType::Https.into(),
            HealthCheckType::Udp => ProtoHealthCheckType::Udp.into(),
            HealthCheckType::Ping => ProtoHealthCheckType::Ping.into(),
        },
        interval_seconds: hc.interval_seconds,
        timeout_seconds: hc.timeout_seconds,
        healthy_threshold: hc.healthy_threshold,
        unhealthy_threshold: hc.unhealthy_threshold,
        http_config: hc.http_config.as_ref().map(|cfg| ProtoHttpHealthConfig {
            method: cfg.method.clone(),
            path: cfg.path.clone(),
            expected_codes: cfg.expected_codes.iter().map(|&c| c as u32).collect(),
            host: cfg.host.clone().unwrap_or_default(),
        }),
        enabled: hc.enabled,
        created_at: hc.created_at,
        updated_at: hc.updated_at,
    }
}

/// Parse HealthCheckId from string
fn parse_hc_id(id: &str) -> Result<HealthCheckId, Status> {
    let uuid: Uuid = id
        .parse()
        .map_err(|_| Status::invalid_argument("invalid health check ID"))?;
    Ok(HealthCheckId::from_uuid(uuid))
}

/// Parse PoolId from string
fn parse_pool_id(id: &str) -> Result<PoolId, Status> {
    let uuid: Uuid = id
        .parse()
        .map_err(|_| Status::invalid_argument("invalid pool ID"))?;
    Ok(PoolId::from_uuid(uuid))
}

/// Convert proto health check type to domain
fn proto_to_check_type(t: i32) -> HealthCheckType {
    match ProtoHealthCheckType::try_from(t) {
        Ok(ProtoHealthCheckType::Tcp) => HealthCheckType::Tcp,
        Ok(ProtoHealthCheckType::Http) => HealthCheckType::Http,
        Ok(ProtoHealthCheckType::Https) => HealthCheckType::Https,
        Ok(ProtoHealthCheckType::Udp) => HealthCheckType::Udp,
        Ok(ProtoHealthCheckType::Ping) => HealthCheckType::Ping,
        _ => HealthCheckType::Tcp,
    }
}

/// Convert proto HTTP config to domain
fn proto_to_http_config(cfg: Option<ProtoHttpHealthConfig>) -> Option<HttpHealthConfig> {
    cfg.map(|c| HttpHealthConfig {
        method: c.method,
        path: c.path,
        expected_codes: c.expected_codes.iter().map(|&c| c as u16).collect(),
        host: if c.host.is_empty() {
            None
        } else {
            Some(c.host)
        },
    })
}

#[tonic::async_trait]
impl HealthCheckService for HealthCheckServiceImpl {
    async fn create_health_check(
        &self,
        request: Request<CreateHealthCheckRequest>,
    ) -> Result<Response<CreateHealthCheckResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();

        // Validate required fields
        if req.name.is_empty() {
            return Err(Status::invalid_argument("name is required"));
        }
        if req.pool_id.is_empty() {
            return Err(Status::invalid_argument("pool_id is required"));
        }

        let pool_id = parse_pool_id(&req.pool_id)?;

        let pool = self
            .metadata
            .load_pool_by_id(&pool_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("pool not found"))?;
        let lb = self
            .metadata
            .load_lb_by_id(&pool.loadbalancer_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("load balancer not found"))?;
        if lb.org_id != tenant.org_id || lb.project_id != tenant.project_id {
            return Err(Status::permission_denied("pool not in tenant scope"));
        }

        self.auth
            .authorize(
                &tenant,
                ACTION_HEALTHCHECKS_CREATE,
                &resource_for_tenant("health-check", "*", &lb.org_id, &lb.project_id),
            )
            .await?;
        let check_type = proto_to_check_type(req.r#type);

        // Create health check based on type
        let mut hc = if check_type == HealthCheckType::Http || check_type == HealthCheckType::Https
        {
            let path = req
                .http_config
                .as_ref()
                .map(|c| c.path.as_str())
                .unwrap_or("/health");
            HealthCheck::new_http(&req.name, pool_id, path)
        } else {
            HealthCheck::new_tcp(&req.name, pool_id)
        };

        // Apply settings
        hc.check_type = check_type;
        if req.interval_seconds > 0 {
            hc.interval_seconds = req.interval_seconds;
        }
        if req.timeout_seconds > 0 {
            hc.timeout_seconds = req.timeout_seconds;
        }
        if req.healthy_threshold > 0 {
            hc.healthy_threshold = req.healthy_threshold;
        }
        if req.unhealthy_threshold > 0 {
            hc.unhealthy_threshold = req.unhealthy_threshold;
        }
        if req.http_config.is_some() {
            hc.http_config = proto_to_http_config(req.http_config);
        }

        // Save health check
        self.metadata
            .save_health_check(&hc)
            .await
            .map_err(|e| Status::internal(format!("failed to save health check: {}", e)))?;

        Ok(Response::new(CreateHealthCheckResponse {
            health_check: Some(health_check_to_proto(&hc)),
        }))
    }

    async fn get_health_check(
        &self,
        request: Request<GetHealthCheckRequest>,
    ) -> Result<Response<GetHealthCheckResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();

        if req.id.is_empty() {
            return Err(Status::invalid_argument("id is required"));
        }

        let hc_id = parse_hc_id(&req.id)?;
        let hc = self
            .metadata
            .load_health_check_by_id(&hc_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("health check not found"))?;
        let pool = self
            .metadata
            .load_pool_by_id(&hc.pool_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("pool not found"))?;
        let lb = self
            .metadata
            .load_lb_by_id(&pool.loadbalancer_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("load balancer not found"))?;

        if lb.org_id != tenant.org_id || lb.project_id != tenant.project_id {
            return Err(Status::permission_denied(
                "health check not in tenant scope",
            ));
        }

        self.auth
            .authorize(
                &tenant,
                ACTION_HEALTHCHECKS_READ,
                &resource_for_tenant(
                    "health-check",
                    &hc.id.to_string(),
                    &lb.org_id,
                    &lb.project_id,
                ),
            )
            .await?;

        Ok(Response::new(GetHealthCheckResponse {
            health_check: Some(health_check_to_proto(&hc)),
        }))
    }

    async fn list_health_checks(
        &self,
        request: Request<ListHealthChecksRequest>,
    ) -> Result<Response<ListHealthChecksResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();

        if req.pool_id.is_empty() {
            return Err(Status::invalid_argument("pool_id is required"));
        }

        let pool_id = parse_pool_id(&req.pool_id)?;

        let pool = self
            .metadata
            .load_pool_by_id(&pool_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("pool not found"))?;
        let lb = self
            .metadata
            .load_lb_by_id(&pool.loadbalancer_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("load balancer not found"))?;
        if lb.org_id != tenant.org_id || lb.project_id != tenant.project_id {
            return Err(Status::permission_denied("pool not in tenant scope"));
        }

        self.auth
            .authorize(
                &tenant,
                ACTION_HEALTHCHECKS_LIST,
                &resource_for_tenant("health-check", "*", &lb.org_id, &lb.project_id),
            )
            .await?;

        let checks = self
            .metadata
            .list_health_checks(&pool_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?;

        let page_size = if req.page_size == 0 {
            50
        } else {
            req.page_size as usize
        };

        let offset = if req.page_token.is_empty() {
            0
        } else {
            let decoded = base64::engine::general_purpose::STANDARD
                .decode(&req.page_token)
                .map_err(|_| Status::invalid_argument("invalid page_token"))?;
            let offset_str = String::from_utf8(decoded)
                .map_err(|_| Status::invalid_argument("invalid page_token encoding"))?;
            offset_str
                .parse::<usize>()
                .map_err(|_| Status::invalid_argument("invalid page_token format"))?
        };

        let total = checks.len();
        let end = std::cmp::min(offset + page_size, total);
        let paginated = checks.iter().skip(offset).take(page_size);

        let proto_checks: Vec<ProtoHealthCheck> = paginated.map(health_check_to_proto).collect();

        let next_page_token = if end < total {
            base64::engine::general_purpose::STANDARD.encode(end.to_string().as_bytes())
        } else {
            String::new()
        };

        Ok(Response::new(ListHealthChecksResponse {
            health_checks: proto_checks,
            next_page_token,
        }))
    }

    async fn update_health_check(
        &self,
        request: Request<UpdateHealthCheckRequest>,
    ) -> Result<Response<UpdateHealthCheckResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();

        if req.id.is_empty() {
            return Err(Status::invalid_argument("id is required"));
        }

        let hc_id = parse_hc_id(&req.id)?;
        let mut hc = self
            .metadata
            .load_health_check_by_id(&hc_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("health check not found"))?;
        let pool = self
            .metadata
            .load_pool_by_id(&hc.pool_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("pool not found"))?;
        let lb = self
            .metadata
            .load_lb_by_id(&pool.loadbalancer_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("load balancer not found"))?;

        if lb.org_id != tenant.org_id || lb.project_id != tenant.project_id {
            return Err(Status::permission_denied(
                "health check not in tenant scope",
            ));
        }

        self.auth
            .authorize(
                &tenant,
                ACTION_HEALTHCHECKS_UPDATE,
                &resource_for_tenant(
                    "health-check",
                    &hc.id.to_string(),
                    &lb.org_id,
                    &lb.project_id,
                ),
            )
            .await?;

        if !req.name.is_empty() {
            hc.name = req.name;
        }
        if req.interval_seconds > 0 {
            hc.interval_seconds = req.interval_seconds;
        }
        if req.timeout_seconds > 0 {
            hc.timeout_seconds = req.timeout_seconds;
        }
        if req.healthy_threshold > 0 {
            hc.healthy_threshold = req.healthy_threshold;
        }
        if req.unhealthy_threshold > 0 {
            hc.unhealthy_threshold = req.unhealthy_threshold;
        }
        if req.http_config.is_some() {
            hc.http_config = proto_to_http_config(req.http_config);
        }
        hc.enabled = req.enabled;
        hc.updated_at = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        self.metadata
            .save_health_check(&hc)
            .await
            .map_err(|e| Status::internal(format!("failed to save health check: {}", e)))?;

        Ok(Response::new(UpdateHealthCheckResponse {
            health_check: Some(health_check_to_proto(&hc)),
        }))
    }

    async fn delete_health_check(
        &self,
        request: Request<DeleteHealthCheckRequest>,
    ) -> Result<Response<DeleteHealthCheckResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();

        if req.id.is_empty() {
            return Err(Status::invalid_argument("id is required"));
        }

        let hc_id = parse_hc_id(&req.id)?;
        let hc = self
            .metadata
            .load_health_check_by_id(&hc_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("health check not found"))?;
        let pool = self
            .metadata
            .load_pool_by_id(&hc.pool_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("pool not found"))?;
        let lb = self
            .metadata
            .load_lb_by_id(&pool.loadbalancer_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("load balancer not found"))?;

        if lb.org_id != tenant.org_id || lb.project_id != tenant.project_id {
            return Err(Status::permission_denied(
                "health check not in tenant scope",
            ));
        }

        self.auth
            .authorize(
                &tenant,
                ACTION_HEALTHCHECKS_DELETE,
                &resource_for_tenant(
                    "health-check",
                    &hc.id.to_string(),
                    &lb.org_id,
                    &lb.project_id,
                ),
            )
            .await?;

        self.metadata
            .delete_health_check(&hc)
            .await
            .map_err(|e| Status::internal(format!("failed to delete health check: {}", e)))?;

        Ok(Response::new(DeleteHealthCheckResponse {}))
    }
}
