//! Agent deployment to Docker containers and cloud providers.
//!
//! Packages agent DSL, Cedar policies, ToolClad manifests, and project config
//! into Docker images. Supports local Docker, Cloud Run, and AWS targets.

#![allow(dead_code)] // DeployTarget, DeployResult, artifact_files used by future cloud targets

use anyhow::{anyhow, Result};
use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Stdio;

/// Base image for agent containers. Uses the pre-built symbi image.
const DEFAULT_BASE_IMAGE: &str = "ghcr.io/thirdkeyai/symbiont:latest";

/// Deployment target.
#[derive(Debug, Clone)]
pub enum DeployTarget {
    Local { port: u16 },
}

/// Result of a deployment.
#[derive(Debug)]
pub struct DeployResult {
    pub target: String,
    pub image_name: String,
    pub container_id: Option<String>,
    pub url: Option<String>,
    pub message: String,
}

/// Information about an agent to deploy.
pub struct AgentBundle {
    pub name: String,
    pub dsl_path: PathBuf,
    pub project_dir: PathBuf,
}

impl AgentBundle {
    /// Discover an agent bundle from the project directory.
    pub fn discover(agent_name: &str) -> Result<Self> {
        let cwd = std::env::current_dir()?;

        // Look for the agent DSL file
        let dsl_path = cwd.join(format!("agents/{}.dsl", agent_name));
        if !dsl_path.exists() {
            return Err(anyhow!(
                "Agent definition not found at {}. Run /spawn first.",
                dsl_path.display()
            ));
        }

        // Verify project is initialized
        if !cwd.join("symbiont.toml").exists() {
            return Err(anyhow!(
                "No symbiont.toml found. Run /init to initialize the project."
            ));
        }

        Ok(Self {
            name: agent_name.to_string(),
            dsl_path,
            project_dir: cwd,
        })
    }

    /// List files to include in the Docker image.
    fn artifact_files(&self) -> Vec<PathBuf> {
        let mut files = vec![
            self.project_dir.join("symbiont.toml"),
            self.dsl_path.clone(),
        ];

        // Include all policies
        let policies_dir = self.project_dir.join("policies");
        if policies_dir.exists() {
            if let Ok(entries) = std::fs::read_dir(&policies_dir) {
                for entry in entries.flatten() {
                    let path = entry.path();
                    if path.extension().is_some_and(|e| e == "cedar") {
                        files.push(path);
                    }
                }
            }
        }

        // Include all tools
        let tools_dir = self.project_dir.join("tools");
        if tools_dir.exists() {
            if let Ok(entries) = std::fs::read_dir(&tools_dir) {
                for entry in entries.flatten() {
                    let path = entry.path();
                    if path.extension().is_some_and(|e| e == "toml") {
                        files.push(path);
                    }
                }
            }
        }

        // Include constraints if present
        let constraints = self.project_dir.join(".symbi/constraints.toml");
        if constraints.exists() {
            files.push(constraints);
        }

        files
    }
}

/// Generate a Dockerfile for an agent deployment.
fn generate_dockerfile(bundle: &AgentBundle, base_image: &str) -> String {
    let mut dockerfile = format!(
        "# Auto-generated by symbi shell /deploy\n\
         # Agent: {}\n\
         FROM {}\n\n",
        bundle.name, base_image
    );

    // Copy project config
    dockerfile.push_str("# Project configuration\n");
    dockerfile.push_str("COPY symbiont.toml /var/lib/symbi/symbiont.toml\n");

    // Copy agent DSL
    dockerfile.push_str("\n# Agent definition\n");
    dockerfile.push_str("COPY agents/ /var/lib/symbi/agents/\n");

    // Copy policies
    dockerfile.push_str("\n# Cedar policies\n");
    dockerfile.push_str("COPY policies/ /var/lib/symbi/policies/\n");

    // Copy tools if present
    let tools_dir = bundle.project_dir.join("tools");
    if tools_dir.exists()
        && std::fs::read_dir(&tools_dir)
            .map(|mut d| d.next().is_some())
            .unwrap_or(false)
    {
        dockerfile.push_str("\n# ToolClad manifests\n");
        dockerfile.push_str("COPY tools/ /var/lib/symbi/tools/\n");
    }

    // Copy constraints if present
    let constraints = bundle.project_dir.join(".symbi/constraints.toml");
    if constraints.exists() {
        dockerfile.push_str("\n# Validation constraints\n");
        dockerfile
            .push_str("COPY .symbi/constraints.toml /var/lib/symbi/.symbi/constraints.toml\n");
    }

    // Entrypoint: run symbi up with this specific agent
    dockerfile.push_str(&format!(
        "\n# Run the agent\n\
         ENV SYMBIONT_AGENT={}\n\
         CMD [\"up\", \"--http-bind\", \"0.0.0.0\"]\n",
        bundle.name
    ));

    dockerfile
}

/// Build a Docker image for an agent.
pub async fn build_image(bundle: &AgentBundle, base_image: Option<&str>) -> Result<String> {
    let base = base_image.unwrap_or(DEFAULT_BASE_IMAGE);
    let image_name = format!("symbi-agent-{}", bundle.name);
    let dockerfile_content = generate_dockerfile(bundle, base);

    // Write Dockerfile to a temp location in the project
    let dockerfile_path = bundle.project_dir.join(".symbi/Dockerfile.agent");
    std::fs::create_dir_all(bundle.project_dir.join(".symbi"))?;
    std::fs::write(&dockerfile_path, &dockerfile_content)?;

    tracing::info!("Building Docker image: {}", image_name);

    let output = tokio::process::Command::new("docker")
        .arg("build")
        .arg("-t")
        .arg(&image_name)
        .arg("-f")
        .arg(&dockerfile_path)
        .arg(&bundle.project_dir)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await
        .map_err(|e| anyhow!("Failed to run docker build: {}. Is Docker running?", e))?;

    // Clean up Dockerfile
    let _ = std::fs::remove_file(&dockerfile_path);

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(anyhow!("Docker build failed:\n{}", stderr));
    }

    Ok(image_name)
}

/// Run a Docker container from a built agent image.
pub async fn run_container(
    image_name: &str,
    port: u16,
    env_vars: &HashMap<String, String>,
) -> Result<String> {
    let container_name = format!("symbi-{}", image_name.replace("symbi-agent-", ""));

    // Stop existing container with same name (if any)
    let _ = tokio::process::Command::new("docker")
        .args(["rm", "-f", &container_name])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .await;

    let mut cmd = tokio::process::Command::new("docker");
    cmd.arg("run")
        .arg("-d") // detached
        .arg("--name")
        .arg(&container_name)
        .arg("-p")
        .arg(format!("{}:8080", port))
        .arg("-p")
        .arg(format!("{}:8081", port + 1));

    // Inject environment variables (secrets)
    for (key, value) in env_vars {
        cmd.arg("-e").arg(format!("{}={}", key, value));
    }

    // Resource limits
    cmd.arg("--memory").arg("512m");
    cmd.arg("--cpus").arg("1.0");

    // Restart policy
    cmd.arg("--restart").arg("unless-stopped");

    cmd.arg(image_name);

    let output = cmd
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await
        .map_err(|e| anyhow!("Failed to start container: {}", e))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(anyhow!("Failed to start container:\n{}", stderr));
    }

    let container_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
    Ok(container_id)
}

/// List running symbi agent containers.
pub async fn list_deployments() -> Result<Vec<(String, String, String)>> {
    let output = tokio::process::Command::new("docker")
        .args([
            "ps",
            "--filter",
            "name=symbi-",
            "--format",
            "{{.Names}}\t{{.Status}}\t{{.Ports}}",
        ])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await
        .map_err(|e| anyhow!("Failed to list containers: {}", e))?;

    let stdout = String::from_utf8_lossy(&output.stdout);
    let deployments: Vec<(String, String, String)> = stdout
        .lines()
        .filter(|l| !l.is_empty())
        .map(|line| {
            let parts: Vec<&str> = line.split('\t').collect();
            (
                parts.first().unwrap_or(&"").to_string(),
                parts.get(1).unwrap_or(&"").to_string(),
                parts.get(2).unwrap_or(&"").to_string(),
            )
        })
        .collect();

    Ok(deployments)
}

/// Stop and remove a deployed agent container.
pub async fn stop_deployment(agent_name: &str) -> Result<String> {
    let container_name = format!("symbi-{}", agent_name);

    let output = tokio::process::Command::new("docker")
        .args(["rm", "-f", &container_name])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await
        .map_err(|e| anyhow!("Failed to stop container: {}", e))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(anyhow!("Failed to stop container:\n{}", stderr));
    }

    Ok(container_name)
}

/// Get logs from a deployed agent container.
pub async fn get_logs(agent_name: &str, tail: usize) -> Result<String> {
    let container_name = format!("symbi-{}", agent_name);

    let output = tokio::process::Command::new("docker")
        .args(["logs", "--tail", &tail.to_string(), &container_name])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await
        .map_err(|e| anyhow!("Failed to get logs: {}", e))?;

    let mut logs = String::from_utf8_lossy(&output.stdout).to_string();
    let stderr = String::from_utf8_lossy(&output.stderr);
    if !stderr.is_empty() {
        logs.push_str(&stderr);
    }

    Ok(logs)
}

// ══════════════════════════════════════════════════════════════════
// Google Cloud Run deployment
// ══════════════════════════════════════════════════════════════════

/// Cloud Run deployment options.
pub struct CloudRunOptions<'a> {
    pub project: &'a str,
    pub region: &'a str,
    pub service_account: Option<&'a str>,
    pub allow_unauthenticated: bool,
    pub min_instances: u32,
    pub max_instances: u32,
    pub memory: &'a str, // e.g. "512Mi"
    pub cpu: &'a str,    // e.g. "1"
}

impl<'a> CloudRunOptions<'a> {
    pub fn new(project: &'a str, region: &'a str) -> Self {
        Self {
            project,
            region,
            service_account: None,
            allow_unauthenticated: false,
            min_instances: 0,
            max_instances: 1,
            memory: "512Mi",
            cpu: "1",
        }
    }
}

/// Verify gcloud CLI is installed and authenticated.
pub async fn check_gcloud() -> Result<String> {
    let output = tokio::process::Command::new("gcloud")
        .args([
            "auth",
            "list",
            "--filter=status:ACTIVE",
            "--format=value(account)",
        ])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await
        .map_err(|_| {
            anyhow!("gcloud CLI not found. Install from https://cloud.google.com/sdk/docs/install")
        })?;

    if !output.status.success() {
        return Err(anyhow!(
            "gcloud authentication check failed. Run: gcloud auth login"
        ));
    }

    let account = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if account.is_empty() {
        return Err(anyhow!("No active gcloud account. Run: gcloud auth login"));
    }
    Ok(account)
}

/// Build the full Artifact Registry image URI.
fn artifact_registry_uri(region: &str, project: &str, agent_name: &str) -> String {
    format!(
        "{}-docker.pkg.dev/{}/symbiont/{}:latest",
        region, project, agent_name
    )
}

/// Ensure the Artifact Registry repository exists (creates if missing).
async fn ensure_artifact_registry(project: &str, region: &str) -> Result<()> {
    // Check if repo exists
    let check = tokio::process::Command::new("gcloud")
        .args([
            "artifacts",
            "repositories",
            "describe",
            "symbiont",
            "--project",
            project,
            "--location",
            region,
        ])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .await?;

    if check.success() {
        return Ok(());
    }

    tracing::info!("Creating Artifact Registry repository: symbiont");

    let output = tokio::process::Command::new("gcloud")
        .args([
            "artifacts",
            "repositories",
            "create",
            "symbiont",
            "--repository-format=docker",
            "--project",
            project,
            "--location",
            region,
            "--description=Symbiont agent images",
        ])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(anyhow!(
            "Failed to create Artifact Registry repository:\n{}",
            stderr
        ));
    }

    // Configure docker to use gcloud as credential helper
    let _ = tokio::process::Command::new("gcloud")
        .args([
            "auth",
            "configure-docker",
            &format!("{}-docker.pkg.dev", region),
            "--quiet",
        ])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .await;

    Ok(())
}

/// Tag and push the local image to Artifact Registry.
pub async fn push_to_artifact_registry(
    local_image: &str,
    project: &str,
    region: &str,
    agent_name: &str,
) -> Result<String> {
    ensure_artifact_registry(project, region).await?;

    let remote_uri = artifact_registry_uri(region, project, agent_name);

    // Tag
    let tag_out = tokio::process::Command::new("docker")
        .args(["tag", local_image, &remote_uri])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await?;

    if !tag_out.status.success() {
        let stderr = String::from_utf8_lossy(&tag_out.stderr);
        return Err(anyhow!("Failed to tag image:\n{}", stderr));
    }

    // Push
    tracing::info!("Pushing image to {}", remote_uri);
    let push_out = tokio::process::Command::new("docker")
        .args(["push", &remote_uri])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await?;

    if !push_out.status.success() {
        let stderr = String::from_utf8_lossy(&push_out.stderr);
        return Err(anyhow!("Failed to push image:\n{}", stderr));
    }

    Ok(remote_uri)
}

/// Upload secrets to Google Secret Manager (idempotent, creates or updates).
async fn upload_secrets_to_secret_manager(
    project: &str,
    agent_name: &str,
    secrets: &HashMap<String, String>,
) -> Result<Vec<(String, String)>> {
    let mut mappings = Vec::new();

    for (key, value) in secrets {
        // Secret Manager name: symbiont-<agent>-<key-lowercased-sanitized>
        let secret_name = format!(
            "symbiont-{}-{}",
            agent_name,
            key.to_lowercase().replace('_', "-")
        );

        // Check if secret exists
        let exists = tokio::process::Command::new("gcloud")
            .args(["secrets", "describe", &secret_name, "--project", project])
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .status()
            .await?
            .success();

        if !exists {
            // Create
            let create = tokio::process::Command::new("gcloud")
                .args([
                    "secrets",
                    "create",
                    &secret_name,
                    "--project",
                    project,
                    "--replication-policy=automatic",
                ])
                .stdout(Stdio::null())
                .stderr(Stdio::piped())
                .output()
                .await?;

            if !create.status.success() {
                let stderr = String::from_utf8_lossy(&create.stderr);
                return Err(anyhow!(
                    "Failed to create secret {}: {}",
                    secret_name,
                    stderr
                ));
            }
        }

        // Add a new version with the current value (always — secrets may have rotated)
        let mut child = tokio::process::Command::new("gcloud")
            .args([
                "secrets",
                "versions",
                "add",
                &secret_name,
                "--project",
                project,
                "--data-file=-",
            ])
            .stdin(Stdio::piped())
            .stdout(Stdio::null())
            .stderr(Stdio::piped())
            .spawn()?;

        if let Some(mut stdin) = child.stdin.take() {
            use tokio::io::AsyncWriteExt;
            stdin.write_all(value.as_bytes()).await?;
            drop(stdin); // Close stdin
        }

        let output = child.wait_with_output().await?;
        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(anyhow!(
                "Failed to add version to secret {}: {}",
                secret_name,
                stderr
            ));
        }

        mappings.push((key.clone(), format!("{}:latest", secret_name)));
    }

    Ok(mappings)
}

/// Deploy an agent to Cloud Run.
pub async fn deploy_cloudrun(
    bundle: &AgentBundle,
    options: &CloudRunOptions<'_>,
    secrets: &HashMap<String, String>,
) -> Result<String> {
    // 1. Verify gcloud
    let account = check_gcloud().await?;
    tracing::info!("Deploying as: {}", account);

    // 2. Build image locally
    let local_image = build_image(bundle, None).await?;

    // 3. Push to Artifact Registry
    let image_uri =
        push_to_artifact_registry(&local_image, options.project, options.region, &bundle.name)
            .await?;

    // 4. Upload secrets to Secret Manager
    let secret_mappings =
        upload_secrets_to_secret_manager(options.project, &bundle.name, secrets).await?;

    // 5. Build set-secrets flag for gcloud run deploy
    let set_secrets = if secret_mappings.is_empty() {
        String::new()
    } else {
        secret_mappings
            .iter()
            .map(|(k, v)| format!("{}={}", k, v))
            .collect::<Vec<_>>()
            .join(",")
    };

    // 6. Deploy to Cloud Run
    let service_name = format!("symbiont-{}", bundle.name);

    let mut cmd = tokio::process::Command::new("gcloud");
    cmd.args([
        "run",
        "deploy",
        &service_name,
        "--image",
        &image_uri,
        "--project",
        options.project,
        "--region",
        options.region,
        "--memory",
        options.memory,
        "--cpu",
        options.cpu,
        "--min-instances",
        &options.min_instances.to_string(),
        "--max-instances",
        &options.max_instances.to_string(),
        "--port",
        "8081",
        "--quiet",
    ]);

    if options.allow_unauthenticated {
        cmd.arg("--allow-unauthenticated");
    } else {
        cmd.arg("--no-allow-unauthenticated");
    }

    if let Some(sa) = options.service_account {
        cmd.args(["--service-account", sa]);
    }

    if !set_secrets.is_empty() {
        cmd.args(["--set-secrets", &set_secrets]);
    }

    let output = cmd
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(anyhow!("Cloud Run deploy failed:\n{}", stderr));
    }

    // 7. Get the service URL
    let url_output = tokio::process::Command::new("gcloud")
        .args([
            "run",
            "services",
            "describe",
            &service_name,
            "--project",
            options.project,
            "--region",
            options.region,
            "--format=value(status.url)",
        ])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await?;

    let url = String::from_utf8_lossy(&url_output.stdout)
        .trim()
        .to_string();
    if url.is_empty() {
        return Err(anyhow!(
            "Deploy succeeded but failed to retrieve service URL"
        ));
    }

    Ok(url)
}

/// List Cloud Run services for the given project/region.
pub async fn list_cloudrun_services(project: &str, region: &str) -> Result<Vec<(String, String)>> {
    let output = tokio::process::Command::new("gcloud")
        .args([
            "run",
            "services",
            "list",
            "--project",
            project,
            "--region",
            region,
            "--filter=metadata.name:symbiont-",
            "--format=value(metadata.name,status.url)",
        ])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(anyhow!("Failed to list services:\n{}", stderr));
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let services: Vec<(String, String)> = stdout
        .lines()
        .filter(|l| !l.is_empty())
        .map(|line| {
            let parts: Vec<&str> = line.split('\t').collect();
            (
                parts.first().unwrap_or(&"").to_string(),
                parts.get(1).unwrap_or(&"").to_string(),
            )
        })
        .collect();

    Ok(services)
}

/// Delete a Cloud Run service.
pub async fn delete_cloudrun_service(agent_name: &str, project: &str, region: &str) -> Result<()> {
    let service_name = format!("symbiont-{}", agent_name);
    let output = tokio::process::Command::new("gcloud")
        .args([
            "run",
            "services",
            "delete",
            &service_name,
            "--project",
            project,
            "--region",
            region,
            "--quiet",
        ])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(anyhow!("Failed to delete service:\n{}", stderr));
    }
    Ok(())
}

// ══════════════════════════════════════════════════════════════════
// AWS App Runner deployment
// ══════════════════════════════════════════════════════════════════

/// AWS App Runner deployment options.
pub struct AwsOptions<'a> {
    pub region: &'a str,
    pub instance_role_arn: Option<&'a str>,
    pub access_role_arn: Option<&'a str>,
    pub cpu: &'a str,    // "0.25 vCPU", "0.5 vCPU", "1 vCPU", "2 vCPU", "4 vCPU"
    pub memory: &'a str, // "0.5 GB", "1 GB", "2 GB", "3 GB", "4 GB", "6 GB", "8 GB", "10 GB", "12 GB"
}

impl<'a> AwsOptions<'a> {
    pub fn new(region: &'a str) -> Self {
        Self {
            region,
            instance_role_arn: None,
            access_role_arn: None,
            cpu: "1 vCPU",
            memory: "2 GB",
        }
    }
}

/// Verify AWS CLI is installed and get the current account ID.
pub async fn check_aws(region: &str) -> Result<String> {
    let output = tokio::process::Command::new("aws")
        .args([
            "sts",
            "get-caller-identity",
            "--query",
            "Account",
            "--output",
            "text",
            "--region",
            region,
        ])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await
        .map_err(|_| anyhow!("AWS CLI not found. Install from https://aws.amazon.com/cli/"))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(anyhow!(
            "AWS authentication check failed:\n{}\nRun: aws configure",
            stderr
        ));
    }

    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}

/// Ensure ECR repository exists (creates if missing).
async fn ensure_ecr_repository(region: &str, repo_name: &str) -> Result<()> {
    let check = tokio::process::Command::new("aws")
        .args([
            "ecr",
            "describe-repositories",
            "--repository-names",
            repo_name,
            "--region",
            region,
        ])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .await?;

    if check.success() {
        return Ok(());
    }

    tracing::info!("Creating ECR repository: {}", repo_name);

    let output = tokio::process::Command::new("aws")
        .args([
            "ecr",
            "create-repository",
            "--repository-name",
            repo_name,
            "--region",
            region,
        ])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(anyhow!("Failed to create ECR repository:\n{}", stderr));
    }

    Ok(())
}

/// Push image to ECR.
pub async fn push_to_ecr(
    local_image: &str,
    account_id: &str,
    region: &str,
    agent_name: &str,
) -> Result<String> {
    let repo_name = format!("symbiont/{}", agent_name);
    ensure_ecr_repository(region, &repo_name).await?;

    let registry = format!("{}.dkr.ecr.{}.amazonaws.com", account_id, region);
    let remote_uri = format!("{}/{}:latest", registry, repo_name);

    // Log in to ECR
    tracing::info!("Authenticating to ECR");
    let login_cmd = tokio::process::Command::new("sh")
        .arg("-c")
        .arg(format!(
            "aws ecr get-login-password --region {} | docker login --username AWS --password-stdin {}",
            region, registry
        ))
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await?;

    if !login_cmd.status.success() {
        let stderr = String::from_utf8_lossy(&login_cmd.stderr);
        return Err(anyhow!("ECR login failed:\n{}", stderr));
    }

    // Tag
    let tag_out = tokio::process::Command::new("docker")
        .args(["tag", local_image, &remote_uri])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await?;

    if !tag_out.status.success() {
        return Err(anyhow!(
            "Failed to tag image:\n{}",
            String::from_utf8_lossy(&tag_out.stderr)
        ));
    }

    // Push
    tracing::info!("Pushing image to {}", remote_uri);
    let push_out = tokio::process::Command::new("docker")
        .args(["push", &remote_uri])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await?;

    if !push_out.status.success() {
        return Err(anyhow!(
            "Failed to push image:\n{}",
            String::from_utf8_lossy(&push_out.stderr)
        ));
    }

    Ok(remote_uri)
}

/// Upload secrets to AWS Secrets Manager (create or update).
/// Returns a list of (env_var_name, secret_arn) tuples.
async fn upload_secrets_to_aws(
    region: &str,
    agent_name: &str,
    secrets: &HashMap<String, String>,
) -> Result<Vec<(String, String)>> {
    let mut mappings = Vec::new();

    for (key, value) in secrets {
        let secret_name = format!("symbiont/{}/{}", agent_name, key);

        // Try to update; if it fails because it doesn't exist, create it
        let update = tokio::process::Command::new("aws")
            .args([
                "secretsmanager",
                "put-secret-value",
                "--secret-id",
                &secret_name,
                "--secret-string",
                value,
                "--region",
                region,
            ])
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .status()
            .await?;

        let arn = if update.success() {
            // Get the ARN
            let describe = tokio::process::Command::new("aws")
                .args([
                    "secretsmanager",
                    "describe-secret",
                    "--secret-id",
                    &secret_name,
                    "--query",
                    "ARN",
                    "--output",
                    "text",
                    "--region",
                    region,
                ])
                .stdout(Stdio::piped())
                .stderr(Stdio::piped())
                .output()
                .await?;
            String::from_utf8_lossy(&describe.stdout).trim().to_string()
        } else {
            // Create
            let create = tokio::process::Command::new("aws")
                .args([
                    "secretsmanager",
                    "create-secret",
                    "--name",
                    &secret_name,
                    "--secret-string",
                    value,
                    "--region",
                    region,
                    "--query",
                    "ARN",
                    "--output",
                    "text",
                ])
                .stdout(Stdio::piped())
                .stderr(Stdio::piped())
                .output()
                .await?;

            if !create.status.success() {
                let stderr = String::from_utf8_lossy(&create.stderr);
                return Err(anyhow!(
                    "Failed to create secret {}: {}",
                    secret_name,
                    stderr
                ));
            }
            String::from_utf8_lossy(&create.stdout).trim().to_string()
        };

        mappings.push((key.clone(), arn));
    }

    Ok(mappings)
}

/// Deploy an agent to AWS App Runner.
pub async fn deploy_aws(
    bundle: &AgentBundle,
    options: &AwsOptions<'_>,
    secrets: &HashMap<String, String>,
) -> Result<String> {
    // 1. Verify AWS auth, get account ID
    let account_id = check_aws(options.region).await?;
    tracing::info!("Deploying to account: {} ({})", account_id, options.region);

    // 2. Build image
    let local_image = build_image(bundle, None).await?;

    // 3. Push to ECR
    let image_uri = push_to_ecr(&local_image, &account_id, options.region, &bundle.name).await?;

    // 4. Upload secrets to Secrets Manager
    let secret_mappings = upload_secrets_to_aws(options.region, &bundle.name, secrets).await?;

    // 5. Build source configuration JSON
    let service_name = format!("symbiont-{}", bundle.name);

    let runtime_env_secrets: Vec<serde_json::Value> = secret_mappings
        .iter()
        .map(|(k, arn)| serde_json::json!({ "Name": k, "Value": arn }))
        .collect();

    let mut image_config = serde_json::json!({
        "Port": "8081",
    });
    if !runtime_env_secrets.is_empty() {
        image_config["RuntimeEnvironmentSecrets"] = serde_json::json!(secret_mappings
            .iter()
            .map(|(k, arn)| (k.clone(), arn.clone()))
            .collect::<HashMap<_, _>>());
    }

    let source_config = serde_json::json!({
        "ImageRepository": {
            "ImageIdentifier": image_uri,
            "ImageRepositoryType": "ECR",
            "ImageConfiguration": image_config,
        },
        "AutoDeploymentsEnabled": false,
    });

    let instance_config = serde_json::json!({
        "Cpu": options.cpu,
        "Memory": options.memory,
    });

    // 6. Check if service exists (update vs create)
    let list_out = tokio::process::Command::new("aws")
        .args([
            "apprunner",
            "list-services",
            "--query",
            &format!(
                "ServiceSummaryList[?ServiceName=='{}'].ServiceArn",
                service_name
            ),
            "--output",
            "text",
            "--region",
            options.region,
        ])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await?;

    let existing_arn = String::from_utf8_lossy(&list_out.stdout).trim().to_string();

    let service_arn = if existing_arn.is_empty() {
        // Create new service
        let mut args = vec![
            "apprunner".to_string(),
            "create-service".to_string(),
            "--service-name".to_string(),
            service_name.clone(),
            "--source-configuration".to_string(),
            source_config.to_string(),
            "--instance-configuration".to_string(),
            instance_config.to_string(),
            "--region".to_string(),
            options.region.to_string(),
        ];
        if let Some(arn) = options.access_role_arn {
            // ECR access role (required for private ECR)
            let with_auth = serde_json::json!({
                "ImageRepository": source_config["ImageRepository"],
                "AutoDeploymentsEnabled": false,
                "AuthenticationConfiguration": { "AccessRoleArn": arn },
            });
            // Replace source-configuration value
            if let Some(pos) = args.iter().position(|a| a == "--source-configuration") {
                args[pos + 1] = with_auth.to_string();
            }
        }
        args.push("--query".to_string());
        args.push("Service.ServiceArn".to_string());
        args.push("--output".to_string());
        args.push("text".to_string());

        let create = tokio::process::Command::new("aws")
            .args(&args)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .output()
            .await?;

        if !create.status.success() {
            let stderr = String::from_utf8_lossy(&create.stderr);
            return Err(anyhow!("Failed to create App Runner service:\n{}", stderr));
        }
        String::from_utf8_lossy(&create.stdout).trim().to_string()
    } else {
        // Update existing service
        tracing::info!("Updating existing service: {}", existing_arn);
        let update = tokio::process::Command::new("aws")
            .args([
                "apprunner",
                "update-service",
                "--service-arn",
                &existing_arn,
                "--source-configuration",
                &source_config.to_string(),
                "--region",
                options.region,
            ])
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .output()
            .await?;

        if !update.status.success() {
            let stderr = String::from_utf8_lossy(&update.stderr);
            return Err(anyhow!("Failed to update App Runner service:\n{}", stderr));
        }
        existing_arn
    };

    // 7. Get service URL
    let describe = tokio::process::Command::new("aws")
        .args([
            "apprunner",
            "describe-service",
            "--service-arn",
            &service_arn,
            "--query",
            "Service.ServiceUrl",
            "--output",
            "text",
            "--region",
            options.region,
        ])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await?;

    let url = String::from_utf8_lossy(&describe.stdout).trim().to_string();
    if url.is_empty() {
        return Err(anyhow!(
            "Deploy succeeded but failed to retrieve service URL"
        ));
    }

    Ok(format!("https://{}", url))
}

/// List App Runner services in the region.
pub async fn list_aws_services(region: &str) -> Result<Vec<(String, String)>> {
    let output = tokio::process::Command::new("aws")
        .args([
            "apprunner",
            "list-services",
            "--query",
            "ServiceSummaryList[?starts_with(ServiceName, `symbiont-`)].[ServiceName, ServiceUrl]",
            "--output",
            "text",
            "--region",
            region,
        ])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(anyhow!("Failed to list services:\n{}", stderr));
    }

    let stdout = String::from_utf8_lossy(&output.stdout);
    let services: Vec<(String, String)> = stdout
        .lines()
        .filter(|l| !l.is_empty())
        .map(|line| {
            let parts: Vec<&str> = line.split('\t').collect();
            (
                parts.first().unwrap_or(&"").to_string(),
                parts.get(1).unwrap_or(&"").to_string(),
            )
        })
        .collect();

    Ok(services)
}

/// Delete an App Runner service.
pub async fn delete_aws_service(agent_name: &str, region: &str) -> Result<()> {
    let service_name = format!("symbiont-{}", agent_name);

    // Find the ARN
    let list = tokio::process::Command::new("aws")
        .args([
            "apprunner",
            "list-services",
            "--query",
            &format!(
                "ServiceSummaryList[?ServiceName=='{}'].ServiceArn",
                service_name
            ),
            "--output",
            "text",
            "--region",
            region,
        ])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await?;

    let arn = String::from_utf8_lossy(&list.stdout).trim().to_string();
    if arn.is_empty() {
        return Err(anyhow!("No App Runner service found for {}", agent_name));
    }

    let del = tokio::process::Command::new("aws")
        .args([
            "apprunner",
            "delete-service",
            "--service-arn",
            &arn,
            "--region",
            region,
        ])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .await?;

    if !del.status.success() {
        let stderr = String::from_utf8_lossy(&del.stderr);
        return Err(anyhow!("Failed to delete service:\n{}", stderr));
    }
    Ok(())
}

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

    #[test]
    fn test_generate_dockerfile() {
        let bundle = AgentBundle {
            name: "monitor".to_string(),
            dsl_path: PathBuf::from("agents/monitor.dsl"),
            project_dir: PathBuf::from("/tmp/test-project"),
        };
        let dockerfile = generate_dockerfile(&bundle, "symbi:latest");
        assert!(dockerfile.contains("FROM symbi:latest"));
        assert!(dockerfile.contains("SYMBIONT_AGENT=monitor"));
        assert!(dockerfile.contains("COPY symbiont.toml"));
        assert!(dockerfile.contains("COPY agents/"));
        assert!(dockerfile.contains("COPY policies/"));
    }

    #[test]
    fn test_default_base_image() {
        assert!(DEFAULT_BASE_IMAGE.contains("symbiont"));
    }
}
