pub mod alias;
pub mod destination;

use axum::{
    Json,
    extract::{Path, Query, State},
    http::StatusCode,
};
use chrono::NaiveDateTime;
use defguard_common::db::Id;
use serde_json::{Value, json};
use sqlx::{PgConnection, query_as};
use utoipa::ToSchema;

use super::LicenseInfo;
use crate::{
    appstate::AppState,
    auth::{AdminRole, SessionInfo},
    enterprise::db::models::acl::{AclRule, AclRuleInfo, Protocol, RuleState},
    error::WebError,
    handlers::{
        ApiResponse, ApiResult,
        pagination::{PaginatedApiResponse, PaginatedApiResult, PaginationParams},
    },
};

/// API representation of [`AclRule`] used in API responses.
/// All relations represented as arrays of IDs.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct ApiAclRule {
    pub id: Id,
    pub parent_id: Option<Id>,
    pub state: RuleState,
    pub name: String,
    pub modified_at: NaiveDateTime,
    pub modified_by: String,
    pub all_locations: bool,
    pub locations: Vec<Id>,
    pub expires: Option<NaiveDateTime>,
    pub enabled: bool,
    // source
    pub allow_all_users: bool,
    pub deny_all_users: bool,
    pub allow_all_groups: bool,
    pub deny_all_groups: bool,
    pub allow_all_network_devices: bool,
    pub deny_all_network_devices: bool,
    pub allowed_users: Vec<Id>,
    pub denied_users: Vec<Id>,
    pub allowed_groups: Vec<Id>,
    pub denied_groups: Vec<Id>,
    pub allowed_network_devices: Vec<Id>,
    pub denied_network_devices: Vec<Id>,
    // destination
    pub use_manual_destination_settings: bool,
    pub addresses: String,
    pub ports: String,
    pub protocols: Vec<Protocol>,
    pub any_address: bool,
    pub any_port: bool,
    pub any_protocol: bool,
    // aliases
    pub aliases: Vec<Id>,
    pub destinations: Vec<Id>,
}

impl From<AclRuleInfo<Id>> for ApiAclRule {
    fn from(info: AclRuleInfo<Id>) -> Self {
        Self {
            addresses: info.format_destination(),
            ports: info.format_ports(),
            id: info.id,
            parent_id: info.parent_id,
            state: info.state,
            name: info.name,
            modified_at: info.modified_at,
            modified_by: info.modified_by,
            all_locations: info.all_locations,
            locations: info.locations.iter().map(|v| v.id).collect(),
            expires: info.expires,
            allow_all_users: info.allow_all_users,
            deny_all_users: info.deny_all_users,
            allow_all_groups: info.allow_all_groups,
            deny_all_groups: info.deny_all_groups,
            allow_all_network_devices: info.allow_all_network_devices,
            deny_all_network_devices: info.deny_all_network_devices,
            allowed_users: info.allowed_users.iter().map(|v| v.id).collect(),
            denied_users: info.denied_users.iter().map(|v| v.id).collect(),
            allowed_groups: info.allowed_groups.iter().map(|v| v.id).collect(),
            denied_groups: info.denied_groups.iter().map(|v| v.id).collect(),
            allowed_network_devices: info.allowed_network_devices.iter().map(|v| v.id).collect(),
            denied_network_devices: info.denied_network_devices.iter().map(|v| v.id).collect(),
            aliases: info.aliases.iter().map(|v| v.id).collect(),
            destinations: info.destinations.iter().map(|v| v.id).collect(),
            protocols: info.protocols,
            enabled: info.enabled,
            any_address: info.any_address,
            any_port: info.any_port,
            any_protocol: info.any_protocol,
            use_manual_destination_settings: info.use_manual_destination_settings,
        }
    }
}

/// API representation of [`AclRule`] used in API requests for modification operations
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, ToSchema)]
pub struct EditAclRule {
    pub name: String,
    pub all_locations: bool,
    pub locations: Vec<Id>,
    pub expires: Option<NaiveDateTime>,
    pub enabled: bool,
    // source
    pub allow_all_users: bool,
    pub deny_all_users: bool,
    pub allow_all_groups: bool,
    pub deny_all_groups: bool,
    pub allow_all_network_devices: bool,
    pub deny_all_network_devices: bool,
    pub allowed_users: Vec<Id>,
    pub denied_users: Vec<Id>,
    pub allowed_groups: Vec<Id>,
    pub denied_groups: Vec<Id>,
    pub allowed_network_devices: Vec<Id>,
    pub denied_network_devices: Vec<Id>,
    // destination
    pub use_manual_destination_settings: bool,
    pub addresses: String,
    pub ports: String,
    pub protocols: Vec<Protocol>,
    pub any_address: bool,
    pub any_port: bool,
    pub any_protocol: bool,
    // aliases & destinations
    pub aliases: Vec<Id>,
    pub destinations: Vec<Id>,
}

impl EditAclRule {
    pub async fn validate(&self, conn: &mut PgConnection) -> Result<(), WebError> {
        if self.use_manual_destination_settings {
            // Determine what the selected component aliases collectively contribute.
            // Note: Component-kind aliases always have any_address/any_port/any_protocol = false,
            // so we only check whether they have non-empty arrays.
            let (alias_has_address, alias_has_port, alias_has_protocol) = if self.aliases.is_empty()
            {
                (false, false, false)
            } else {
                let row = query_as::<_, (Option<bool>, Option<bool>, Option<bool>)>(
                    "SELECT \
                            bool_or(array_length(addresses, 1) > 0) \
                                OR EXISTS ( \
                                    SELECT 1 FROM aclaliasdestinationrange dr \
                                    JOIN aclalias a ON a.id = dr.alias_id \
                                    WHERE a.id = ANY($1) \
                                      AND a.kind = 'component'::aclalias_kind \
                                ), \
                            bool_or(array_length(ports, 1) > 0), \
                            bool_or(array_length(protocols, 1) > 0) \
                        FROM aclalias WHERE id = ANY($1) AND kind = 'component'::aclalias_kind",
                )
                .bind(&self.aliases)
                .fetch_one(&mut *conn)
                .await
                .map_err(WebError::from)?;
                (
                    row.0.unwrap_or(false),
                    row.1.unwrap_or(false),
                    row.2.unwrap_or(false),
                )
            };

            if !self.any_address && self.addresses.trim().is_empty() && !alias_has_address {
                return Err(WebError::BadRequest(
                    "Must provide destination address, or enable any address".to_string(),
                ));
            }
            if !self.any_port && self.ports.trim().is_empty() && !alias_has_port {
                return Err(WebError::BadRequest(
                    "Must provide destination port, or enable any port".to_string(),
                ));
            }
            if !self.any_protocol && self.protocols.is_empty() && !alias_has_protocol {
                return Err(WebError::BadRequest(
                    "Must provide destination protocol, or enable any protocol".to_string(),
                ));
            }
        } else if self.destinations.is_empty() {
            return Err(WebError::BadRequest(
                "Must provide destination alias".to_string(),
            ));
        }

        // check if some allowed users/group/devices are configured
        if !self.allow_all_users
            && !self.allow_all_groups
            && !self.allow_all_network_devices
            && self.allowed_users.is_empty()
            && self.allowed_groups.is_empty()
            && self.allowed_network_devices.is_empty()
        {
            return Err(WebError::BadRequest(
                "Must provide some allowed users, groups or devices".to_string(),
            ));
        }

        Ok(())
    }
}

impl From<AclRuleInfo<Id>> for EditAclRule {
    fn from(info: AclRuleInfo<Id>) -> Self {
        Self {
            addresses: info.format_destination(),
            ports: info.format_ports(),
            name: info.name,
            all_locations: info.all_locations,
            locations: info.locations.iter().map(|v| v.id).collect(),
            expires: info.expires,
            allow_all_users: info.allow_all_users,
            deny_all_users: info.deny_all_users,
            allow_all_groups: info.allow_all_groups,
            deny_all_groups: info.deny_all_groups,
            allow_all_network_devices: info.allow_all_network_devices,
            deny_all_network_devices: info.deny_all_network_devices,
            allowed_users: info.allowed_users.iter().map(|v| v.id).collect(),
            denied_users: info.denied_users.iter().map(|v| v.id).collect(),
            allowed_groups: info.allowed_groups.iter().map(|v| v.id).collect(),
            denied_groups: info.denied_groups.iter().map(|v| v.id).collect(),
            allowed_network_devices: info.allowed_network_devices.iter().map(|v| v.id).collect(),
            denied_network_devices: info.denied_network_devices.iter().map(|v| v.id).collect(),
            aliases: info.aliases.iter().map(|v| v.id).collect(),
            destinations: info.destinations.iter().map(|v| v.id).collect(),
            protocols: info.protocols,
            enabled: info.enabled,
            any_address: info.any_address,
            any_port: info.any_port,
            any_protocol: info.any_protocol,
            use_manual_destination_settings: info.use_manual_destination_settings,
        }
    }
}

#[derive(Debug, Deserialize, ToSchema)]
pub(crate) struct ApplyAclRulesData {
    rules: Vec<Id>,
}

#[derive(Debug, Serialize, ToSchema, sqlx::FromRow)]
pub struct AclStateCount {
    pub applied: i64,
    pub pending: i64,
}

/// List all ACL rules.
#[utoipa::path(
    get,
    path = "/api/v1/acl/rule",
    tag = "ACL",
    responses(
        (status = OK, description = "ACL rules"),
    ),
)]
pub(crate) async fn list_acl_rules(
    _admin: AdminRole,
    State(appstate): State<AppState>,
    session: SessionInfo,
    pagination: Query<PaginationParams>,
) -> PaginatedApiResult<ApiAclRule> {
    let pagination = pagination.0;

    debug!("User {} listing ACL rules", session.user.username);

    let mut conn = appstate.pool.acquire().await?;
    let rules = AclRule::all_paginated(
        &mut *conn,
        i64::from(pagination.per_page()),
        i64::from(pagination.offset()),
    )
    .await?;
    let mut api_rules = Vec::with_capacity(rules.len());
    for rule in &rules {
        // TODO: may require optimisation wrt. sql queries
        let info = rule.to_info(&mut conn).await.map_err(|err| {
            error!("Error retrieving ACL rule {rule:?}: {err}");
            err
        })?;
        api_rules.push(info.into());
    }

    info!("User {} listed ACL rules", session.user.username);

    let count = AclRule::count(&mut *conn).await?;
    Ok(PaginatedApiResponse::new(
        api_rules,
        pagination,
        count as u32,
    ))
}

/// Count ACL rules by state.
#[utoipa::path(
    get,
    path = "/api/v1/acl/rule/count",
    tag = "ACL",
    responses(
        (status = OK, description = "ACL rule state counts", body = AclStateCount),
    ),
)]
pub(crate) async fn count_acl_rules(
    _admin: AdminRole,
    State(appstate): State<AppState>,
) -> ApiResult {
    let counts = query_as::<_, AclStateCount>(
        "SELECT \
            COUNT(*) FILTER (WHERE state = 'applied'::aclrule_state) AS applied, \
            COUNT(*) FILTER ( \
                WHERE state IN ('new'::aclrule_state, 'modified'::aclrule_state, 'deleted'::aclrule_state) \
            ) AS pending \
        FROM aclrule",
    )
    .fetch_one(&appstate.pool)
    .await?;

    Ok(ApiResponse::json(counts, StatusCode::OK))
}

/// Get ACL rule.
#[utoipa::path(
    get,
    path = "/api/v1/acl/rule/{id}",
    tag = "ACL",
    params(
        ("id" = Id, Path, description = "ID of ACL rule",)
    ),
    responses(
        (status = OK, description = "ACL rule"),
    )
)]
pub(crate) async fn get_acl_rule(
    _license: LicenseInfo,
    _admin: AdminRole,
    State(appstate): State<AppState>,
    session: SessionInfo,
    Path(id): Path<Id>,
) -> ApiResult {
    debug!("User {} retrieving ACL rule {id}", session.user.username);
    let mut conn = appstate.pool.acquire().await?;
    let (rule, status) = match AclRule::find_by_id(&mut *conn, id).await? {
        Some(rule) => (
            json!(ApiAclRule::from(rule.to_info(&mut conn).await.map_err(
                |err| {
                    error!("Error retrieving ACL rule {rule:?}: {err}");
                    err
                }
            )?)),
            StatusCode::OK,
        ),
        None => (Value::Null, StatusCode::NOT_FOUND),
    };

    info!("User {} retrieved ACL rule {id}", session.user.username);
    Ok(ApiResponse::new(rule, status))
}

/// Create ACL rule.
#[utoipa::path(
    post,
    path = "/api/v1/acl/rule",
    tag = "ACL",
    request_body = EditAclRule,
    responses(
        (status = OK, description = "ACL rule"),
    )
)]
pub(crate) async fn create_acl_rule(
    _license: LicenseInfo,
    _admin: AdminRole,
    State(appstate): State<AppState>,
    session: SessionInfo,
    Json(data): Json<EditAclRule>,
) -> ApiResult {
    debug!("User {} creating ACL rule {data:?}", session.user.username);

    let mut tx = appstate.pool.begin().await?;
    data.validate(&mut tx).await?;
    let rule = AclRule::create_from_api(&mut tx, &data, &session.user.username)
        .await
        .map_err(|err| {
            error!("Error creating ACL rule {data:?}: {err}");
            err
        })?;
    tx.commit().await?;
    info!(
        "User {} created ACL rule {}",
        session.user.username, rule.id
    );
    Ok(ApiResponse::json(rule, StatusCode::CREATED))
}

/// Update ACL rule.
#[utoipa::path(
    put,
    path = "/api/v1/acl/rule/{id}",
    tag = "ACL",
    params(
        ("id" = Id, Path, description = "ID of ACL rule",)
    ),
    request_body = EditAclRule,
    responses(
        (status = OK, description = "ACL rule"),
    )
)]
pub(crate) async fn update_acl_rule(
    _license: LicenseInfo,
    _admin: AdminRole,
    State(appstate): State<AppState>,
    session: SessionInfo,
    Path(id): Path<Id>,
    Json(data): Json<EditAclRule>,
) -> ApiResult {
    debug!("User {} updating ACL rule {data:?}", session.user.username);

    let mut tx = appstate.pool.begin().await?;
    data.validate(&mut tx).await?;
    let rule = AclRule::update_from_api(&mut tx, id, &data, &session.user.username)
        .await
        .map_err(|err| {
            error!("Error updating ACL rule {data:?}: {err}");
            err
        })?;
    tx.commit().await?;
    info!("User {} updated ACL rule", session.user.username);
    Ok(ApiResponse::json(rule, StatusCode::OK))
}

/// Delete ACL rule.
#[utoipa::path(
    delete,
    path = "/api/v1/acl/rule/{id}",
    tag = "ACL",
    params(
        ("id" = Id, Path, description = "ID of ACL rule",)
    ),
    responses(
        (status = OK, description = "ACL rule"),
    )
)]
pub(crate) async fn delete_acl_rule(
    _license: LicenseInfo,
    _admin: AdminRole,
    State(appstate): State<AppState>,
    session: SessionInfo,
    Path(id): Path<Id>,
) -> ApiResult {
    debug!("User {} deleting ACL rule {id}", session.user.username);
    AclRule::delete_from_api(&appstate.pool, id, &session.user.username)
        .await
        .map_err(|err| {
            error!("Error deleting ACL rule {id}: {err}");
            err
        })?;
    info!("User {} deleted ACL rule {id}", session.user.username);
    Ok(ApiResponse::default())
}

/// Apply ACL alias.
#[utoipa::path(
    put,
    path = "/api/v1/acl/rule/apply",
    request_body = ApplyAclRulesData,
    responses(
        (status = OK, description = "ACL alias"),
    )
)]
pub(crate) async fn apply_acl_rules(
    _license: LicenseInfo,
    _admin: AdminRole,
    State(appstate): State<AppState>,
    session: SessionInfo,
    Json(data): Json<ApplyAclRulesData>,
) -> ApiResult {
    debug!(
        "User {} applying ACL rules: {:?}",
        session.user.username, data.rules
    );
    AclRule::apply_rules(&data.rules, &session.user.username, &appstate)
        .await
        .map_err(|err| {
            error!("Error applying ACL rules {data:?}: {err}");
            err
        })?;
    info!(
        "User {} applied ACL rules: {:?}",
        session.user.username, data.rules
    );
    Ok(ApiResponse::default())
}
