//! Deploy commands for Synor Hosting.
//!
//! Handles deploying web applications to Synor's decentralized hosting.

use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};

use crate::output::{self, OutputFormat};

/// Deploy configuration from synor.json
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SynorJson {
    /// Name of the deployment
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,

    /// Build configuration
    #[serde(skip_serializing_if = "Option::is_none")]
    pub build: Option<BuildConfig>,

    /// Routes configuration
    #[serde(skip_serializing_if = "Option::is_none")]
    pub routes: Option<RoutesConfig>,

    /// Headers configuration
    #[serde(skip_serializing_if = "Option::is_none")]
    pub headers: Option<Vec<HeaderRule>>,

    /// Redirects configuration
    #[serde(skip_serializing_if = "Option::is_none")]
    pub redirects: Option<Vec<RedirectRule>>,

    /// Custom error pages
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error_pages: Option<HashMap<u16, String>>,
}

/// Build configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildConfig {
    /// Build command
    #[serde(skip_serializing_if = "Option::is_none")]
    pub command: Option<String>,

    /// Output directory (default: "dist" or "build")
    #[serde(skip_serializing_if = "Option::is_none")]
    pub output: Option<String>,

    /// Install command (default: "npm install" or "pnpm install")
    #[serde(skip_serializing_if = "Option::is_none")]
    pub install: Option<String>,
}

/// Routes configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RoutesConfig {
    /// Enable SPA mode (fallback to index.html)
    #[serde(default)]
    pub spa: bool,

    /// Custom cleanUrls (remove .html extensions)
    #[serde(default)]
    pub clean_urls: bool,
}

/// Header rule
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeaderRule {
    pub source: String,
    pub headers: Vec<HeaderKeyValue>,
}

/// Single header key-value pair
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeaderKeyValue {
    pub key: String,
    pub value: String,
}

/// Redirect rule
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RedirectRule {
    pub source: String,
    pub destination: String,
    #[serde(default = "default_redirect_status")]
    pub status: u16,
}

fn default_redirect_status() -> u16 {
    308
}

/// File to be uploaded
#[derive(Debug, Clone, Serialize)]
pub struct DeployFile {
    /// Relative path from output directory
    pub path: String,
    /// File size in bytes
    pub size: u64,
    /// Content type
    pub content_type: String,
}

/// Deployment result
#[derive(Debug, Clone, Serialize)]
pub struct DeployResult {
    /// Deployment name
    pub name: String,
    /// Content ID (CID) of the deployment
    pub cid: String,
    /// URL to access the deployment
    pub url: String,
    /// Number of files deployed
    pub file_count: usize,
    /// Total size in bytes
    pub total_size: u64,
}

impl SynorJson {
    /// Load from file path
    pub fn load(path: &Path) -> Result<Self> {
        let content = fs::read_to_string(path)
            .with_context(|| format!("Failed to read {}", path.display()))?;
        serde_json::from_str(&content)
            .with_context(|| format!("Failed to parse {}", path.display()))
    }

    /// Find synor.json in current directory or parent directories
    pub fn find(start: &Path) -> Option<PathBuf> {
        let mut current = start.to_path_buf();
        loop {
            let config_path = current.join("synor.json");
            if config_path.exists() {
                return Some(config_path);
            }
            if !current.pop() {
                return None;
            }
        }
    }

    /// Get the output directory
    pub fn output_dir(&self) -> &str {
        self.build
            .as_ref()
            .and_then(|b| b.output.as_deref())
            .unwrap_or("dist")
    }
}

/// Deploy a project to Synor Hosting.
pub async fn deploy(
    name: Option<String>,
    output_dir: Option<PathBuf>,
    hosting_gateway: &str,
    skip_build: bool,
    format: OutputFormat,
) -> Result<()> {
    let cwd = std::env::current_dir()?;

    // Find and load synor.json
    let config_path = SynorJson::find(&cwd);
    let config = if let Some(path) = &config_path {
        output::print_info(&format!("Found configuration: {}", path.display()));
        SynorJson::load(path)?
    } else {
        output::print_warning("No synor.json found, using defaults");
        SynorJson::default()
    };

    // Determine deployment name
    let deploy_name = name
        .or_else(|| config.name.clone())
        .or_else(|| {
            cwd.file_name()
                .and_then(|n| n.to_str())
                .map(|s| s.to_string())
        })
        .ok_or_else(|| anyhow!("Could not determine deployment name"))?;

    // Validate name
    validate_name(&deploy_name)?;

    output::print_header(&format!("Deploying: {}", deploy_name));

    // Run build if configured and not skipped
    if !skip_build {
        if let Some(build) = &config.build {
            run_build(build, &cwd)?;
        }
    }

    // Determine output directory
    let output_path = output_dir
        .unwrap_or_else(|| cwd.join(config.output_dir()));

    if !output_path.exists() {
        return Err(anyhow!(
            "Output directory not found: {}\nRun your build command or specify --output",
            output_path.display()
        ));
    }

    // Collect files
    let spinner = output::create_spinner("Collecting files...");
    let files = collect_files(&output_path)?;
    spinner.finish_and_clear();

    if files.is_empty() {
        return Err(anyhow!("No files found in {}", output_path.display()));
    }

    let total_size: u64 = files.iter().map(|f| f.size).sum();
    output::print_info(&format!(
        "Found {} files ({} total)",
        files.len(),
        output::format_size(total_size)
    ));

    // Upload files to storage
    let spinner = output::create_spinner("Uploading to Synor Storage...");
    let cid = upload_files(&output_path, &files, hosting_gateway).await?;
    spinner.finish_and_clear();

    output::print_success(&format!("Uploaded with CID: {}", cid));

    // Register deployment
    let spinner = output::create_spinner("Registering deployment...");
    let url = register_deployment(&deploy_name, &cid, hosting_gateway).await?;
    spinner.finish_and_clear();

    // Build result
    let result = DeployResult {
        name: deploy_name.clone(),
        cid: cid.clone(),
        url: url.clone(),
        file_count: files.len(),
        total_size,
    };

    // Output result
    match format {
        OutputFormat::Json => {
            println!("{}", serde_json::to_string_pretty(&result)?);
        }
        OutputFormat::Text => {
            output::print_success("Deployment complete!");
            println!();
            output::print_kv("Name", &result.name);
            output::print_kv("CID", &result.cid);
            output::print_kv("URL", &result.url);
            output::print_kv("Files", &result.file_count.to_string());
            output::print_kv("Size", &output::format_size(result.total_size));
        }
    }

    Ok(())
}

/// Validate deployment name.
fn validate_name(name: &str) -> Result<()> {
    if name.is_empty() {
        return Err(anyhow!("Name cannot be empty"));
    }
    if name.len() > 63 {
        return Err(anyhow!("Name must be 63 characters or less"));
    }
    if !name.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') {
        return Err(anyhow!(
            "Name must contain only lowercase letters, numbers, and hyphens"
        ));
    }
    if name.starts_with('-') || name.ends_with('-') {
        return Err(anyhow!("Name cannot start or end with a hyphen"));
    }

    // Reserved names
    const RESERVED: &[&str] = &[
        "www", "api", "app", "admin", "mail", "ftp", "ssh", "cdn",
        "storage", "gateway", "hosting", "node", "synor",
    ];
    if RESERVED.contains(&name) {
        return Err(anyhow!("Name '{}' is reserved", name));
    }

    Ok(())
}

/// Run the build command.
fn run_build(build: &BuildConfig, cwd: &Path) -> Result<()> {
    // Run install command if specified
    if let Some(install) = &build.install {
        output::print_info(&format!("Running: {}", install));
        let status = Command::new("sh")
            .arg("-c")
            .arg(install)
            .current_dir(cwd)
            .status()
            .context("Failed to run install command")?;

        if !status.success() {
            return Err(anyhow!("Install command failed with status: {}", status));
        }
    }

    // Run build command
    if let Some(command) = &build.command {
        output::print_info(&format!("Running: {}", command));
        let status = Command::new("sh")
            .arg("-c")
            .arg(command)
            .current_dir(cwd)
            .status()
            .context("Failed to run build command")?;

        if !status.success() {
            return Err(anyhow!("Build command failed with status: {}", status));
        }
        output::print_success("Build complete");
    }

    Ok(())
}

/// Collect all files from the output directory.
fn collect_files(dir: &Path) -> Result<Vec<DeployFile>> {
    let mut files = Vec::new();
    collect_files_recursive(dir, dir, &mut files)?;
    Ok(files)
}

fn collect_files_recursive(base: &Path, dir: &Path, files: &mut Vec<DeployFile>) -> Result<()> {
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();

        if path.is_dir() {
            collect_files_recursive(base, &path, files)?;
        } else {
            let relative = path
                .strip_prefix(base)
                .map(|p| p.to_string_lossy().to_string())
                .unwrap_or_default();

            let metadata = fs::metadata(&path)?;
            let content_type = guess_content_type(&path);

            files.push(DeployFile {
                path: relative,
                size: metadata.len(),
                content_type,
            });
        }
    }
    Ok(())
}

/// Guess content type from file extension.
fn guess_content_type(path: &Path) -> String {
    let ext = path
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("")
        .to_lowercase();

    match ext.as_str() {
        "html" | "htm" => "text/html",
        "css" => "text/css",
        "js" | "mjs" => "application/javascript",
        "json" => "application/json",
        "xml" => "application/xml",
        "svg" => "image/svg+xml",
        "png" => "image/png",
        "jpg" | "jpeg" => "image/jpeg",
        "gif" => "image/gif",
        "webp" => "image/webp",
        "avif" => "image/avif",
        "ico" => "image/x-icon",
        "woff" => "font/woff",
        "woff2" => "font/woff2",
        "ttf" => "font/ttf",
        "otf" => "font/otf",
        "eot" => "application/vnd.ms-fontobject",
        "pdf" => "application/pdf",
        "wasm" => "application/wasm",
        "txt" => "text/plain",
        "md" => "text/markdown",
        "map" => "application/json",
        _ => "application/octet-stream",
    }
    .to_string()
}

/// Upload files to Synor Storage.
async fn upload_files(
    base_dir: &Path,
    files: &[DeployFile],
    gateway_url: &str,
) -> Result<String> {
    let client = reqwest::Client::new();

    // Create a multipart form with all files
    let mut form = reqwest::multipart::Form::new();

    for file in files {
        let file_path = base_dir.join(&file.path);
        let content = fs::read(&file_path)
            .with_context(|| format!("Failed to read {}", file_path.display()))?;

        let part = reqwest::multipart::Part::bytes(content)
            .file_name(file.path.clone())
            .mime_str(&file.content_type)?;

        form = form.part(file.path.clone(), part);
    }

    // Upload to storage gateway
    let upload_url = format!("{}/api/upload", gateway_url.trim_end_matches('/'));

    let response = client
        .post(&upload_url)
        .multipart(form)
        .send()
        .await
        .context("Failed to upload files")?;

    if !response.status().is_success() {
        let status = response.status();
        let body = response.text().await.unwrap_or_default();
        return Err(anyhow!("Upload failed: {} - {}", status, body));
    }

    #[derive(Deserialize)]
    struct UploadResponse {
        cid: String,
    }

    let result: UploadResponse = response.json().await?;
    Ok(result.cid)
}

/// Register the deployment with the hosting gateway.
async fn register_deployment(
    name: &str,
    cid: &str,
    gateway_url: &str,
) -> Result<String> {
    let client = reqwest::Client::new();

    #[derive(Serialize)]
    struct RegisterRequest<'a> {
        name: &'a str,
        cid: &'a str,
    }

    let register_url = format!("{}/api/register", gateway_url.trim_end_matches('/'));

    let response = client
        .post(&register_url)
        .json(&RegisterRequest { name, cid })
        .send()
        .await
        .context("Failed to register deployment")?;

    if !response.status().is_success() {
        let status = response.status();
        let body = response.text().await.unwrap_or_default();
        return Err(anyhow!("Registration failed: {} - {}", status, body));
    }

    #[derive(Deserialize)]
    struct RegisterResponse {
        url: String,
    }

    let result: RegisterResponse = response.json().await?;
    Ok(result.url)
}

/// Initialize a new synor.json in the current directory.
pub fn init(
    name: Option<String>,
    spa: bool,
    output: Option<String>,
    format: OutputFormat,
) -> Result<()> {
    let cwd = std::env::current_dir()?;
    let config_path = cwd.join("synor.json");

    if config_path.exists() {
        return Err(anyhow!("synor.json already exists"));
    }

    // Infer name from directory
    let name = name.or_else(|| {
        cwd.file_name()
            .and_then(|n| n.to_str())
            .map(|s| s.to_lowercase().replace(' ', "-"))
    });

    // Detect framework and suggest build config
    let build_config = detect_framework(&cwd, output);

    let config = SynorJson {
        name,
        build: Some(build_config),
        routes: if spa {
            Some(RoutesConfig {
                spa: true,
                clean_urls: true,
            })
        } else {
            None
        },
        ..Default::default()
    };

    let json = serde_json::to_string_pretty(&config)?;
    fs::write(&config_path, &json)?;

    match format {
        OutputFormat::Json => {
            println!("{}", json);
        }
        OutputFormat::Text => {
            output::print_success("Created synor.json");
            println!("{}", json);
        }
    }

    Ok(())
}

/// Detect framework and return suggested build config.
fn detect_framework(cwd: &Path, output: Option<String>) -> BuildConfig {
    // Check for common framework configs
    if cwd.join("next.config.js").exists() || cwd.join("next.config.mjs").exists() {
        return BuildConfig {
            command: Some("npm run build".to_string()),
            output: output.or(Some("out".to_string())),
            install: Some("npm install".to_string()),
        };
    }

    if cwd.join("vite.config.js").exists() || cwd.join("vite.config.ts").exists() {
        return BuildConfig {
            command: Some("npm run build".to_string()),
            output: output.or(Some("dist".to_string())),
            install: Some("npm install".to_string()),
        };
    }

    if cwd.join("astro.config.mjs").exists() {
        return BuildConfig {
            command: Some("npm run build".to_string()),
            output: output.or(Some("dist".to_string())),
            install: Some("npm install".to_string()),
        };
    }

    if cwd.join("angular.json").exists() {
        return BuildConfig {
            command: Some("npm run build".to_string()),
            output: output.or(Some("dist".to_string())),
            install: Some("npm install".to_string()),
        };
    }

    if cwd.join("svelte.config.js").exists() {
        return BuildConfig {
            command: Some("npm run build".to_string()),
            output: output.or(Some("build".to_string())),
            install: Some("npm install".to_string()),
        };
    }

    // Default config
    BuildConfig {
        command: None,
        output: output.or(Some("dist".to_string())),
        install: None,
    }
}

/// List deployments.
pub async fn list(gateway_url: &str, format: OutputFormat) -> Result<()> {
    let client = reqwest::Client::new();
    let list_url = format!("{}/api/deployments", gateway_url.trim_end_matches('/'));

    let response = client
        .get(&list_url)
        .send()
        .await
        .context("Failed to list deployments")?;

    if !response.status().is_success() {
        let status = response.status();
        let body = response.text().await.unwrap_or_default();
        return Err(anyhow!("Failed to list deployments: {} - {}", status, body));
    }

    #[derive(Deserialize, Serialize)]
    struct Deployment {
        name: String,
        cid: String,
        url: String,
        created_at: String,
    }

    let deployments: Vec<Deployment> = response.json().await?;

    match format {
        OutputFormat::Json => {
            println!("{}", serde_json::to_string_pretty(&deployments)?);
        }
        OutputFormat::Text => {
            if deployments.is_empty() {
                output::print_info("No deployments found");
                return Ok(());
            }

            output::print_header("Deployments");
            let headers = vec!["Name", "CID", "URL", "Created"];
            let rows: Vec<Vec<String>> = deployments
                .iter()
                .map(|d| {
                    vec![
                        d.name.clone(),
                        output::format_hash(&d.cid),
                        d.url.clone(),
                        d.created_at.clone(),
                    ]
                })
                .collect();
            output::print_table(headers, rows);
        }
    }

    Ok(())
}

/// Delete a deployment.
pub async fn delete(name: &str, gateway_url: &str, format: OutputFormat) -> Result<()> {
    let client = reqwest::Client::new();
    let delete_url = format!(
        "{}/api/deployments/{}",
        gateway_url.trim_end_matches('/'),
        name
    );

    let response = client
        .delete(&delete_url)
        .send()
        .await
        .context("Failed to delete deployment")?;

    if !response.status().is_success() {
        let status = response.status();
        let body = response.text().await.unwrap_or_default();
        return Err(anyhow!("Failed to delete deployment: {} - {}", status, body));
    }

    match format {
        OutputFormat::Json => {
            println!(
                "{}",
                serde_json::to_string_pretty(&serde_json::json!({
                    "deleted": name
                }))?
            );
        }
        OutputFormat::Text => {
            output::print_success(&format!("Deleted deployment: {}", name));
        }
    }

    Ok(())
}

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

    #[test]
    fn test_validate_name_valid() {
        assert!(validate_name("myapp").is_ok());
        assert!(validate_name("my-app").is_ok());
        assert!(validate_name("my-app-123").is_ok());
        assert!(validate_name("a1b2c3").is_ok());
    }

    #[test]
    fn test_validate_name_invalid() {
        assert!(validate_name("").is_err());
        assert!(validate_name("MyApp").is_err()); // uppercase
        assert!(validate_name("my_app").is_err()); // underscore
        assert!(validate_name("-myapp").is_err()); // starts with hyphen
        assert!(validate_name("myapp-").is_err()); // ends with hyphen
        assert!(validate_name("www").is_err()); // reserved
        assert!(validate_name("api").is_err()); // reserved
    }

    #[test]
    fn test_guess_content_type() {
        assert_eq!(
            guess_content_type(Path::new("index.html")),
            "text/html"
        );
        assert_eq!(
            guess_content_type(Path::new("style.css")),
            "text/css"
        );
        assert_eq!(
            guess_content_type(Path::new("app.js")),
            "application/javascript"
        );
        assert_eq!(
            guess_content_type(Path::new("image.png")),
            "image/png"
        );
        assert_eq!(
            guess_content_type(Path::new("data.wasm")),
            "application/wasm"
        );
        assert_eq!(
            guess_content_type(Path::new("unknown.xyz")),
            "application/octet-stream"
        );
    }

    #[test]
    fn test_synor_json_output_dir() {
        let config = SynorJson::default();
        assert_eq!(config.output_dir(), "dist");

        let config = SynorJson {
            build: Some(BuildConfig {
                output: Some("build".to_string()),
                command: None,
                install: None,
            }),
            ..Default::default()
        };
        assert_eq!(config.output_dir(), "build");
    }
}
