//! L7 Policy service implementation

use std::sync::Arc;

use base64::Engine as _;
use crate::metadata::LbMetadataStore;
use fiberlb_api::{
    l7_policy_service_server::L7PolicyService,
    CreateL7PolicyRequest, CreateL7PolicyResponse,
    DeleteL7PolicyRequest, DeleteL7PolicyResponse,
    GetL7PolicyRequest, GetL7PolicyResponse,
    ListL7PoliciesRequest, ListL7PoliciesResponse,
    UpdateL7PolicyRequest, UpdateL7PolicyResponse,
    L7Policy as ProtoL7Policy, L7PolicyAction as ProtoL7PolicyAction,
};
use fiberlb_types::{
    ListenerId, L7Policy, L7PolicyAction, L7PolicyId, PoolId,
};
use iam_service_auth::{get_tenant_context, resource_for_tenant, AuthService};
use tonic::{Request, Response, Status};
use uuid::Uuid;

/// L7 Policy service implementation
pub struct L7PolicyServiceImpl {
    metadata: Arc<LbMetadataStore>,
    auth: Arc<AuthService>,
}

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

const ACTION_L7POLICIES_CREATE: &str = "network:l7-policies:create";
const ACTION_L7POLICIES_READ: &str = "network:l7-policies:read";
const ACTION_L7POLICIES_LIST: &str = "network:l7-policies:list";
const ACTION_L7POLICIES_UPDATE: &str = "network:l7-policies:update";
const ACTION_L7POLICIES_DELETE: &str = "network:l7-policies:delete";

/// Convert domain L7Policy to proto
fn l7_policy_to_proto(policy: &L7Policy) -> ProtoL7Policy {
    ProtoL7Policy {
        id: policy.id.to_string(),
        listener_id: policy.listener_id.to_string(),
        name: policy.name.clone(),
        position: policy.position,
        action: match policy.action {
            L7PolicyAction::RedirectToPool => ProtoL7PolicyAction::RedirectToPool.into(),
            L7PolicyAction::RedirectToUrl => ProtoL7PolicyAction::RedirectToUrl.into(),
            L7PolicyAction::Reject => ProtoL7PolicyAction::Reject.into(),
        },
        redirect_url: policy.redirect_url.clone().unwrap_or_default(),
        redirect_pool_id: policy.redirect_pool_id.as_ref().map(|id| id.to_string()).unwrap_or_default(),
        redirect_http_status_code: policy.redirect_http_status_code.unwrap_or(0) as u32,
        enabled: policy.enabled,
        created_at: policy.created_at,
        updated_at: policy.updated_at,
    }
}

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

/// Parse ListenerId from string
fn parse_listener_id(id: &str) -> Result<ListenerId, Status> {
    let uuid: Uuid = id
        .parse()
        .map_err(|_| Status::invalid_argument("invalid listener ID"))?;
    Ok(ListenerId::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 action to domain
fn proto_to_action(action: i32) -> L7PolicyAction {
    match ProtoL7PolicyAction::try_from(action) {
        Ok(ProtoL7PolicyAction::RedirectToPool) => L7PolicyAction::RedirectToPool,
        Ok(ProtoL7PolicyAction::RedirectToUrl) => L7PolicyAction::RedirectToUrl,
        Ok(ProtoL7PolicyAction::Reject) => L7PolicyAction::Reject,
        _ => L7PolicyAction::RedirectToPool,
    }
}

async fn find_listener_scope(
    metadata: &LbMetadataStore,
    org_id: &str,
    project_id: &str,
    listener_id: &ListenerId,
) -> Result<(String, String), Status> {
    let lbs = metadata
        .list_lbs(org_id, Some(project_id))
        .await
        .map_err(|e| Status::internal(format!("metadata error: {}", e)))?;

    for lb in &lbs {
        if metadata
            .load_listener(&lb.id, listener_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .is_some()
        {
            return Ok((lb.org_id.clone(), lb.project_id.clone()));
        }
    }

    Err(Status::not_found("listener not found"))
}

#[tonic::async_trait]
impl L7PolicyService for L7PolicyServiceImpl {
    async fn create_l7_policy(
        &self,
        request: Request<CreateL7PolicyRequest>,
    ) -> Result<Response<CreateL7PolicyResponse>, 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.listener_id.is_empty() {
            return Err(Status::invalid_argument("listener_id is required"));
        }

        let listener_id = parse_listener_id(&req.listener_id)?;

        let (lb_org_id, lb_project_id) = find_listener_scope(
            &self.metadata,
            &tenant.org_id,
            &tenant.project_id,
            &listener_id,
        )
        .await?;

        self.auth
            .authorize(
                &tenant,
                ACTION_L7POLICIES_CREATE,
                &resource_for_tenant("l7-policy", "*", &lb_org_id, &lb_project_id),
            )
            .await?;

        // Parse action-specific fields
        let action = proto_to_action(req.action);
        let redirect_url = if req.redirect_url.is_empty() {
            None
        } else {
            Some(req.redirect_url)
        };
        let redirect_pool_id = if req.redirect_pool_id.is_empty() {
            None
        } else {
            Some(parse_pool_id(&req.redirect_pool_id)?)
        };
        let redirect_http_status_code = if req.redirect_http_status_code > 0 {
            Some(req.redirect_http_status_code as u16)
        } else {
            None
        };

        // Create new policy
        let mut policy = L7Policy::new(&req.name, listener_id, req.position, action);
        policy.redirect_url = redirect_url;
        policy.redirect_pool_id = redirect_pool_id;
        policy.redirect_http_status_code = redirect_http_status_code;

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

        Ok(Response::new(CreateL7PolicyResponse {
            l7_policy: Some(l7_policy_to_proto(&policy)),
        }))
    }

    async fn get_l7_policy(
        &self,
        request: Request<GetL7PolicyRequest>,
    ) -> Result<Response<GetL7PolicyResponse>, 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 policy_id = parse_policy_id(&req.id)?;

        let policy = self
            .metadata
            .find_l7_policy_by_id(&policy_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("policy not found"))?;

        let (lb_org_id, lb_project_id) = find_listener_scope(
            &self.metadata,
            &tenant.org_id,
            &tenant.project_id,
            &policy.listener_id,
        )
        .await?;

        self.auth
            .authorize(
                &tenant,
                ACTION_L7POLICIES_READ,
                &resource_for_tenant("l7-policy", &policy.id.to_string(), &lb_org_id, &lb_project_id),
            )
            .await?;

        Ok(Response::new(GetL7PolicyResponse {
            l7_policy: Some(l7_policy_to_proto(&policy)),
        }))
    }

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

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

        let listener_id = parse_listener_id(&req.listener_id)?;

        let (lb_org_id, lb_project_id) = find_listener_scope(
            &self.metadata,
            &tenant.org_id,
            &tenant.project_id,
            &listener_id,
        )
        .await?;

        self.auth
            .authorize(
                &tenant,
                ACTION_L7POLICIES_LIST,
                &resource_for_tenant("l7-policy", "*", &lb_org_id, &lb_project_id),
            )
            .await?;

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

        let proto_policies: Vec<ProtoL7Policy> =
            paginated.map(l7_policy_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(ListL7PoliciesResponse {
            l7_policies: proto_policies,
            next_page_token,
        }))
    }

    async fn update_l7_policy(
        &self,
        request: Request<UpdateL7PolicyRequest>,
    ) -> Result<Response<UpdateL7PolicyResponse>, 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 policy_id = parse_policy_id(&req.id)?;

        // Load existing policy
        let mut policy = self
            .metadata
            .find_l7_policy_by_id(&policy_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("policy not found"))?;

        let (lb_org_id, lb_project_id) = find_listener_scope(
            &self.metadata,
            &tenant.org_id,
            &tenant.project_id,
            &policy.listener_id,
        )
        .await?;

        self.auth
            .authorize(
                &tenant,
                ACTION_L7POLICIES_UPDATE,
                &resource_for_tenant("l7-policy", &policy.id.to_string(), &lb_org_id, &lb_project_id),
            )
            .await?;

        // Update fields
        if !req.name.is_empty() {
            policy.name = req.name;
        }
        policy.position = req.position;
        policy.action = proto_to_action(req.action);
        policy.redirect_url = if req.redirect_url.is_empty() {
            None
        } else {
            Some(req.redirect_url)
        };
        policy.redirect_pool_id = if req.redirect_pool_id.is_empty() {
            None
        } else {
            Some(parse_pool_id(&req.redirect_pool_id)?)
        };
        policy.redirect_http_status_code = if req.redirect_http_status_code > 0 {
            Some(req.redirect_http_status_code as u16)
        } else {
            None
        };
        policy.enabled = req.enabled;
        policy.updated_at = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_secs();

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

        Ok(Response::new(UpdateL7PolicyResponse {
            l7_policy: Some(l7_policy_to_proto(&policy)),
        }))
    }

    async fn delete_l7_policy(
        &self,
        request: Request<DeleteL7PolicyRequest>,
    ) -> Result<Response<DeleteL7PolicyResponse>, 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 policy_id = parse_policy_id(&req.id)?;

        // Load policy to verify it exists
        let policy = self
            .metadata
            .find_l7_policy_by_id(&policy_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("policy not found"))?;

        let (lb_org_id, lb_project_id) = find_listener_scope(
            &self.metadata,
            &tenant.org_id,
            &tenant.project_id,
            &policy.listener_id,
        )
        .await?;

        self.auth
            .authorize(
                &tenant,
                ACTION_L7POLICIES_DELETE,
                &resource_for_tenant("l7-policy", &policy.id.to_string(), &lb_org_id, &lb_project_id),
            )
            .await?;

        // Delete policy (this will cascade delete rules)
        self.metadata
            .delete_l7_policy(&policy)
            .await
            .map_err(|e| Status::internal(format!("failed to delete policy: {}", e)))?;

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