//! System-level agent permissions, distinct from filesystem/bash permissions.
//!
//! This model stores the policy the user wants IdeA to apply. It does not claim
//! that an external assistant runtime can be elevated live: resolution exposes
//! the wanted policy separately from the observed runtime state.

use serde::{Deserialize, Serialize};

use crate::ids::AgentId;

/// Current schema version for `.ideai/system-permissions.json`.
pub const SYSTEM_PERMISSIONS_VERSION: u32 = 1;

/// Wanted/effective network policy.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum NetworkPolicy {
    /// Network access is wanted/observed as allowed.
    Allow,
    /// Network access is wanted/observed as denied.
    Deny,
    /// Ask before allowing network access when the runtime supports it.
    Ask,
}

/// Optional system permissions.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SystemPermissionSet {
    /// Network policy. `None` means no IdeA-level policy has been set.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub network: Option<NetworkPolicy>,
}

impl SystemPermissionSet {
    /// Builds a set from an optional network policy.
    #[must_use]
    pub const fn new(network: Option<NetworkPolicy>) -> Self {
        Self { network }
    }

    /// Whether the set carries no policy.
    #[must_use]
    pub const fn is_empty(&self) -> bool {
        self.network.is_none()
    }
}

/// Per-agent system permission override.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentSystemPermissionOverride {
    /// Agent id.
    pub agent_id: AgentId,
    /// Agent-specific permissions.
    pub permissions: SystemPermissionSet,
}

impl AgentSystemPermissionOverride {
    /// Builds an override.
    #[must_use]
    pub const fn new(agent_id: AgentId, permissions: SystemPermissionSet) -> Self {
        Self {
            agent_id,
            permissions,
        }
    }
}

/// Persisted project system permission document.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectSystemPermissions {
    /// Document format version.
    pub version: u32,
    /// Optional project-wide default permissions.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub project_default: Option<SystemPermissionSet>,
    /// Per-agent overrides.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub agents: Vec<AgentSystemPermissionOverride>,
}

impl Default for ProjectSystemPermissions {
    fn default() -> Self {
        Self {
            version: SYSTEM_PERMISSIONS_VERSION,
            project_default: None,
            agents: Vec::new(),
        }
    }
}

impl ProjectSystemPermissions {
    /// Builds a document.
    #[must_use]
    pub fn new(
        project_default: Option<SystemPermissionSet>,
        agents: Vec<AgentSystemPermissionOverride>,
    ) -> Self {
        Self {
            version: SYSTEM_PERMISSIONS_VERSION,
            project_default,
            agents,
        }
    }

    /// Replaces the project default permissions.
    pub fn set_project_default(&mut self, permissions: Option<SystemPermissionSet>) {
        self.project_default = permissions.filter(|set| !set.is_empty());
    }

    /// Returns the override for one agent, if present.
    #[must_use]
    pub fn agent_permissions(&self, agent_id: AgentId) -> Option<&SystemPermissionSet> {
        self.agents
            .iter()
            .find(|entry| entry.agent_id == agent_id)
            .map(|entry| &entry.permissions)
    }

    /// Replaces or removes an agent override.
    pub fn set_agent_permissions(
        &mut self,
        agent_id: AgentId,
        permissions: Option<SystemPermissionSet>,
    ) {
        self.agents.retain(|entry| entry.agent_id != agent_id);
        if let Some(permissions) = permissions.filter(|set| !set.is_empty()) {
            self.agents
                .push(AgentSystemPermissionOverride::new(agent_id, permissions));
        }
    }

    /// Resolves the wanted network policy before runtime constraints.
    #[must_use]
    pub fn wanted_network_for(&self, agent_id: AgentId) -> Option<NetworkPolicy> {
        self.agent_permissions(agent_id)
            .and_then(|set| set.network)
            .or_else(|| self.project_default.as_ref().and_then(|set| set.network))
    }
}

/// Runtime lock state.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum RuntimeLockState {
    /// No runtime lock is known.
    None,
    /// Runtime locks the effective network policy.
    Locked,
}

/// Runtime lock details.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RuntimeLock {
    /// Lock state.
    pub state: RuntimeLockState,
    /// Runtime/source that imposes the lock, when known.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub source: Option<String>,
    /// Human-readable reason.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
}

impl RuntimeLock {
    /// No known runtime lock.
    #[must_use]
    pub const fn none() -> Self {
        Self {
            state: RuntimeLockState::None,
            source: None,
            reason: None,
        }
    }

    /// Runtime-locked state.
    #[must_use]
    pub fn locked(source: impl Into<String>, reason: impl Into<String>) -> Self {
        Self {
            state: RuntimeLockState::Locked,
            source: Some(source.into()),
            reason: Some(reason.into()),
        }
    }
}

/// UI control mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum SystemPermissionControlMode {
    /// IdeA can edit the wanted policy.
    Editable,
    /// IdeA can only display the state.
    ReadOnly,
}

/// UI control state.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SystemPermissionControl {
    /// Control mode.
    pub mode: SystemPermissionControlMode,
    /// Human-readable reason for read-only state.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
}

impl SystemPermissionControl {
    /// Editable control.
    #[must_use]
    pub const fn editable() -> Self {
        Self {
            mode: SystemPermissionControlMode::Editable,
            reason: None,
        }
    }

    /// Read-only control.
    #[must_use]
    pub fn read_only(reason: impl Into<String>) -> Self {
        Self {
            mode: SystemPermissionControlMode::ReadOnly,
            reason: Some(reason.into()),
        }
    }
}

/// Read-only snapshot from the host/provider runtime.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimePermissionSnapshot {
    /// Effective network policy observed or conservatively inferred.
    pub effective_network: NetworkPolicy,
    /// Runtime lock details.
    pub runtime_lock: RuntimeLock,
    /// Runtime control state, distinct from editing the persisted wanted policy.
    pub runtime_control: SystemPermissionControl,
}

impl RuntimePermissionSnapshot {
    /// Snapshot for an agent with no known active runtime lock.
    #[must_use]
    pub fn unobserved() -> Self {
        const REASON: &str = "No active runtime network permission state is currently observed.";
        Self {
            effective_network: NetworkPolicy::Deny,
            runtime_lock: RuntimeLock::none(),
            runtime_control: SystemPermissionControl::read_only(REASON),
        }
    }

    /// Snapshot for a runtime that IdeA cannot inspect or elevate.
    #[must_use]
    pub fn locked_uninspectable() -> Self {
        const REASON: &str =
            "IdeA cannot inspect or change network access for the active external runtime.";
        Self {
            effective_network: NetworkPolicy::Deny,
            runtime_lock: RuntimeLock::locked("external-runtime", REASON),
            runtime_control: SystemPermissionControl::read_only(REASON),
        }
    }
}

/// Resolved read-model for one agent.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResolvedAgentSystemPermissions {
    /// Wanted network policy from project/agent configuration.
    pub wanted: Option<NetworkPolicy>,
    /// Effective network policy after runtime constraints.
    pub effective: NetworkPolicy,
    /// Runtime lock state.
    pub runtime_lock: RuntimeLock,
    /// Whether the UI can edit the persisted wanted policy.
    pub control: SystemPermissionControl,
    /// Whether IdeA can inspect or change the active runtime state.
    pub runtime_control: SystemPermissionControl,
}

/// Resolves wanted system permissions against the runtime snapshot.
#[must_use]
pub fn resolve_agent_system_permissions(
    doc: &ProjectSystemPermissions,
    agent_id: AgentId,
    runtime: RuntimePermissionSnapshot,
) -> ResolvedAgentSystemPermissions {
    let wanted = doc.wanted_network_for(agent_id);
    let effective = match runtime.runtime_lock.state {
        RuntimeLockState::Locked => runtime.effective_network,
        RuntimeLockState::None => wanted.unwrap_or(runtime.effective_network),
    };
    ResolvedAgentSystemPermissions {
        wanted,
        effective,
        runtime_lock: runtime.runtime_lock,
        control: SystemPermissionControl::editable(),
        runtime_control: runtime.runtime_control,
    }
}

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

    #[test]
    fn agent_override_wins_over_project_default_for_wanted_policy() {
        let agent = AgentId::new_random();
        let doc = ProjectSystemPermissions::new(
            Some(SystemPermissionSet::new(Some(NetworkPolicy::Ask))),
            vec![AgentSystemPermissionOverride::new(
                agent,
                SystemPermissionSet::new(Some(NetworkPolicy::Deny)),
            )],
        );

        assert_eq!(doc.wanted_network_for(agent), Some(NetworkPolicy::Deny));
    }

    #[test]
    fn locked_runtime_controls_effective_policy_without_making_wanted_read_only() {
        let agent = AgentId::new_random();
        let doc = ProjectSystemPermissions::default();

        let resolved = resolve_agent_system_permissions(
            &doc,
            agent,
            RuntimePermissionSnapshot::locked_uninspectable(),
        );

        assert_eq!(resolved.wanted, None);
        assert_eq!(resolved.effective, NetworkPolicy::Deny);
        assert_eq!(resolved.runtime_lock.state, RuntimeLockState::Locked);
        assert_eq!(resolved.control.mode, SystemPermissionControlMode::Editable);
        assert_eq!(
            resolved.runtime_control.mode,
            SystemPermissionControlMode::ReadOnly
        );
    }

    #[test]
    fn unobserved_runtime_has_no_active_lock_and_preserves_wanted_effective_policy() {
        let agent = AgentId::new_random();
        let doc = ProjectSystemPermissions::new(
            Some(SystemPermissionSet::new(Some(NetworkPolicy::Allow))),
            vec![],
        );

        let resolved =
            resolve_agent_system_permissions(&doc, agent, RuntimePermissionSnapshot::unobserved());

        assert_eq!(resolved.wanted, Some(NetworkPolicy::Allow));
        assert_eq!(resolved.effective, NetworkPolicy::Allow);
        assert_eq!(resolved.runtime_lock.state, RuntimeLockState::None);
        assert_eq!(resolved.control.mode, SystemPermissionControlMode::Editable);
        assert_eq!(
            resolved.runtime_control.mode,
            SystemPermissionControlMode::ReadOnly
        );
    }
}
