//! SecurityGroup gRPC service implementation

use std::sync::Arc;
use tonic::{Request, Response, Status};

use prismnet_api::{
    security_group_service_server::SecurityGroupService, AddRuleRequest, AddRuleResponse,
    CreateSecurityGroupRequest, CreateSecurityGroupResponse, DeleteSecurityGroupRequest,
    DeleteSecurityGroupResponse, GetSecurityGroupRequest, GetSecurityGroupResponse,
    IpProtocol as ProtoIpProtocol, ListSecurityGroupsRequest, ListSecurityGroupsResponse,
    RemoveRuleRequest, RemoveRuleResponse, RuleDirection as ProtoRuleDirection,
    SecurityGroup as ProtoSecurityGroup, SecurityGroupRule as ProtoSecurityGroupRule,
    UpdateSecurityGroupRequest, UpdateSecurityGroupResponse,
};
use iam_service_auth::{
    get_tenant_context, resolve_tenant_ids_from_context, resource_for_tenant, AuthService,
};
use prismnet_types::{
    IpProtocol, Port, RuleDirection, SecurityGroup, SecurityGroupId, SecurityGroupRule,
};

use crate::ovn::{build_acl_match, calculate_priority};
use crate::{NetworkMetadataStore, OvnClient};

const ACTION_SG_CREATE: &str = "network:security-groups:create";
const ACTION_SG_READ: &str = "network:security-groups:read";
const ACTION_SG_LIST: &str = "network:security-groups:list";
const ACTION_SG_UPDATE: &str = "network:security-groups:update";
const ACTION_SG_DELETE: &str = "network:security-groups:delete";
const ACTION_SG_RULE_CREATE: &str = "network:security-group-rules:create";
const ACTION_SG_RULE_DELETE: &str = "network:security-group-rules:delete";

#[derive(Clone)]
pub struct SecurityGroupServiceImpl {
    metadata: Arc<NetworkMetadataStore>,
    ovn: Arc<OvnClient>,
    auth: Arc<AuthService>,
}

impl SecurityGroupServiceImpl {
    pub fn new(
        metadata: Arc<NetworkMetadataStore>,
        ovn: Arc<OvnClient>,
        auth: Arc<AuthService>,
    ) -> Self {
        Self {
            metadata,
            ovn,
            auth,
        }
    }
}

fn ensure_unique_security_group_name(
    existing: &[SecurityGroup],
    desired_name: &str,
    exclude: Option<SecurityGroupId>,
) -> Result<(), Status> {
    if desired_name.trim().is_empty() {
        return Err(Status::invalid_argument("SecurityGroup name is required"));
    }
    if existing
        .iter()
        .any(|sg| sg.name == desired_name && Some(sg.id) != exclude)
    {
        return Err(Status::already_exists(
            "SecurityGroup name already exists in the tenant",
        ));
    }
    Ok(())
}

fn security_group_is_referenced(
    security_group_id: SecurityGroupId,
    security_groups: &[SecurityGroup],
    ports: &[Port],
) -> bool {
    ports
        .iter()
        .any(|port| port.security_groups.contains(&security_group_id))
        || security_groups.iter().any(|sg| {
            sg.id != security_group_id
                && sg.rules
                    .iter()
                    .any(|rule| rule.remote_group_id == Some(security_group_id))
        })
}

fn security_group_to_proto(sg: &SecurityGroup) -> ProtoSecurityGroup {
    ProtoSecurityGroup {
        id: sg.id.to_string(),
        project_id: sg.project_id.clone(),
        name: sg.name.clone(),
        description: sg.description.clone().unwrap_or_default(),
        rules: sg.rules.iter().map(rule_to_proto).collect(),
        created_at: sg.created_at,
        updated_at: sg.updated_at,
    }
}

fn rule_to_proto(rule: &SecurityGroupRule) -> ProtoSecurityGroupRule {
    ProtoSecurityGroupRule {
        id: rule.id.to_string(),
        security_group_id: rule.security_group_id.to_string(),
        direction: direction_to_proto(&rule.direction) as i32,
        protocol: protocol_to_proto(&rule.protocol) as i32,
        port_range_min: rule.port_range_min.map(|p| p as u32).unwrap_or(0),
        port_range_max: rule.port_range_max.map(|p| p as u32).unwrap_or(0),
        remote_cidr: rule.remote_cidr.clone().unwrap_or_default(),
        remote_group_id: rule
            .remote_group_id
            .as_ref()
            .map(|id| id.to_string())
            .unwrap_or_default(),
        description: rule.description.clone().unwrap_or_default(),
        created_at: rule.created_at,
    }
}

fn direction_to_proto(direction: &RuleDirection) -> ProtoRuleDirection {
    match direction {
        RuleDirection::Ingress => ProtoRuleDirection::Ingress,
        RuleDirection::Egress => ProtoRuleDirection::Egress,
    }
}

fn proto_to_direction(direction: i32) -> RuleDirection {
    match ProtoRuleDirection::try_from(direction) {
        Ok(ProtoRuleDirection::Ingress) => RuleDirection::Ingress,
        Ok(ProtoRuleDirection::Egress) | Ok(ProtoRuleDirection::Unspecified) | Err(_) => {
            RuleDirection::Egress
        }
    }
}

fn protocol_to_proto(protocol: &IpProtocol) -> ProtoIpProtocol {
    match protocol {
        IpProtocol::Any => ProtoIpProtocol::Any,
        IpProtocol::Tcp => ProtoIpProtocol::Tcp,
        IpProtocol::Udp => ProtoIpProtocol::Udp,
        IpProtocol::Icmp => ProtoIpProtocol::Icmp,
        IpProtocol::Icmpv6 => ProtoIpProtocol::Icmpv6,
    }
}

fn proto_to_protocol(protocol: i32) -> IpProtocol {
    match ProtoIpProtocol::try_from(protocol) {
        Ok(ProtoIpProtocol::Any) | Ok(ProtoIpProtocol::Unspecified) => IpProtocol::Any,
        Ok(ProtoIpProtocol::Tcp) => IpProtocol::Tcp,
        Ok(ProtoIpProtocol::Udp) => IpProtocol::Udp,
        Ok(ProtoIpProtocol::Icmp) => IpProtocol::Icmp,
        Ok(ProtoIpProtocol::Icmpv6) => IpProtocol::Icmpv6,
        Err(_) => IpProtocol::Any,
    }
}

#[tonic::async_trait]
impl SecurityGroupService for SecurityGroupServiceImpl {
    async fn create_security_group(
        &self,
        request: Request<CreateSecurityGroupRequest>,
    ) -> Result<Response<CreateSecurityGroupResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        self.auth
            .authorize(
                &tenant,
                ACTION_SG_CREATE,
                &resource_for_tenant("security-group", "*", &org_id, &project_id),
            )
            .await?;
        let req = request.into_inner();
        let existing = self
            .metadata
            .list_security_groups(&org_id, &project_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;
        ensure_unique_security_group_name(&existing, &req.name, None)?;

        let sg = SecurityGroup::new(&req.name, &org_id, &project_id);
        let mut sg = sg;
        if !req.description.is_empty() {
            sg.description = Some(req.description);
        }

        self.metadata
            .create_security_group(sg.clone())
            .await
            .map_err(|e| Status::internal(e.to_string()))?;

        Ok(Response::new(CreateSecurityGroupResponse {
            security_group: Some(security_group_to_proto(&sg)),
        }))
    }

    async fn get_security_group(
        &self,
        request: Request<GetSecurityGroupRequest>,
    ) -> Result<Response<GetSecurityGroupResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let req = request.into_inner();

        let id = uuid::Uuid::parse_str(&req.id)
            .map_err(|_| Status::invalid_argument("Invalid SecurityGroup ID"))?;
        let sg_id = SecurityGroupId::from_uuid(id);
        self.auth
            .authorize(
                &tenant,
                ACTION_SG_READ,
                &resource_for_tenant("security-group", sg_id.to_string(), &org_id, &project_id),
            )
            .await?;

        let sg = self
            .metadata
            .get_security_group(&org_id, &project_id, &sg_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("SecurityGroup not found"))?;

        Ok(Response::new(GetSecurityGroupResponse {
            security_group: Some(security_group_to_proto(&sg)),
        }))
    }

    async fn list_security_groups(
        &self,
        request: Request<ListSecurityGroupsRequest>,
    ) -> Result<Response<ListSecurityGroupsResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        self.auth
            .authorize(
                &tenant,
                ACTION_SG_LIST,
                &resource_for_tenant("security-group", "*", &org_id, &project_id),
            )
            .await?;
        let _ = request.into_inner();

        let security_groups = self
            .metadata
            .list_security_groups(&org_id, &project_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;

        Ok(Response::new(ListSecurityGroupsResponse {
            security_groups: security_groups
                .iter()
                .map(security_group_to_proto)
                .collect(),
            next_page_token: String::new(),
        }))
    }

    async fn update_security_group(
        &self,
        request: Request<UpdateSecurityGroupRequest>,
    ) -> Result<Response<UpdateSecurityGroupResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let req = request.into_inner();

        let id = uuid::Uuid::parse_str(&req.id)
            .map_err(|_| Status::invalid_argument("Invalid SecurityGroup ID"))?;
        let sg_id = SecurityGroupId::from_uuid(id);
        self.auth
            .authorize(
                &tenant,
                ACTION_SG_UPDATE,
                &resource_for_tenant("security-group", sg_id.to_string(), &org_id, &project_id),
            )
            .await?;

        let name = if !req.name.is_empty() {
            Some(req.name)
        } else {
            None
        };
        let description = if !req.description.is_empty() {
            Some(req.description)
        } else {
            None
        };
        if let Some(name) = name.as_deref() {
            let existing = self
                .metadata
                .list_security_groups(&org_id, &project_id)
                .await
                .map_err(|e| Status::internal(e.to_string()))?;
            ensure_unique_security_group_name(&existing, name, Some(sg_id))?;
        }

        let sg = self
            .metadata
            .update_security_group(&org_id, &project_id, &sg_id, name, description)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("SecurityGroup not found"))?;

        Ok(Response::new(UpdateSecurityGroupResponse {
            security_group: Some(security_group_to_proto(&sg)),
        }))
    }

    async fn delete_security_group(
        &self,
        request: Request<DeleteSecurityGroupRequest>,
    ) -> Result<Response<DeleteSecurityGroupResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let req = request.into_inner();

        let id = uuid::Uuid::parse_str(&req.id)
            .map_err(|_| Status::invalid_argument("Invalid SecurityGroup ID"))?;
        let sg_id = SecurityGroupId::from_uuid(id);
        self.auth
            .authorize(
                &tenant,
                ACTION_SG_DELETE,
                &resource_for_tenant("security-group", sg_id.to_string(), &org_id, &project_id),
            )
            .await?;

        let security_groups = self
            .metadata
            .list_security_groups(&org_id, &project_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;
        let vpcs = self
            .metadata
            .list_vpcs(&org_id, &project_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;
        let mut ports = Vec::new();
        for vpc in vpcs {
            let subnets = self
                .metadata
                .list_subnets(&org_id, &project_id, &vpc.id)
                .await
                .map_err(|e| Status::internal(e.to_string()))?;
            for subnet in subnets {
                ports.extend(
                    self.metadata
                        .list_ports(Some(&subnet.id), None)
                        .await
                        .map_err(|e| Status::internal(e.to_string()))?,
                );
            }
        }
        if security_group_is_referenced(sg_id, &security_groups, &ports) {
            return Err(Status::failed_precondition(
                "cannot delete SecurityGroup while it is referenced by ports or rules",
            ));
        }

        self.metadata
            .delete_security_group(&org_id, &project_id, &sg_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;

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

    async fn add_rule(
        &self,
        request: Request<AddRuleRequest>,
    ) -> Result<Response<AddRuleResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let req = request.into_inner();

        let sg_id = uuid::Uuid::parse_str(&req.security_group_id)
            .map_err(|_| Status::invalid_argument("Invalid SecurityGroup ID"))?;
        let sg_id = SecurityGroupId::from_uuid(sg_id);
        self.auth
            .authorize(
                &tenant,
                ACTION_SG_RULE_CREATE,
                &resource_for_tenant("security-group-rule", "*", &org_id, &project_id),
            )
            .await?;

        let direction = proto_to_direction(req.direction);
        let protocol = proto_to_protocol(req.protocol);

        let port_range_min = if req.port_range_min > 0 {
            Some(req.port_range_min)
        } else {
            None
        };
        let port_range_max = if req.port_range_max > 0 {
            Some(req.port_range_max)
        } else {
            None
        };

        let remote_cidr = if !req.remote_cidr.is_empty() {
            Some(req.remote_cidr)
        } else {
            None
        };
        let remote_group_id = if !req.remote_group_id.is_empty() {
            let id = uuid::Uuid::parse_str(&req.remote_group_id)
                .map_err(|_| Status::invalid_argument("Invalid remote SecurityGroup ID"))?;
            Some(SecurityGroupId::from_uuid(id))
        } else {
            None
        };

        let description = if !req.description.is_empty() {
            Some(req.description)
        } else {
            None
        };

        let mut rule = SecurityGroupRule::new(sg_id, direction, protocol);
        rule.port_range_min = port_range_min.map(|p| p as u16);
        rule.port_range_max = port_range_max.map(|p| p as u16);
        rule.remote_cidr = remote_cidr;
        rule.remote_group_id = remote_group_id;
        rule.description = description;

        let rule_added = self
            .metadata
            .add_security_group_rule(&org_id, &project_id, &sg_id, rule.clone())
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("SecurityGroup not found"))?;

        // Best-effort ACL creation for each VPC in tenant
        let vpcs = self
            .metadata
            .list_vpcs(&org_id, &project_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;

        // Build ACL match expression and calculate priority
        let match_expr = build_acl_match(&rule_added, None);
        let priority = calculate_priority(&rule_added);

        for vpc in vpcs {
            self.ovn
                .create_acl(&sg_id, &rule_added, &vpc.id, &match_expr, priority)
                .await
                .map_err(|e| Status::internal(e.to_string()))?;
        }

        Ok(Response::new(AddRuleResponse {
            rule: Some(rule_to_proto(&rule_added)),
        }))
    }

    async fn remove_rule(
        &self,
        request: Request<RemoveRuleRequest>,
    ) -> Result<Response<RemoveRuleResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let (org_id, project_id) = resolve_tenant_ids_from_context(
            &tenant,
            &request.get_ref().org_id,
            &request.get_ref().project_id,
        )?;
        let req = request.into_inner();

        let sg_id = uuid::Uuid::parse_str(&req.security_group_id)
            .map_err(|_| Status::invalid_argument("Invalid SecurityGroup ID"))?;
        let sg_id = SecurityGroupId::from_uuid(sg_id);

        let rule_id_uuid = uuid::Uuid::parse_str(&req.rule_id)
            .map_err(|_| Status::invalid_argument("Invalid Rule ID"))?;
        let rule_id = prismnet_types::SecurityGroupRuleId::from_uuid(rule_id_uuid);
        self.auth
            .authorize(
                &tenant,
                ACTION_SG_RULE_DELETE,
                &resource_for_tenant(
                    "security-group-rule",
                    rule_id.to_string(),
                    &org_id,
                    &project_id,
                ),
            )
            .await?;

        let _removed = self
            .metadata
            .remove_security_group_rule(&org_id, &project_id, &sg_id, &rule_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?
            .ok_or_else(|| Status::not_found("SecurityGroup or Rule not found"))?;

        self.ovn
            .delete_acl(&rule_id)
            .await
            .map_err(|e| Status::internal(e.to_string()))?;

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

#[cfg(test)]
mod tests {
    use super::*;
    use prismnet_types::SubnetId;

    #[test]
    fn rejects_duplicate_security_group_name() {
        let existing = vec![SecurityGroup::new("web", "org", "proj")];
        let err = ensure_unique_security_group_name(&existing, "web", None).unwrap_err();
        assert_eq!(err.code(), tonic::Code::AlreadyExists);
    }

    #[test]
    fn detects_security_group_references() {
        let sg = SecurityGroup::new("web", "org", "proj");
        let sg_id = sg.id;
        let mut port = Port::new("port", SubnetId::new());
        port.security_groups = vec![sg_id];
        assert!(security_group_is_referenced(sg_id, &[sg], &[port]));
    }
}
