use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use tracing::{debug, trace, warn};

use core_api::provider::LlmStrength;

const AGENTS_DIR: &str = "agents";

/// The role an agent plays, declared by the required `type` field in `meta.json`.
///
/// - `Chat`: a conversational entry-point the user talks to directly (e.g. `main`,
///   `project-coordinator`). Not dispatchable as a sub-agent, not a valid task root.
/// - `Task`: a task executor. Dispatchable by a parent agent **and** a valid root of a
///   scheduled/async task (e.g. `software-engineer`, `researcher`, `generalist`).
/// - `System`: a hidden background agent wired into the runtime by id (e.g. `tic`).
///   Never listed, never user-chattable, never dispatchable from the tool surface.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentType {
    Chat,
    Task,
    System,
}

#[derive(Deserialize)]
struct RawMeta {
    name:          String,
    description:   String,
    #[serde(default)]
    friendly_description: Option<String>,
    #[serde(default)]
    instructions:  Option<String>,
    #[serde(default)]
    inject_memory: Vec<String>,
    #[serde(default)]
    client:        Option<String>,
    #[serde(default)]
    scope:         Option<String>,
    #[serde(default)]
    strength:      Option<LlmStrength>,
    /// Required: declares the agent's role. A `meta.json` without `type` fails to load.
    #[serde(rename = "type")]
    agent_type:    AgentType,
    #[serde(default = "default_true")]
    inject_skills: bool,
    #[serde(default)]
    icon:          Option<String>,
}

/// Serde default for boolean fields that should be `true` when the key is absent.
fn default_true() -> bool { true }

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentMeta {
    pub id:            String,
    pub name:          String,
    /// Routing description for the **orchestrator LLM**: "when should I delegate to this
    /// agent, and what does it return?". Injected into `<!-- AGENTS_LIST -->` and returned
    /// by `list_items` (type=agents). Required.
    pub description:   String,
    /// Human-facing blurb shown to the **user** on the frontend Agents page. When absent
    /// the frontend falls back to `description`. Applies to every agent type.
    #[serde(default)]
    pub friendly_description: Option<String>,
    /// Note for the **calling LLM** on *how* to invoke this agent for the best result
    /// (expected inputs, format, gotchas). Kept short. Only meaningful for `task` agents —
    /// it is surfaced solely via `list_items` (type=agents), which already lists task
    /// agents only, so no extra gating is needed.
    #[serde(default)]
    pub instructions:  Option<String>,
    #[serde(default)]
    pub inject_memory: Vec<String>,
    /// Preferred LLM client name (must exist in the DB, configured via the web app).
    /// If unset, the sub-agent inherits the caller's client.
    #[serde(default)]
    pub client:        Option<String>,
    /// Task domain this agent operates in (e.g. "coding", "reasoning").
    /// Used by AUTO client selection to find a matching LLM.
    #[serde(default)]
    pub scope:         Option<String>,
    /// Minimum LLM capability required to run this agent reliably.
    /// AUTO selection skips clients weaker than this threshold.
    #[serde(default)]
    pub strength:      Option<LlmStrength>,
    /// The agent's role (`chat` / `task` / `system`). Only `task` agents are listed in
    /// `list_items` (type=agents) / the AGENTS_LIST injection and are dispatchable or
    /// runnable as a task root; `chat` and `system` are excluded from those paths.
    #[serde(rename = "type")]
    pub agent_type:    AgentType,
    /// When true (the default, including when the key is absent), the skills index
    /// (`skills/index.md`) is injected into this agent's system prompt so it can
    /// discover and use installed skills. Set false for background agents that don't
    /// need them (e.g. TIC) to save tokens.
    #[serde(default = "default_true")]
    pub inject_skills: bool,
    /// Path to the agent's icon image file (relative to the agent's directory).
    /// Defaults to None if no icon is configured.
    #[serde(default)]
    pub icon: Option<String>,
}

/// Scan `agents/` and return metadata for every agent that has both
/// `meta.json` and `AGENT.md`. Skips the `common/` directory.
pub fn discover() -> Result<Vec<AgentMeta>> {
    let mut agents = Vec::new();

    let dir = std::fs::read_dir(AGENTS_DIR)
        .with_context(|| format!("Failed to read agents directory '{AGENTS_DIR}'"))?;

    for entry in dir {
        let entry = entry?;
        let path = entry.path();
        if !path.is_dir() { continue; }

        let id = match path.file_name().and_then(|n| n.to_str()) {
            Some(n) if !n.is_empty() && n != "common" => n.to_string(),
            _ => continue,
        };

        let meta_path   = path.join("meta.json");
        let system_path = path.join("AGENT.md");
        if !meta_path.exists() || !system_path.exists() {
            warn!(agent_id = %id, "skipping agent: missing meta.json or AGENT.md");
            continue;
        }

        let raw_str = match std::fs::read_to_string(&meta_path) {
            Ok(s) => s,
            Err(e) => {
                warn!(agent_id = %id, error = %e, "skipping agent: cannot read meta.json");
                continue;
            }
        };
        // A single malformed meta.json (e.g. missing the required `type` field) must not
        // blank the whole roster — warn and skip it, keep discovering the rest.
        let raw: RawMeta = match serde_json::from_str(&raw_str) {
            Ok(r) => r,
            Err(e) => {
                warn!(agent_id = %id, error = %e, "skipping agent: invalid meta.json");
                continue;
            }
        };

        let meta = AgentMeta {
            id,
            name:            raw.name,
            description:     raw.description,
            friendly_description: raw.friendly_description,
            instructions:    raw.instructions,
            inject_memory:   raw.inject_memory,
            client:          raw.client,
            scope:           raw.scope,
            strength:        raw.strength,
            agent_type:      raw.agent_type,
            inject_skills:   raw.inject_skills,
            icon:            raw.icon,
        };
        trace!(agent_id = %meta.id, client = ?meta.client, scope = ?meta.scope, strength = ?meta.strength, "agent meta loaded");
        debug!(agent_id = %meta.id, name = %meta.name, "agent discovered");
        agents.push(meta);
    }

    agents.sort_by(|a, b| a.id.cmp(&b.id));
    Ok(agents)
}

/// Load metadata for a single agent (reads its `meta.json`).
pub fn load_meta(agent_id: &str) -> Result<AgentMeta> {
    let path = format!("{AGENTS_DIR}/{agent_id}/meta.json");
    let raw_str = std::fs::read_to_string(&path)
        .with_context(|| format!("Agent '{agent_id}': meta.json not found at '{path}'"))?;

    let raw: RawMeta = serde_json::from_str(&raw_str)
        .with_context(|| format!("Agent '{agent_id}': failed to parse meta.json"))?;

    Ok(AgentMeta {
        id:              agent_id.to_string(),
        name:            raw.name,
        description:     raw.description,
        friendly_description: raw.friendly_description,
        instructions:    raw.instructions,
        inject_memory:   raw.inject_memory,
        client:          raw.client,
        scope:           raw.scope,
        strength:        raw.strength,
        agent_type:      raw.agent_type,
        inject_skills:   raw.inject_skills,
        icon:            raw.icon,
    })
}

/// Load metadata for `agent_id` and assert it is a runnable **task** agent.
/// Errors if the agent does not exist or is a `chat` / `system` agent — i.e. the
/// single gate for "can this agent be dispatched or run as a task root?".
pub fn load_task_meta(agent_id: &str) -> Result<AgentMeta> {
    let meta = load_meta(agent_id)?;
    if meta.agent_type != AgentType::Task {
        anyhow::bail!(
            "agent `{agent_id}` is a {:?} agent and cannot be dispatched or run as a task — only `task` agents can",
            meta.agent_type
        );
    }
    Ok(meta)
}

/// Load and resolve the system prompt for `agent_id` from disk.
/// Called at request time so edits to `.md` files take effect without restart.
pub fn load_prompt(agent_id: &str) -> Result<String> {
    let path = format!("{AGENTS_DIR}/{agent_id}/AGENT.md");
    let content = std::fs::read_to_string(&path)
        .with_context(|| format!("Agent '{agent_id}': AGENT.md not found at '{path}'"))?;
    resolve_includes(&content)
}

fn resolve_includes(content: &str) -> Result<String> {
    let mut out = String::with_capacity(content.len());
    for line in content.lines() {
        let trimmed = line.trim();
        if let Some(path_raw) = trimmed
            .strip_prefix("<!-- INCLUDE:")
            .and_then(|s| s.strip_suffix("-->"))
        {
            let path = format!("{AGENTS_DIR}/{}", path_raw.trim());
            let included = std::fs::read_to_string(&path)
                .with_context(|| format!("INCLUDE: failed to read '{path}'"))?;
            out.push_str(&format!("<included_file path=\"{path}\">\n"));
            out.push_str(&resolve_includes(&included)?);
            out.push_str("</included_file>\n");
        } else if trimmed == "<!-- AGENTS_LIST -->" {
            out.push_str(&render_agents_list()?);
        } else if trimmed == "<!-- MCP_LIST -->" {
            // Replaced at request time in build_openai_messages with dynamic
            // active/hidden sections. Leave a sentinel so the injection point
            // is preserved and positioned correctly in the prompt.
            out.push_str("__MCP_LIST__\n");
        } else if let Some(key) = trimmed
            .strip_prefix("<!-- ")
            .and_then(|s| s.strip_suffix(" -->"))
            .filter(|k| k.chars().all(|c| c.is_ascii_uppercase() || c == '_'))
        {
            // Generic runtime substitution: <!-- KEY --> → __KEY__ sentinel.
            // Replaced at request time via SendMessageOptions::system_substitutions.
            out.push_str(&format!("__{key}__\n"));
        } else {
            out.push_str(line);
            out.push('\n');
        }
    }
    Ok(out)
}

fn render_agents_list() -> Result<String> {
    let agents = discover()?;
    let mut out = String::new();
    for agent in agents.iter().filter(|a| a.agent_type == AgentType::Task) {
        out.push_str(&format!("- **{}** — {}\n", agent.id, agent.description));
    }
    Ok(out)
}
