//! L7 Rule service implementation

use std::sync::Arc;

use base64::Engine as _;
use crate::metadata::LbMetadataStore;
use fiberlb_api::{
    l7_rule_service_server::L7RuleService,
    CreateL7RuleRequest, CreateL7RuleResponse,
    DeleteL7RuleRequest, DeleteL7RuleResponse,
    GetL7RuleRequest, GetL7RuleResponse,
    ListL7RulesRequest, ListL7RulesResponse,
    UpdateL7RuleRequest, UpdateL7RuleResponse,
    L7Rule as ProtoL7Rule, L7RuleType as ProtoL7RuleType, L7CompareType as ProtoL7CompareType,
};
use fiberlb_types::{
    L7CompareType, L7Policy, L7PolicyId, L7Rule, L7RuleId, L7RuleType,
};
use iam_service_auth::{get_tenant_context, resource_for_tenant, AuthService};
use tonic::{Request, Response, Status};
use uuid::Uuid;

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

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

const ACTION_L7RULES_CREATE: &str = "network:l7-rules:create";
const ACTION_L7RULES_READ: &str = "network:l7-rules:read";
const ACTION_L7RULES_LIST: &str = "network:l7-rules:list";
const ACTION_L7RULES_UPDATE: &str = "network:l7-rules:update";
const ACTION_L7RULES_DELETE: &str = "network:l7-rules:delete";

/// Convert domain L7Rule to proto
fn l7_rule_to_proto(rule: &L7Rule) -> ProtoL7Rule {
    ProtoL7Rule {
        id: rule.id.to_string(),
        policy_id: rule.policy_id.to_string(),
        rule_type: match rule.rule_type {
            L7RuleType::HostName => ProtoL7RuleType::HostName.into(),
            L7RuleType::Path => ProtoL7RuleType::Path.into(),
            L7RuleType::FileType => ProtoL7RuleType::FileType.into(),
            L7RuleType::Header => ProtoL7RuleType::Header.into(),
            L7RuleType::Cookie => ProtoL7RuleType::Cookie.into(),
            L7RuleType::SslConnHasSni => ProtoL7RuleType::SslConnHasSni.into(),
        },
        compare_type: match rule.compare_type {
            L7CompareType::EqualTo => ProtoL7CompareType::EqualTo.into(),
            L7CompareType::Regex => ProtoL7CompareType::Regex.into(),
            L7CompareType::StartsWith => ProtoL7CompareType::StartsWith.into(),
            L7CompareType::EndsWith => ProtoL7CompareType::EndsWith.into(),
            L7CompareType::Contains => ProtoL7CompareType::Contains.into(),
        },
        value: rule.value.clone(),
        key: rule.key.clone().unwrap_or_default(),
        invert: rule.invert,
        created_at: rule.created_at,
        updated_at: rule.updated_at,
    }
}

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

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

/// Convert proto rule type to domain
fn proto_to_rule_type(rule_type: i32) -> L7RuleType {
    match ProtoL7RuleType::try_from(rule_type) {
        Ok(ProtoL7RuleType::HostName) => L7RuleType::HostName,
        Ok(ProtoL7RuleType::Path) => L7RuleType::Path,
        Ok(ProtoL7RuleType::FileType) => L7RuleType::FileType,
        Ok(ProtoL7RuleType::Header) => L7RuleType::Header,
        Ok(ProtoL7RuleType::Cookie) => L7RuleType::Cookie,
        Ok(ProtoL7RuleType::SslConnHasSni) => L7RuleType::SslConnHasSni,
        _ => L7RuleType::Path,
    }
}

/// Convert proto compare type to domain
fn proto_to_compare_type(compare_type: i32) -> L7CompareType {
    match ProtoL7CompareType::try_from(compare_type) {
        Ok(ProtoL7CompareType::EqualTo) => L7CompareType::EqualTo,
        Ok(ProtoL7CompareType::Regex) => L7CompareType::Regex,
        Ok(ProtoL7CompareType::StartsWith) => L7CompareType::StartsWith,
        Ok(ProtoL7CompareType::EndsWith) => L7CompareType::EndsWith,
        Ok(ProtoL7CompareType::Contains) => L7CompareType::Contains,
        _ => L7CompareType::EqualTo,
    }
}

async fn find_policy_scope(
    metadata: &LbMetadataStore,
    org_id: &str,
    project_id: &str,
    policy_id: &L7PolicyId,
) -> Result<(L7Policy, String, String), Status> {
    let policy = 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 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, &policy.listener_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .is_some()
        {
            return Ok((policy, lb.org_id.clone(), lb.project_id.clone()));
        }
    }

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

#[tonic::async_trait]
impl L7RuleService for L7RuleServiceImpl {
    async fn create_l7_rule(
        &self,
        request: Request<CreateL7RuleRequest>,
    ) -> Result<Response<CreateL7RuleResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();

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

        let policy_id = parse_policy_id(&req.policy_id)?;

        let (_policy, lb_org_id, lb_project_id) = find_policy_scope(
            &self.metadata,
            &tenant.org_id,
            &tenant.project_id,
            &policy_id,
        )
        .await?;

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

        // Parse rule type and compare type
        let rule_type = proto_to_rule_type(req.rule_type);
        let compare_type = proto_to_compare_type(req.compare_type);

        // Create new rule
        let mut rule = L7Rule::new(policy_id, rule_type, compare_type, &req.value);
        rule.key = if req.key.is_empty() {
            None
        } else {
            Some(req.key)
        };
        rule.invert = req.invert;

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

        Ok(Response::new(CreateL7RuleResponse {
            l7_rule: Some(l7_rule_to_proto(&rule)),
        }))
    }

    async fn get_l7_rule(
        &self,
        request: Request<GetL7RuleRequest>,
    ) -> Result<Response<GetL7RuleResponse>, 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 rule_id = parse_rule_id(&req.id)?;

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

        let (_policy, lb_org_id, lb_project_id) = find_policy_scope(
            &self.metadata,
            &tenant.org_id,
            &tenant.project_id,
            &rule.policy_id,
        )
        .await?;

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

        Ok(Response::new(GetL7RuleResponse {
            l7_rule: Some(l7_rule_to_proto(&rule)),
        }))
    }

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

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

        let policy_id = parse_policy_id(&req.policy_id)?;

        let (_policy, lb_org_id, lb_project_id) = find_policy_scope(
            &self.metadata,
            &tenant.org_id,
            &tenant.project_id,
            &policy_id,
        )
        .await?;

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

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

        let proto_rules: Vec<ProtoL7Rule> = paginated.map(l7_rule_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(ListL7RulesResponse {
            l7_rules: proto_rules,
            next_page_token,
        }))
    }

    async fn update_l7_rule(
        &self,
        request: Request<UpdateL7RuleRequest>,
    ) -> Result<Response<UpdateL7RuleResponse>, 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 rule_id = parse_rule_id(&req.id)?;

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

        let (_policy, lb_org_id, lb_project_id) = find_policy_scope(
            &self.metadata,
            &tenant.org_id,
            &tenant.project_id,
            &rule.policy_id,
        )
        .await?;

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

        // Update fields
        rule.rule_type = proto_to_rule_type(req.rule_type);
        rule.compare_type = proto_to_compare_type(req.compare_type);
        if !req.value.is_empty() {
            rule.value = req.value;
        }
        rule.key = if req.key.is_empty() {
            None
        } else {
            Some(req.key)
        };
        rule.invert = req.invert;
        rule.updated_at = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_secs();

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

        Ok(Response::new(UpdateL7RuleResponse {
            l7_rule: Some(l7_rule_to_proto(&rule)),
        }))
    }

    async fn delete_l7_rule(
        &self,
        request: Request<DeleteL7RuleRequest>,
    ) -> Result<Response<DeleteL7RuleResponse>, 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 rule_id = parse_rule_id(&req.id)?;

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

        let (_policy, lb_org_id, lb_project_id) = find_policy_scope(
            &self.metadata,
            &tenant.org_id,
            &tenant.project_id,
            &rule.policy_id,
        )
        .await?;

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

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

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