//! Orchestrator-facing wrapper: task.toml → everything needed to invoke
//! Claude Code's `Agent` tool in a single copy-paste-ready bundle.
//!
//! Per RULE 0.13, the orchestrator (main session) owns branch creation,
//! `isolation: "worktree"` selection, and the actual Agent-tool call. This
//! module only assembles the arguments — no git, no spawn, no shell.
//!
//! Wire: `prepare()` = role resolution + `compose_prompt()` + role→subagent_type
//! resolution + `Dna::compose`. Deliberately does NOT create `tasks/<id>/` on
//! disk (that is `spawn::prepare_agent`'s job) so orchestrator can inspect
//! before committing. The "ledger row" field is a pretty-printed string, not
//! a DB write — ledger persistence is the orchestrator's step.

use crate::capability::TaskSpec;
use crate::compose::compose_prompt;
use crate::dna::Dna;
use crate::role::resolve_role;
use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};

/// Everything the orchestrator needs to hand the Claude `Agent` tool.
#[derive(Debug, Clone, Serialize)]
pub struct AgentInvocation {
    pub agent_id: String,
    pub role: String,
    pub prompt: String,
    pub subagent_type: String,
    pub isolation: Option<String>,
    pub description: String,
    pub verify_command: String,
    pub ledger_row: String,
    /// Layer G — composition fingerprint, `<role>::<caps>::<scope>::<body>-<nonce>`.
    pub dna: String,
}

/// Assemble an `AgentInvocation` from a parsed task.toml.
///
/// Errors if the role is unknown or non-spawnable (points at RULE 0.13).
pub fn prepare(task: &TaskSpec, kit_root: &Path) -> Result<AgentInvocation> {
    if task.task.role.is_empty() {
        return Err(anyhow!("task.role is empty"));
    }
    // Auto-generate agent-id if absent. Format: `ag-<role-slug>-<unix-ms-hex>-<4hex-rand>`
    // Orchestrator can still pre-allocate via `kei-ledger fork` and write explicit
    // agent-id into task.toml for deterministic id; auto-gen is the ergonomic default.
    let agent_id = if task.task.agent_id.is_empty() {
        autogen_agent_id(&task.task.role)
    } else {
        task.task.agent_id.clone()
    };
    let role_file = load_role_meta(kit_root, &task.task.role)?;
    if !role_file.role.spawnable {
        return Err(anyhow!(
            "role '{}' is NOT spawnable (per RULE 0.13 git-ops is \
             orchestrator-only) — refusing to prepare Agent tool invocation",
            task.task.role
        ));
    }
    let resolved = resolve_role(kit_root, &task.task.role)?;
    let prompt = compose_prompt(task, kit_root)?;
    let subagent_type = role_file
        .role
        .claude_subagent_type
        .clone()
        .unwrap_or_else(|| default_subagent_type(&task.task.role));
    let isolation = default_isolation(&task.task.role);
    let description = build_description(&task.task.role, &agent_id);
    let verify_command = build_verify_command(&agent_id);
    let ledger_row = build_ledger_row_with_id(task, &agent_id);
    // DNA uses effective agent-id; if auto-generated we inject it into a clone
    // of TaskSpec so Dna::compose sees the resolved id.
    let mut task_for_dna = task.clone();
    task_for_dna.task.agent_id = agent_id.clone();
    let dna = Dna::compose(&task_for_dna, &resolved).render();
    Ok(AgentInvocation {
        agent_id,
        role: task.task.role.clone(),
        prompt,
        subagent_type,
        isolation,
        description,
        verify_command,
        ledger_row,
        dna,
    })
}

fn autogen_agent_id(role: &str) -> String {
    let ts_ms = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_millis())
        .unwrap_or(0);
    let rand_hex = format!("{:04x}", rand::random::<u16>());
    format!("ag-{}-{:x}-{}", role, ts_ms, rand_hex)
}

/// Human-readable block — copy into Claude Code's Agent-tool dialog.
pub fn render_human(inv: &AgentInvocation) -> String {
    let iso = inv.isolation.as_deref().unwrap_or("<none>");
    let mut out = String::new();
    out.push_str("=== AGENT SUBSTRATE v1 — PREPARED SPAWN ===\n");
    out.push_str(&format!("agent-id: {}\n", inv.agent_id));
    out.push_str(&format!("dna: {}\n", inv.dna));
    out.push_str(&format!("subagent_type: {}\n", inv.subagent_type));
    out.push_str(&format!("isolation: {iso}\n"));
    out.push_str(&format!("description: {}\n", inv.description));
    out.push_str("\n--- PROMPT (copy into Agent tool `prompt` param) ---\n");
    out.push_str(&inv.prompt);
    if !inv.prompt.ends_with('\n') {
        out.push('\n');
    }
    out.push_str("--- END PROMPT ---\n\n");
    out.push_str("on return:\n");
    out.push_str(&format!("  {}\n", inv.verify_command));
    out.push_str("  (orchestrator harness returns worktree path in the task-notification)\n\n");
    out.push_str(&format!("ledger: {}\n", inv.ledger_row));
    out
}

pub fn render_json(inv: &AgentInvocation) -> Result<String> {
    serde_json::to_string_pretty(inv).context("serialize AgentInvocation to JSON")
}

pub fn render_toml(inv: &AgentInvocation) -> Result<String> {
    toml::to_string_pretty(inv).context("serialize AgentInvocation to TOML")
}

fn default_isolation(role: &str) -> Option<String> {
    match role {
        "edit-local" | "edit-shared" => Some("worktree".into()),
        _ => None,
    }
}

fn default_subagent_type(role: &str) -> String {
    match role {
        "edit-local" | "edit-shared" => "code-implementer",
        "explorer" => "Explore",
        "read-only" => "critic",
        _ => "critic",
    }
    .into()
}

fn build_description(role: &str, agent_id: &str) -> String {
    let short = agent_id.split('-').take(2).collect::<Vec<_>>().join("-");
    format!("{role} agent {short}")
}

fn build_verify_command(agent_id: &str) -> String {
    format!(
        "kei-agent-runtime verify tasks/{id}/task.toml \
         --worktree <path-from-harness>",
        id = agent_id
    )
}

fn build_ledger_row(task: &TaskSpec) -> String {
    build_ledger_row_with_id(task, &task.task.agent_id)
}

fn build_ledger_row_with_id(task: &TaskSpec, agent_id: &str) -> String {
    let parent = task
        .task
        .parent_agent
        .as_deref()
        .filter(|s| !s.is_empty())
        .unwrap_or("none");
    format!(
        "running agent-id={} role={} parent={}",
        agent_id, task.task.role, parent
    )
}

fn load_role_meta(kit_root: &Path, role: &str) -> Result<RoleFile> {
    let path = kit_root.join("_roles").join(format!("{role}.toml"));
    let text = std::fs::read_to_string(&path)
        .with_context(|| format!("read role file {}", path.display()))?;
    toml::from_str::<RoleFile>(&text)
        .with_context(|| format!("parse role TOML {}", path.display()))
}

#[derive(Debug, Deserialize)]
struct RoleFile {
    #[serde(default)]
    role: RoleMeta,
}

#[derive(Debug, Default, Deserialize)]
struct RoleMeta {
    #[serde(default = "spawnable_default")]
    spawnable: bool,
    #[serde(default, rename = "claude-subagent-type")]
    claude_subagent_type: Option<String>,
}

fn spawnable_default() -> bool {
    true
}
