//! Backend service implementation

use std::sync::Arc;

use base64::Engine as _;
use crate::metadata::LbMetadataStore;
use fiberlb_api::{
    backend_service_server::BackendService,
    CreateBackendRequest, CreateBackendResponse,
    DeleteBackendRequest, DeleteBackendResponse,
    GetBackendRequest, GetBackendResponse,
    ListBackendsRequest, ListBackendsResponse,
    UpdateBackendRequest, UpdateBackendResponse,
    Backend as ProtoBackend, BackendAdminState as ProtoBackendAdminState,
    BackendStatus as ProtoBackendStatus,
};
use fiberlb_types::{Backend, BackendAdminState, BackendId, BackendStatus, PoolId};
use iam_service_auth::{get_tenant_context, resource_for_tenant, AuthService};
use tonic::{Request, Response, Status};
use uuid::Uuid;

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

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

const ACTION_BACKENDS_CREATE: &str = "network:backends:create";
const ACTION_BACKENDS_READ: &str = "network:backends:read";
const ACTION_BACKENDS_LIST: &str = "network:backends:list";
const ACTION_BACKENDS_UPDATE: &str = "network:backends:update";
const ACTION_BACKENDS_DELETE: &str = "network:backends:delete";

/// Convert domain Backend to proto
fn backend_to_proto(backend: &Backend) -> ProtoBackend {
    ProtoBackend {
        id: backend.id.to_string(),
        name: backend.name.clone(),
        pool_id: backend.pool_id.to_string(),
        address: backend.address.clone(),
        port: backend.port as u32,
        weight: backend.weight,
        admin_state: match backend.admin_state {
            BackendAdminState::Enabled => ProtoBackendAdminState::Enabled.into(),
            BackendAdminState::Disabled => ProtoBackendAdminState::Disabled.into(),
            BackendAdminState::Drain => ProtoBackendAdminState::Drain.into(),
        },
        status: match backend.status {
            BackendStatus::Online => ProtoBackendStatus::Online.into(),
            BackendStatus::Offline => ProtoBackendStatus::Offline.into(),
            BackendStatus::Checking => ProtoBackendStatus::Checking.into(),
            BackendStatus::Disabled => ProtoBackendStatus::Offline.into(),
            BackendStatus::Unknown => ProtoBackendStatus::Unknown.into(),
        },
        created_at: backend.created_at,
        updated_at: backend.updated_at,
    }
}

/// Parse BackendId from string
fn parse_backend_id(id: &str) -> Result<BackendId, Status> {
    let uuid: Uuid = id
        .parse()
        .map_err(|_| Status::invalid_argument("invalid backend ID"))?;
    Ok(BackendId::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))
}

fn proto_to_admin_state(admin_state: i32) -> Option<BackendAdminState> {
    match ProtoBackendAdminState::try_from(admin_state).ok()? {
        ProtoBackendAdminState::Enabled => Some(BackendAdminState::Enabled),
        ProtoBackendAdminState::Disabled => Some(BackendAdminState::Disabled),
        ProtoBackendAdminState::Drain => Some(BackendAdminState::Drain),
        ProtoBackendAdminState::Unspecified => None,
    }
}


#[tonic::async_trait]
impl BackendService for BackendServiceImpl {
    async fn create_backend(
        &self,
        request: Request<CreateBackendRequest>,
    ) -> Result<Response<CreateBackendResponse>, 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"));
        }
        if req.address.is_empty() {
            return Err(Status::invalid_argument("address is required"));
        }
        if req.port == 0 {
            return Err(Status::invalid_argument("port is required"));
        }

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

        let lbs = self
            .metadata
            .list_lbs(&tenant.org_id, Some(tenant.project_id.as_str()))
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?;

        let mut scope: Option<(String, String)> = None;
        for lb in &lbs {
            if let Some(_) = self
                .metadata
                .load_pool(&lb.id, &pool_id)
                .await
                .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            {
                scope = Some((lb.org_id.clone(), lb.project_id.clone()));
                break;
            }
        }

        let (lb_org_id, lb_project_id) =
            scope.ok_or_else(|| Status::not_found("pool not found"))?;

        self.auth
            .authorize(
                &tenant,
                ACTION_BACKENDS_CREATE,
                &resource_for_tenant("backend", "*", &lb_org_id, &lb_project_id),
            )
            .await?;

        // Create new backend
        let mut backend = Backend::new(&req.name, pool_id, &req.address, req.port as u16);

        // Apply weight if specified
        if req.weight > 0 {
            backend.weight = req.weight;
        }

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

        Ok(Response::new(CreateBackendResponse {
            backend: Some(backend_to_proto(&backend)),
        }))
    }

    async fn get_backend(
        &self,
        request: Request<GetBackendRequest>,
    ) -> Result<Response<GetBackendResponse>, 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 backend_id = parse_backend_id(&req.id)?;
        let backend = self
            .metadata
            .load_backend_by_id(&backend_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("backend not found"))?;
        let pool = self
            .metadata
            .load_pool_by_id(&backend.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("backend not in tenant scope"));
        }

        self.auth
            .authorize(
                &tenant,
                ACTION_BACKENDS_READ,
                &resource_for_tenant("backend", &backend.id.to_string(), &lb.org_id, &lb.project_id),
            )
            .await?;

        Ok(Response::new(GetBackendResponse {
            backend: Some(backend_to_proto(&backend)),
        }))
    }

    async fn list_backends(
        &self,
        request: Request<ListBackendsRequest>,
    ) -> Result<Response<ListBackendsResponse>, 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 lbs = self
            .metadata
            .list_lbs(&tenant.org_id, Some(tenant.project_id.as_str()))
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?;

        let mut scope: Option<(String, String)> = None;
        for lb in &lbs {
            if let Some(_) = self
                .metadata
                .load_pool(&lb.id, &pool_id)
                .await
                .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            {
                scope = Some((lb.org_id.clone(), lb.project_id.clone()));
                break;
            }
        }

        let (lb_org_id, lb_project_id) =
            scope.ok_or_else(|| Status::not_found("pool not found"))?;

        self.auth
            .authorize(
                &tenant,
                ACTION_BACKENDS_LIST,
                &resource_for_tenant("backend", "*", &lb_org_id, &lb_project_id),
            )
            .await?;

        let backends = self
            .metadata
            .list_backends(&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 = backends.len();
        let end = std::cmp::min(offset + page_size, total);
        let paginated = backends.iter().skip(offset).take(page_size);

        let proto_backends: Vec<ProtoBackend> = paginated.map(backend_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(ListBackendsResponse {
            backends: proto_backends,
            next_page_token,
        }))
    }

    async fn update_backend(
        &self,
        request: Request<UpdateBackendRequest>,
    ) -> Result<Response<UpdateBackendResponse>, 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 backend_id = parse_backend_id(&req.id)?;
        let mut backend = self
            .metadata
            .load_backend_by_id(&backend_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("backend not found"))?;
        let pool = self
            .metadata
            .load_pool_by_id(&backend.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("backend not in tenant scope"));
        }

        self.auth
            .authorize(
                &tenant,
                ACTION_BACKENDS_UPDATE,
                &resource_for_tenant("backend", &backend.id.to_string(), &lb.org_id, &lb.project_id),
            )
            .await?;

        if !req.name.is_empty() {
            backend.name = req.name;
        }
        if req.weight > 0 {
            backend.weight = req.weight;
        }
        if let Some(admin_state) = proto_to_admin_state(req.admin_state) {
            backend.admin_state = admin_state;
        }
        backend.updated_at = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

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

        Ok(Response::new(UpdateBackendResponse {
            backend: Some(backend_to_proto(&backend)),
        }))
    }

    async fn delete_backend(
        &self,
        request: Request<DeleteBackendRequest>,
    ) -> Result<Response<DeleteBackendResponse>, 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 backend_id = parse_backend_id(&req.id)?;
        let backend = self
            .metadata
            .load_backend_by_id(&backend_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("backend not found"))?;
        let pool = self
            .metadata
            .load_pool_by_id(&backend.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("backend not in tenant scope"));
        }

        self.auth
            .authorize(
                &tenant,
                ACTION_BACKENDS_DELETE,
                &resource_for_tenant("backend", &backend.id.to_string(), &lb.org_id, &lb.project_id),
            )
            .await?;

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

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