use std::fs;
use std::path::Path;

use serde::{Deserialize, Serialize};

use crate::pipe::{Action, ExecutionSurfaceMetadata};
use crate::security::SecurityError;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MacPolicy {
    pub version: String,
    pub domains: DomainRules,
    pub pipe_actions: PipeActionRules,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DomainRules {
    pub allowed: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipeActionRules {
    pub allowed: Vec<String>,
    #[serde(default)]
    pub blocked: Vec<String>,
}

const LOCAL_DASHBOARD_EXPECTED_DOMAIN: &str = "__sgclaw_local_dashboard__";

impl MacPolicy {
    pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self, SecurityError> {
        let contents = fs::read_to_string(path)?;
        let policy: Self = serde_json::from_str(&contents)?;
        policy.validate_rules()?;
        Ok(policy)
    }

    pub fn from_json_str(contents: &str) -> Result<Self, SecurityError> {
        let policy: Self = serde_json::from_str(contents)?;
        policy.validate_rules()?;
        Ok(policy)
    }

    pub fn supports_pipe_action(&self, action: &Action) -> bool {
        let action_name = action.as_str();
        !self
            .pipe_actions
            .blocked
            .iter()
            .any(|blocked| blocked == action_name)
            && self
                .pipe_actions
                .allowed
                .iter()
                .any(|allowed| allowed == action_name)
    }

    pub fn validate(&self, action: &Action, expected_domain: &str) -> Result<(), SecurityError> {
        let action_name = action.as_str();
        if self
            .pipe_actions
            .blocked
            .iter()
            .any(|blocked| blocked == action_name)
        {
            return Err(SecurityError::ActionNotAllowed(action_name.to_string()));
        }

        if !self
            .pipe_actions
            .allowed
            .iter()
            .any(|allowed| allowed == action_name)
        {
            return Err(SecurityError::ActionNotAllowed(action_name.to_string()));
        }

        let normalized = normalize_domain(expected_domain);
        if normalized.is_empty() {
            return Err(SecurityError::DomainNotAllowed(expected_domain.to_string()));
        }

        if !self
            .domains
            .allowed
            .iter()
            .map(|domain| normalize_domain(domain))
            .any(|allowed| allowed == normalized)
        {
            return Err(SecurityError::DomainNotAllowed(normalized));
        }

        Ok(())
    }

    pub fn validate_local_dashboard_presentation(
        &self,
        action: &Action,
        expected_domain: &str,
        presentation_url: &str,
        output_path: &str,
    ) -> Result<(), SecurityError> {
        let action_name = action.as_str();
        if self
            .pipe_actions
            .blocked
            .iter()
            .any(|blocked| blocked == action_name)
        {
            return Err(SecurityError::ActionNotAllowed(action_name.to_string()));
        }

        if !self
            .pipe_actions
            .allowed
            .iter()
            .any(|allowed| allowed == action_name)
        {
            return Err(SecurityError::ActionNotAllowed(action_name.to_string()));
        }

        if action != &Action::Navigate {
            return Err(SecurityError::InvalidLocalDashboard(
                "local dashboard open only supports navigate".to_string(),
            ));
        }
        if expected_domain != LOCAL_DASHBOARD_EXPECTED_DOMAIN {
            return Err(SecurityError::InvalidLocalDashboard(
                "local dashboard expected_domain is invalid".to_string(),
            ));
        }
        if !presentation_url.starts_with("file:///") {
            return Err(SecurityError::InvalidLocalDashboard(
                "local dashboard presentation_url must be file:///".to_string(),
            ));
        }
        if !output_path.to_ascii_lowercase().ends_with(".html") {
            return Err(SecurityError::InvalidLocalDashboard(
                "local dashboard output_path must point to .html".to_string(),
            ));
        }

        let normalized_output = normalize_local_dashboard_path(output_path);
        let normalized_presentation = normalize_local_dashboard_file_url(presentation_url)?;
        if normalized_output != normalized_presentation {
            return Err(SecurityError::InvalidLocalDashboard(
                "local dashboard presentation_url does not match output_path".to_string(),
            ));
        }

        Ok(())
    }

    pub fn privileged_surface_metadata(&self) -> ExecutionSurfaceMetadata {
        let mut metadata = ExecutionSurfaceMetadata::privileged_browser_pipe("mac_policy");
        metadata.allowed_domains = self.domains.allowed.clone();
        metadata.allowed_actions = self.pipe_actions.allowed.clone();
        metadata
    }

    fn validate_rules(&self) -> Result<(), SecurityError> {
        if self.version.trim().is_empty() {
            return Err(SecurityError::InvalidRules(
                "rules version must not be empty".to_string(),
            ));
        }
        if self.domains.allowed.is_empty() {
            return Err(SecurityError::InvalidRules(
                "at least one allowed domain is required".to_string(),
            ));
        }
        if self.pipe_actions.allowed.is_empty() {
            return Err(SecurityError::InvalidRules(
                "at least one allowed action is required".to_string(),
            ));
        }
        Ok(())
    }
}

fn normalize_domain(raw: &str) -> String {
    raw.trim()
        .trim_start_matches("https://")
        .trim_start_matches("http://")
        .split('/')
        .next()
        .unwrap_or_default()
        .split(':')
        .next()
        .unwrap_or_default()
        .to_ascii_lowercase()
}

fn normalize_local_dashboard_path(raw: &str) -> String {
    raw.trim().replace('\\', "/").to_ascii_lowercase()
}

fn normalize_local_dashboard_file_url(raw: &str) -> Result<String, SecurityError> {
    let path = raw
        .trim()
        .strip_prefix("file:///")
        .ok_or_else(|| {
            SecurityError::InvalidLocalDashboard(
                "local dashboard presentation_url must be file:///".to_string(),
            )
        })?;
    Ok(normalize_local_dashboard_path(path))
}
