use anyhow::Result;
use log::{info, warn};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use reqwest::Client;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DirectoryConfig {
    pub base_url: String,
    pub issuer_url: String,
    pub issuer: String,
    pub client_id: String,
    pub client_secret: String,
    pub redirect_uri: String,
    pub project_id: String,
    pub api_url: String,
    pub service_account_key: Option<String>,
}

pub struct DirectorySetup {
    base_url: String,
    config_path: PathBuf,
    http_client: Client,
    pat_token: Option<String>,
}

#[derive(Debug, Serialize)]
struct CreateProjectRequest {
    name: String,
}

#[derive(Debug, Deserialize)]
struct ProjectResponse {
    id: String,
}

impl DirectorySetup {
    pub fn new(base_url: String, config_path: PathBuf) -> Self {
        let http_client = Client::builder()
            .timeout(std::time::Duration::from_secs(30))
            .build()
            .unwrap_or_else(|_| Client::new());

        Self {
            base_url,
            config_path,
            http_client,
            pat_token: None,
        }
    }

    pub async fn initialize(&mut self) -> Result<DirectoryConfig> {
        info!("Initializing Directory (Zitadel) OAuth client...");

        // Step 1: Wait for Zitadel to be ready
        self.wait_for_zitadel().await?;

        // Step 2: Load PAT token from file (created by start-from-init --steps)
        info!("Loading PAT token from Zitadel init steps...");
        self.load_pat_token()?;

        // Step 3: Get or create project
        let project_id = self.get_or_create_project().await?;
        info!("Using project ID: {project_id}");

        // Step 4: Create OAuth application
        let (client_id, client_secret) = self.create_oauth_application(&project_id).await?;
        info!("Created OAuth application with client_id: {client_id}");

        // Step 5: Create config
        let config = DirectoryConfig {
            base_url: self.base_url.clone(),
            issuer_url: self.base_url.clone(),
            issuer: self.base_url.clone(),
            client_id,
            client_secret,
            redirect_uri: "http://localhost:3000/auth/callback".to_string(),
            project_id,
            api_url: self.base_url.clone(),
            service_account_key: None,
        };

        // Step 6: Save config
        self.save_config(&config)?;

        Ok(config)
    }

    async fn wait_for_zitadel(&self) -> Result<()> {
        info!("Waiting for Zitadel to be ready...");

        for i in 1..=120 {
            match self.http_client
                .get(format!("{}/debug/healthz", self.base_url))
                .send()
                .await
            {
                Ok(response) if response.status().is_success() => {
                    info!("Zitadel is ready (healthz OK)");
                    return Ok(());
                }
                _ => {
                    if i % 15 == 0 {
                        info!("Still waiting for Zitadel... (attempt {i}/120)");
                    }
                    tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
                }
            }
        }

        Err(anyhow::anyhow!("Zitadel did not become ready within 240 seconds"))
    }

    /// Load the PAT token from the file generated by Zitadel's start-from-init --steps
    /// The steps YAML configures FirstInstance.Org.PatPath which tells Zitadel to
    /// create a machine user with IAM_OWNER role and write its PAT to disk
    fn load_pat_token(&mut self) -> Result<()> {
        let stack_path = crate::core::shared::utils::get_stack_path();

        let pat_path = PathBuf::from(&stack_path).join("conf/directory/admin-pat.txt");

        if pat_path.exists() {
            let pat_token = std::fs::read_to_string(&pat_path)
                .map_err(|e| anyhow::anyhow!("Failed to read PAT file {}: {}", pat_path.display(), e))?
                .trim()
                .to_string();

            if pat_token.is_empty() {
                return Err(anyhow::anyhow!(
                    "PAT file exists at {} but is empty. Zitadel start-from-init may have failed.",
                    pat_path.display()
                ));
            }

            info!("Loaded PAT token from: {} (len={})", pat_path.display(), pat_token.len());
            self.pat_token = Some(pat_token);
            return Ok(());
        }

        // Also check the legacy location
        let stack = crate::core::shared::utils::get_stack_path();
        let legacy_pat_path = std::path::PathBuf::from(format!("{}/conf/directory/admin-pat.txt", stack));
        if legacy_pat_path.exists() {
            let pat_token = std::fs::read_to_string(legacy_pat_path)
                .map_err(|e| anyhow::anyhow!("Failed to read PAT file: {e}"))?
                .trim()
                .to_string();

            if !pat_token.is_empty() {
                info!("Loaded PAT token from legacy path");
                self.pat_token = Some(pat_token);
                return Ok(());
            }
        }

        Err(anyhow::anyhow!(
            "No PAT token file found at {}. \
             Zitadel must be started with 'start-from-init --steps <steps.yaml>' \
             where steps.yaml has FirstInstance.Org.PatPath configured.",
            pat_path.display()
        ))
    }

    async fn get_or_create_project(&self) -> Result<String> {
        info!("Getting or creating Zitadel project...");

        let auth_header = self.get_auth_header()?;

        // Try to list existing projects via management API v1
        let list_response = self.http_client
            .post(format!("{}/management/v1/projects/_search", self.base_url))
            .header("Authorization", &auth_header)
            .json(&serde_json::json!({}))
            .send()
            .await?;

        if list_response.status().is_success() {
            let projects: serde_json::Value = list_response.json().await?;

            if let Some(result) = projects.get("result").and_then(|r| r.as_array()) {
                for project in result {
                    if project.get("name")
                        .and_then(|n| n.as_str())
                        .map(|n| n == "General Bots")
                        .unwrap_or(false)
                    {
                        if let Some(id) = project.get("id").and_then(|i| i.as_str()) {
                            info!("Found existing 'General Bots' project: {id}");
                            return Ok(id.to_string());
                        }
                    }
                }
            }
        }

        // Create new project
        info!("Creating new 'General Bots' project...");
        let create_request = CreateProjectRequest {
            name: "General Bots".to_string(),
        };

        let create_response = self.http_client
            .post(format!("{}/management/v1/projects", self.base_url))
            .header("Authorization", self.get_auth_header()?)
            .json(&create_request)
            .send()
            .await?;

        if !create_response.status().is_success() {
            let error_text = create_response.text().await.unwrap_or_default();
            return Err(anyhow::anyhow!("Failed to create project: {error_text}"));
        }

        let project: ProjectResponse = create_response.json().await?;
        info!("Created project with ID: {}", project.id);

        Ok(project.id)
    }

    async fn create_oauth_application(&self, project_id: &str) -> Result<(String, String)> {
        info!("Creating OAuth/OIDC application for BotServer...");

        let auth_header = self.get_auth_header()?;

        // Use the management v1 OIDC app creation endpoint which returns
        // client_id and client_secret in the response directly
        let app_body = serde_json::json!({
            "name": "BotServer",
            "redirectUris": [
                "http://localhost:3000/auth/callback",
                "http://localhost:8080/auth/callback"
            ],
            "responseTypes": ["OIDC_RESPONSE_TYPE_CODE"],
            "grantTypes": [
                "OIDC_GRANT_TYPE_AUTHORIZATION_CODE",
                "OIDC_GRANT_TYPE_REFRESH_TOKEN"
            ],
            "appType": "OIDC_APP_TYPE_WEB",
            "authMethodType": "OIDC_AUTH_METHOD_TYPE_POST",
            "postLogoutRedirectUris": ["http://localhost:3000"],
            "devMode": true
        });

        let app_response = self.http_client
            .post(format!("{}/management/v1/projects/{project_id}/apps/oidc", self.base_url))
            .header("Authorization", &auth_header)
            .json(&app_body)
            .send()
            .await?;

        if !app_response.status().is_success() {
            let error_text = app_response.text().await.unwrap_or_default();
            return Err(anyhow::anyhow!("Failed to create OIDC app: {error_text}"));
        }

        let app_data: serde_json::Value = app_response.json().await
            .map_err(|e| anyhow::anyhow!("Failed to parse OIDC app response: {e}"))?;

        info!("OIDC app creation response: {}", serde_json::to_string_pretty(&app_data).unwrap_or_default());

        // The response contains clientId and clientSecret directly
        let client_id = app_data
            .get("clientId")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("No clientId in OIDC app response: {app_data}"))?
            .to_string();

        let client_secret = app_data
            .get("clientSecret")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_string();

        if client_secret.is_empty() {
            warn!("No clientSecret returned — app may use PKCE only");
        }

        info!("Retrieved OAuth client credentials (client_id: {client_id})");
        Ok((client_id, client_secret))
    }

    fn get_auth_header(&self) -> Result<String> {
        match &self.pat_token {
            Some(token) => Ok(format!("Bearer {token}")),
            None => Err(anyhow::anyhow!(
                "No PAT token available. Cannot authenticate with Zitadel API."
            )),
        }
    }

    fn save_config(&self, config: &DirectoryConfig) -> Result<()> {
        if let Some(parent) = self.config_path.parent() {
            std::fs::create_dir_all(parent)?;
        }

        let content = serde_json::to_string_pretty(config)?;
        std::fs::write(&self.config_path, content)?;

        info!("Saved Directory config to: {}", self.config_path.display());

        println!();
        println!("╔════════════════════════════════════════════════════════════╗");
        println!("║     ZITADEL OAUTH CLIENT CONFIGURED                       ║");
        println!("╠════════════════════════════════════════════════════════════╣");
        println!("║  Project ID:    {:<43}║", config.project_id);
        println!("║  Client ID:     {:<43}║", config.client_id);
        println!("║  Redirect URI:  {:<43}║", config.redirect_uri);
        println!("║  Config saved:  {:<43}║", self.config_path.display().to_string().chars().take(43).collect::<String>());
        println!("╚════════════════════════════════════════════════════════════╝");
        println!();

        Ok(())
    }
}

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

    #[test]
    fn test_directory_config_serialization() {
        let config = DirectoryConfig {
            base_url: "http://localhost:8300".to_string(),
            issuer_url: "http://localhost:8300".to_string(),
            issuer: "http://localhost:8300".to_string(),
            client_id: "test_client".to_string(),
            client_secret: "test_secret".to_string(),
            redirect_uri: "http://localhost:3000/callback".to_string(),
            project_id: "12345".to_string(),
            api_url: "http://localhost:8300".to_string(),
            service_account_key: None,
        };

        let json = serde_json::to_string(&config).unwrap();
        assert!(json.contains("test_client"));
        assert!(json.contains("test_secret"));
    }
}
