//! Pool service implementation

use std::sync::Arc;

use base64::Engine as _;
use crate::metadata::LbMetadataStore;
use fiberlb_api::{
    pool_service_server::PoolService,
    CreatePoolRequest, CreatePoolResponse,
    DeletePoolRequest, DeletePoolResponse,
    GetPoolRequest, GetPoolResponse,
    ListPoolsRequest, ListPoolsResponse,
    UpdatePoolRequest, UpdatePoolResponse,
    Pool as ProtoPool, PoolAlgorithm as ProtoPoolAlgorithm, PoolProtocol as ProtoPoolProtocol,
    SessionPersistence as ProtoSessionPersistence, PersistenceType as ProtoPersistenceType,
};
use fiberlb_types::{
    LoadBalancerId, Pool, PoolAlgorithm, PoolId, PoolProtocol,
    SessionPersistence, PersistenceType,
};
use iam_service_auth::{get_tenant_context, resource_for_tenant, AuthService};
use tonic::{Request, Response, Status};
use uuid::Uuid;

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

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

const ACTION_POOLS_CREATE: &str = "network:pools:create";
const ACTION_POOLS_READ: &str = "network:pools:read";
const ACTION_POOLS_LIST: &str = "network:pools:list";
const ACTION_POOLS_UPDATE: &str = "network:pools:update";
const ACTION_POOLS_DELETE: &str = "network:pools:delete";

/// Convert domain Pool to proto
fn pool_to_proto(pool: &Pool) -> ProtoPool {
    ProtoPool {
        id: pool.id.to_string(),
        name: pool.name.clone(),
        loadbalancer_id: pool.loadbalancer_id.to_string(),
        algorithm: match pool.algorithm {
            PoolAlgorithm::RoundRobin => ProtoPoolAlgorithm::RoundRobin.into(),
            PoolAlgorithm::LeastConnections => ProtoPoolAlgorithm::LeastConnections.into(),
            PoolAlgorithm::IpHash => ProtoPoolAlgorithm::IpHash.into(),
            PoolAlgorithm::WeightedRoundRobin => ProtoPoolAlgorithm::WeightedRoundRobin.into(),
            PoolAlgorithm::Random => ProtoPoolAlgorithm::Random.into(),
            PoolAlgorithm::Maglev => ProtoPoolAlgorithm::Maglev.into(),
        },
        protocol: match pool.protocol {
            PoolProtocol::Tcp => ProtoPoolProtocol::Tcp.into(),
            PoolProtocol::Udp => ProtoPoolProtocol::Udp.into(),
            PoolProtocol::Http => ProtoPoolProtocol::Http.into(),
            PoolProtocol::Https => ProtoPoolProtocol::Https.into(),
        },
        session_persistence: pool.session_persistence.as_ref().map(|sp| {
            ProtoSessionPersistence {
                r#type: match sp.persistence_type {
                    PersistenceType::SourceIp => ProtoPersistenceType::SourceIp.into(),
                    PersistenceType::Cookie => ProtoPersistenceType::Cookie.into(),
                    PersistenceType::AppCookie => ProtoPersistenceType::AppCookie.into(),
                },
                cookie_name: sp.cookie_name.clone().unwrap_or_default(),
                timeout_seconds: sp.timeout_seconds,
            }
        }),
        created_at: pool.created_at,
        updated_at: pool.updated_at,
    }
}

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

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

/// Convert proto algorithm to domain
fn proto_to_algorithm(algo: i32) -> PoolAlgorithm {
    match ProtoPoolAlgorithm::try_from(algo) {
        Ok(ProtoPoolAlgorithm::RoundRobin) => PoolAlgorithm::RoundRobin,
        Ok(ProtoPoolAlgorithm::LeastConnections) => PoolAlgorithm::LeastConnections,
        Ok(ProtoPoolAlgorithm::IpHash) => PoolAlgorithm::IpHash,
        Ok(ProtoPoolAlgorithm::WeightedRoundRobin) => PoolAlgorithm::WeightedRoundRobin,
        Ok(ProtoPoolAlgorithm::Random) => PoolAlgorithm::Random,
        _ => PoolAlgorithm::RoundRobin,
    }
}

/// Convert proto protocol to domain
fn proto_to_protocol(proto: i32) -> PoolProtocol {
    match ProtoPoolProtocol::try_from(proto) {
        Ok(ProtoPoolProtocol::Tcp) => PoolProtocol::Tcp,
        Ok(ProtoPoolProtocol::Udp) => PoolProtocol::Udp,
        Ok(ProtoPoolProtocol::Http) => PoolProtocol::Http,
        Ok(ProtoPoolProtocol::Https) => PoolProtocol::Https,
        _ => PoolProtocol::Tcp,
    }
}

/// Convert proto session persistence to domain
fn proto_to_session_persistence(sp: Option<ProtoSessionPersistence>) -> Option<SessionPersistence> {
    sp.map(|s| {
        let persistence_type = match ProtoPersistenceType::try_from(s.r#type) {
            Ok(ProtoPersistenceType::SourceIp) => PersistenceType::SourceIp,
            Ok(ProtoPersistenceType::Cookie) => PersistenceType::Cookie,
            Ok(ProtoPersistenceType::AppCookie) => PersistenceType::AppCookie,
            _ => PersistenceType::SourceIp,
        };
        SessionPersistence {
            persistence_type,
            cookie_name: if s.cookie_name.is_empty() { None } else { Some(s.cookie_name) },
            timeout_seconds: s.timeout_seconds,
        }
    })
}

#[tonic::async_trait]
impl PoolService for PoolServiceImpl {
    async fn create_pool(
        &self,
        request: Request<CreatePoolRequest>,
    ) -> Result<Response<CreatePoolResponse>, 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.loadbalancer_id.is_empty() {
            return Err(Status::invalid_argument("loadbalancer_id is required"));
        }

        let lb_id = parse_lb_id(&req.loadbalancer_id)?;

        // Verify load balancer exists
        let lb = self.metadata
            .load_lb_by_id(&lb_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("load balancer not in tenant scope"));
        }

        self.auth
            .authorize(
                &tenant,
                ACTION_POOLS_CREATE,
                &resource_for_tenant("pool", "*", &lb.org_id, &lb.project_id),
            )
            .await?;

        // Create new pool
        let algorithm = proto_to_algorithm(req.algorithm);
        let protocol = proto_to_protocol(req.protocol);
        let mut pool = Pool::new(&req.name, lb_id, algorithm, protocol);
        pool.session_persistence = proto_to_session_persistence(req.session_persistence);

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

        Ok(Response::new(CreatePoolResponse {
            pool: Some(pool_to_proto(&pool)),
        }))
    }

    async fn get_pool(
        &self,
        request: Request<GetPoolRequest>,
    ) -> Result<Response<GetPoolResponse>, 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 pool_id = parse_pool_id(&req.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_POOLS_READ,
                &resource_for_tenant("pool", &pool.id.to_string(), &lb.org_id, &lb.project_id),
            )
            .await?;

        Ok(Response::new(GetPoolResponse {
            pool: Some(pool_to_proto(&pool)),
        }))
    }

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

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

        let lb_id = parse_lb_id(&req.loadbalancer_id)?;

        let lb = self
            .metadata
            .load_lb_by_id(&lb_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("load balancer not in tenant scope"));
        }

        self.auth
            .authorize(
                &tenant,
                ACTION_POOLS_LIST,
                &resource_for_tenant("pool", "*", &lb.org_id, &lb.project_id),
            )
            .await?;

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

        let proto_pools: Vec<ProtoPool> = paginated.map(pool_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(ListPoolsResponse {
            pools: proto_pools,
            next_page_token,
        }))
    }

    async fn update_pool(
        &self,
        request: Request<UpdatePoolRequest>,
    ) -> Result<Response<UpdatePoolResponse>, 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 pool_id = parse_pool_id(&req.id)?;

        let mut 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"));
        }
        let lb_org_id = lb.org_id;
        let lb_project_id = lb.project_id;

        self.auth
            .authorize(
                &tenant,
                ACTION_POOLS_UPDATE,
                &resource_for_tenant("pool", &pool.id.to_string(), &lb_org_id, &lb_project_id),
            )
            .await?;

        // Apply updates
        if !req.name.is_empty() {
            pool.name = req.name;
        }
        if req.algorithm != 0 {
            pool.algorithm = proto_to_algorithm(req.algorithm);
        }
        if req.session_persistence.is_some() {
            pool.session_persistence = proto_to_session_persistence(req.session_persistence);
        }

        // Update timestamp
        pool.updated_at = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

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

        Ok(Response::new(UpdatePoolResponse {
            pool: Some(pool_to_proto(&pool)),
        }))
    }

    async fn delete_pool(
        &self,
        request: Request<DeletePoolRequest>,
    ) -> Result<Response<DeletePoolResponse>, 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 pool_id = parse_pool_id(&req.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"));
        }
        let lb_org_id = lb.org_id;
        let lb_project_id = lb.project_id;

        self.auth
            .authorize(
                &tenant,
                ACTION_POOLS_DELETE,
                &resource_for_tenant("pool", &pool.id.to_string(), &lb_org_id, &lb_project_id),
            )
            .await?;

        // Delete all backends first
        self.metadata
            .delete_pool_backends(&pool.id)
            .await
            .map_err(|e| Status::internal(format!("failed to delete backends: {}", e)))?;

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

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