use crate::syftbox::storage::SyftBoxStorage;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use serde_yaml;
use std::fs;
use std::path::{Path, PathBuf};

const PERMISSION_FILE_NAME: &str = "syft.pub.yaml";
pub const DEFAULT_RPC_PERMISSION_CONTENT: &str = r#"rules:
  - pattern: "**/*.request"
    access:
      admin: []
      read:
        - "*"
      write:
        - "*"
  - pattern: "**/*.response"
    access:
      admin: []
      read:
        - "*"
      write:
        - "*"
"#;

pub const DEFAULT_APP_PERMISSION_CONTENT: &str = r#"rules:
  - pattern: "**"
    access:
      admin: []
      read:
        - "*"
      write: []
"#;

/// Represents a SyftBox application
#[derive(Debug, Clone)]
pub struct SyftBoxApp {
    pub app_name: String,
    pub email: String,
    pub data_dir: PathBuf,
    pub app_data_dir: PathBuf,
    pub rpc_dir: PathBuf,
    pub storage: SyftBoxStorage,
}

pub struct SessionPaths {
    pub owner_path: PathBuf,
    pub peer_path: PathBuf,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
struct Access {
    #[serde(default)]
    admin: Vec<String>,
    #[serde(default)]
    read: Vec<String>,
    #[serde(default)]
    write: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
struct Rule {
    pattern: String,
    access: Access,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
struct RuleSet {
    #[serde(default)]
    rules: Vec<Rule>,
    #[serde(default)]
    terminal: bool,
}

impl SyftBoxApp {
    /// Create a new SyftBox app, ensuring all necessary directories and files exist
    pub fn new(data_dir: &Path, email: &str, app_name: &str) -> Result<Self> {
        let app_data_dir = data_dir
            .join("datasites")
            .join(email)
            .join("app_data")
            .join(app_name);

        let rpc_dir = app_data_dir.join("rpc");

        let storage = SyftBoxStorage::new(data_dir);

        let app = Self {
            app_name: app_name.to_string(),
            email: email.to_string(),
            data_dir: data_dir.to_path_buf(),
            app_data_dir,
            rpc_dir,
            storage,
        };

        app.ensure_initialized()?;
        Ok(app)
    }

    /// Ensure the app directory structure and permission files exist
    fn ensure_initialized(&self) -> Result<()> {
        // Create app_data directory if it doesn't exist
        if !self.app_data_dir.exists() {
            fs::create_dir_all(&self.app_data_dir).with_context(|| {
                format!(
                    "Failed to create app_data directory: {:?}",
                    self.app_data_dir
                )
            })?;
            // quiet: avoid noisy output in normal operations
        }

        // Create app-level permission file if it doesn't exist
        let app_permission_file = self.app_data_dir.join(PERMISSION_FILE_NAME);
        if !app_permission_file.exists() {
            self.storage.write_plaintext_file(
                &app_permission_file,
                DEFAULT_APP_PERMISSION_CONTENT.as_bytes(),
                false,
            )?;
        }

        // Create RPC directory if it doesn't exist
        if !self.rpc_dir.exists() {
            fs::create_dir_all(&self.rpc_dir)
                .with_context(|| format!("Failed to create RPC directory: {:?}", self.rpc_dir))?;
            // quiet
        }

        // Create RPC permission file if it doesn't exist
        let rpc_permission_file = self.rpc_dir.join(PERMISSION_FILE_NAME);
        if !rpc_permission_file.exists() {
            self.storage.write_plaintext_file(
                &rpc_permission_file,
                DEFAULT_RPC_PERMISSION_CONTENT.as_bytes(),
                false,
            )?;
        }

        Ok(())
    }

    /// Get the path for a specific endpoint
    pub fn endpoint_path(&self, endpoint_name: &str) -> PathBuf {
        // Remove leading slash if present
        let clean_name = endpoint_name.trim_start_matches('/');
        self.rpc_dir.join(clean_name)
    }

    /// Register a new endpoint by creating its directory
    pub fn register_endpoint(&self, endpoint_name: &str) -> Result<PathBuf> {
        let endpoint_dir = self.endpoint_path(endpoint_name);

        if !endpoint_dir.exists() {
            fs::create_dir_all(&endpoint_dir).with_context(|| {
                format!("Failed to create endpoint directory: {:?}", endpoint_dir)
            })?;
            // quiet
        }

        Ok(endpoint_dir)
    }

    /// Check if an endpoint exists
    pub fn endpoint_exists(&self, endpoint_name: &str) -> bool {
        self.endpoint_path(endpoint_name).exists()
    }

    /// List all registered endpoints
    pub fn list_endpoints(&self) -> Result<Vec<String>> {
        let mut endpoints = Vec::new();

        if self.rpc_dir.exists() {
            for entry in fs::read_dir(&self.rpc_dir)? {
                let entry = entry?;
                let path = entry.path();

                // Skip the permission file
                if path.is_dir() {
                    if let Some(name) = path.file_name() {
                        if let Some(name_str) = name.to_str() {
                            endpoints.push(format!("/{}", name_str));
                        }
                    }
                }
            }
        }

        Ok(endpoints)
    }

    /// Build a syft:// URL for an endpoint
    pub fn build_syft_url(&self, endpoint_name: &str) -> String {
        let clean_endpoint = endpoint_name.trim_start_matches('/');
        format!(
            "syft://{}/app_data/{}/rpc/{}",
            self.email, self.app_name, clean_endpoint
        )
    }

    fn merge_rule_access(rule: &mut Rule, owner: &str, peer: &str, allow_write: bool) {
        let add_unique = |vec: &mut Vec<String>, val: &str| {
            if !vec.iter().any(|v| v == val) {
                vec.push(val.to_string());
            }
        };
        add_unique(&mut rule.access.admin, owner);
        add_unique(&mut rule.access.read, owner);
        add_unique(&mut rule.access.read, peer);
        if allow_write {
            add_unique(&mut rule.access.write, owner);
            add_unique(&mut rule.access.write, peer);
        }
    }

    fn ensure_acl(
        &self,
        target_dir: &Path,
        owner: &str,
        peer: &str,
        allow_write: bool,
    ) -> Result<()> {
        let perm_path = target_dir.join(PERMISSION_FILE_NAME);

        let mut ruleset = if perm_path.exists() {
            let content = fs::read_to_string(&perm_path)
                .with_context(|| format!("Failed to read ACL file {:?}", perm_path))?;
            serde_yaml::from_str::<RuleSet>(&content).unwrap_or_default()
        } else {
            RuleSet::default()
        };

        if ruleset.rules.is_empty() {
            ruleset.rules.push(Rule {
                pattern: "**".to_string(),
                access: Access::default(),
            });
        }

        // Use or create catch-all rule
        let mut has_catch_all = false;
        for rule in ruleset.rules.iter_mut() {
            if rule.pattern == "**" {
                has_catch_all = true;
                Self::merge_rule_access(rule, owner, peer, allow_write);
            }
        }
        if !has_catch_all {
            let mut rule = Rule {
                pattern: "**".to_string(),
                access: Access::default(),
            };
            Self::merge_rule_access(&mut rule, owner, peer, allow_write);
            ruleset.rules.push(rule);
        }

        let serialized =
            serde_yaml::to_string(&ruleset).context("Failed to serialize ACL ruleset")?;
        self.storage
            .write_plaintext_file(&perm_path, serialized.as_bytes(), true)?;

        Ok(())
    }

    /// Ensure a peer can read (and optionally write) under a relative path in the owner's datasite.
    ///
    /// Creates the directory if missing and updates/merges syft.pub.yaml to include owner+peer.
    pub fn ensure_peer_can_read<P: AsRef<Path>>(
        &self,
        relative_path: P,
        peer_email: &str,
        allow_write: bool,
    ) -> Result<PathBuf> {
        let relative_path = relative_path.as_ref();
        let target_dir = self
            .data_dir
            .join("datasites")
            .join(&self.email)
            .join(relative_path);

        fs::create_dir_all(&target_dir).with_context(|| {
            format!(
                "Failed to create target directory for ACL: {:?}",
                target_dir
            )
        })?;

        self.ensure_acl(&target_dir, &self.email, peer_email, allow_write)?;

        Ok(target_dir)
    }
}

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

    #[test]
    fn test_app_initialization() -> Result<()> {
        let temp_dir = TempDir::new()?;
        let app = SyftBoxApp::new(temp_dir.path(), "test@example.com", "test_app")?;

        // Check that directories were created
        assert!(app.rpc_dir.exists());

        // Check that permission file was created
        let permission_file = app.rpc_dir.join(PERMISSION_FILE_NAME);
        assert!(permission_file.exists());

        Ok(())
    }

    #[test]
    fn test_endpoint_registration() -> Result<()> {
        let temp_dir = TempDir::new()?;
        let app = SyftBoxApp::new(temp_dir.path(), "test@example.com", "test_app")?;

        // Register an endpoint
        let endpoint_path = app.register_endpoint("/message")?;
        assert!(endpoint_path.exists());
        assert!(app.endpoint_exists("/message"));

        // List endpoints
        let endpoints = app.list_endpoints()?;
        assert!(endpoints.contains(&"/message".to_string()));

        Ok(())
    }

    #[test]
    fn test_syft_url_building() {
        let temp_dir = TempDir::new().unwrap();
        let app = SyftBoxApp::new(temp_dir.path(), "test@example.com", "test_app").unwrap();

        let url = app.build_syft_url("/message");
        assert_eq!(url, "syft://test@example.com/app_data/test_app/rpc/message");
    }
}
