use std::sync::Arc;

use async_trait::async_trait;
use serde_json::{Value, json};
use uuid::Uuid;

use crate::agent::AgentCatalog;
use crate::scheduler::{Schedule, next_run_for_schedule};
use crate::storage::{DeliveryPolicy, ScheduledJob, ScheduledJobUpdate, Storage};
use crate::tools::traits::{Tool, ToolResult};

fn now_ms() -> i64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as i64
}

#[derive(Clone)]
struct ScheduledJobValidator {
    valid_channels: Vec<String>,
    catalog: Arc<AgentCatalog>,
}

impl ScheduledJobValidator {
    fn validate_channel(&self, channel: &str) -> Result<(), String> {
        if self.valid_channels.iter().any(|value| value == channel) {
            Ok(())
        } else {
            Err(format!(
                "unknown channel '{channel}'; available: {}",
                self.valid_channels.join(", ")
            ))
        }
    }

    fn validate_agent(&self, agent_id: Option<&str>) -> Result<(), String> {
        match agent_id {
            Some("") => Err("agent_id must not be empty".to_string()),
            Some(agent_id) if self.catalog.get(agent_id).is_none() => {
                Err(format!("unknown or disabled Agent '{agent_id}'"))
            }
            _ => Ok(()),
        }
    }
}

pub struct CronAddTool {
    storage: Arc<Storage>,
    validator: ScheduledJobValidator,
}

impl CronAddTool {
    pub fn new(
        storage: Arc<Storage>,
        valid_channels: Vec<String>,
        catalog: Arc<AgentCatalog>,
    ) -> Self {
        Self {
            storage,
            validator: ScheduledJobValidator {
                valid_channels,
                catalog,
            },
        }
    }
}

#[async_trait]
impl Tool for CronAddTool {
    fn name(&self) -> &str {
        "cron_add"
    }

    fn description(&self) -> &str {
        "Create an unattended AI scheduled task executed by the Root Agent or a named Agent. Each run is isolated (no chat history), so the prompt must carry all required context. Every run must finish by calling complete_scheduled_run exactly once; delivery_policy (always/on_alert/never) alone decides whether the structured result is delivered."
    }

    fn parameters_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "schedule": {"type":"object", "description":"at/every/cron schedule", "required":["type"]},
                "prompt": {"type":"string", "minLength":1},
                "channel": {"type":"string", "minLength":1},
                "chat_id": {"type":"string", "minLength":1},
                "name": {"type":"string", "minLength":1},
                "agent_id": {"type":"string", "description":"Optional named Agent; omit for Root"},
                "delivery_policy": {
                    "type":"string",
                    "enum":["always","on_alert","never"],
                    "description":"always sends every result; on_alert suppresses only ok; never sends nothing"
                }
            },
            "required":["schedule","prompt","channel","chat_id"],
            "additionalProperties": false
        })
    }

    async fn execute(&self, args: Value) -> anyhow::Result<ToolResult> {
        let schedule = match parse_schedule(args.get("schedule"), now_ms()) {
            Ok(schedule) => schedule,
            Err(error) => return Ok(failure(error)),
        };
        let prompt = args
            .get("prompt")
            .and_then(Value::as_str)
            .unwrap_or_default()
            .trim()
            .to_string();
        let channel = args
            .get("channel")
            .and_then(Value::as_str)
            .unwrap_or_default()
            .trim()
            .to_string();
        let chat_id = args
            .get("chat_id")
            .and_then(Value::as_str)
            .unwrap_or_default()
            .trim()
            .to_string();
        if prompt.is_empty() || chat_id.is_empty() {
            return Ok(failure("prompt and chat_id are required"));
        }
        if let Err(error) = self.validator.validate_channel(&channel) {
            return Ok(failure(error));
        }
        let agent_id = args
            .get("agent_id")
            .and_then(Value::as_str)
            .map(str::trim)
            .map(str::to_string);
        if let Err(error) = self.validator.validate_agent(agent_id.as_deref()) {
            return Ok(failure(error));
        }
        let delivery_policy = match parse_policy(args.get("delivery_policy")) {
            Ok(policy) => policy.unwrap_or(DeliveryPolicy::Always),
            Err(error) => return Ok(failure(error)),
        };
        let name = args
            .get("name")
            .and_then(Value::as_str)
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(str::to_string)
            .unwrap_or_else(|| prompt.chars().take(50).collect());
        let now = now_ms();
        let next_run_at = next_run_for_schedule(&schedule, now)
            .ok_or_else(|| anyhow::anyhow!("could not compute next run"))?;
        let id = Uuid::new_v4().to_string()[..8].to_string();
        self.storage
            .add_scheduled_job(&ScheduledJob {
                id: id.clone(),
                name: name.clone(),
                schedule,
                prompt,
                agent_id,
                channel,
                chat_id,
                delivery_policy,
                enabled: true,
                next_run_at,
                last_run_at: None,
                last_outcome: None,
                created_at: now,
                updated_at: now,
                locked_at: None,
                lock_owner: None,
                lease_until: None,
            })
            .await?;
        Ok(success(format!(
            "Scheduled job created: id={id}, name=\"{name}\", next_run_at={next_run_at}"
        )))
    }
}

pub struct CronListTool {
    storage: Arc<Storage>,
}

impl CronListTool {
    pub fn new(storage: Arc<Storage>) -> Self {
        Self { storage }
    }
}

#[async_trait]
impl Tool for CronListTool {
    fn name(&self) -> &str {
        "cron_list"
    }

    fn description(&self) -> &str {
        "List scheduled tasks and their latest structured outcome."
    }

    fn read_only(&self) -> bool {
        true
    }

    fn parameters_schema(&self) -> Value {
        json!({
            "type":"object",
            "properties":{"status":{"type":"string","enum":["all","enabled","disabled"]}}
        })
    }

    async fn execute(&self, args: Value) -> anyhow::Result<ToolResult> {
        let filter = args.get("status").and_then(Value::as_str).unwrap_or("all");
        let jobs = self.storage.list_scheduled_jobs().await?;
        let jobs: Vec<_> = jobs
            .into_iter()
            .filter(|job| match filter {
                "enabled" => job.enabled,
                "disabled" => !job.enabled,
                _ => true,
            })
            .map(|job| {
                json!({
                    "id": job.id,
                    "name": job.name,
                    "enabled": job.enabled,
                    "agent_id": job.agent_id,
                    "delivery_policy": job.delivery_policy,
                    "channel": job.channel,
                    "chat_id": job.chat_id,
                    "next_run_at": job.next_run_at,
                    "last_run_at": job.last_run_at,
                    "last_outcome": job.last_outcome,
                })
            })
            .collect();
        Ok(success(serde_json::to_string(&json!({"jobs":jobs}))?))
    }
}

pub struct CronRunsTool {
    storage: Arc<Storage>,
}

impl CronRunsTool {
    pub fn new(storage: Arc<Storage>) -> Self {
        Self { storage }
    }
}

#[async_trait]
impl Tool for CronRunsTool {
    fn name(&self) -> &str {
        "cron_runs"
    }

    fn description(&self) -> &str {
        "Read structured execution and delivery results for one scheduled task, including silent runs."
    }

    fn read_only(&self) -> bool {
        true
    }

    fn parameters_schema(&self) -> Value {
        json!({
            "type":"object",
            "properties":{
                "job_id":{"type":"string","minLength":1},
                "run_id":{"type":"integer","minimum":1},
                "limit":{"type":"integer","minimum":1,"maximum":100}
            },
            "required":["job_id"],
            "additionalProperties":false
        })
    }

    async fn execute(&self, args: Value) -> anyhow::Result<ToolResult> {
        let job_id = args
            .get("job_id")
            .and_then(Value::as_str)
            .unwrap_or_default();
        if job_id.is_empty() {
            return Ok(failure("job_id is required"));
        }
        self.storage.get_scheduled_job(job_id).await?;
        if let Some(value) = args.get("run_id") {
            let Some(run_id) = value.as_i64() else {
                return Ok(failure("run_id must be an integer greater than zero"));
            };
            if run_id < 1 {
                return Ok(failure("run_id must be greater than zero"));
            }
            let run = self.storage.get_scheduled_job_run(run_id).await?;
            if run.job_id != job_id {
                return Ok(failure("run does not belong to the requested job"));
            }
            return Ok(success(serde_json::to_string(
                &json!({"run":scheduled_run_json(&run, true)}),
            )?));
        }
        let limit = match args.get("limit") {
            Some(value) => match value.as_u64() {
                Some(limit @ 1..=100) => limit as usize,
                _ => return Ok(failure("limit must be an integer from 1 to 100")),
            },
            None => 20,
        };
        let runs = self.storage.list_scheduled_job_runs(job_id, limit).await?;
        let runs = runs
            .iter()
            .map(|run| scheduled_run_json(run, false))
            .collect::<Vec<_>>();
        Ok(success(serde_json::to_string(&json!({"runs":runs}))?))
    }
}

pub struct CronRemoveTool {
    storage: Arc<Storage>,
}

impl CronRemoveTool {
    pub fn new(storage: Arc<Storage>) -> Self {
        Self { storage }
    }
}

#[async_trait]
impl Tool for CronRemoveTool {
    fn name(&self) -> &str {
        "cron_remove"
    }

    fn description(&self) -> &str {
        "Permanently delete a scheduled task that has no active occurrence or pending delivery."
    }

    fn parameters_schema(&self) -> Value {
        job_id_schema()
    }

    async fn execute(&self, args: Value) -> anyhow::Result<ToolResult> {
        let job_id = required_job_id(&args)?;
        self.storage.remove_scheduled_job(job_id).await?;
        Ok(success(format!("Job {job_id} deleted.")))
    }
}

pub struct CronEnableTool {
    storage: Arc<Storage>,
}

impl CronEnableTool {
    pub fn new(storage: Arc<Storage>) -> Self {
        Self { storage }
    }
}

#[async_trait]
impl Tool for CronEnableTool {
    fn name(&self) -> &str {
        "cron_enable"
    }

    fn description(&self) -> &str {
        "Enable a scheduled task and recompute its next run time. An expired one-shot At schedule must be updated to a future time before it can be enabled."
    }

    fn parameters_schema(&self) -> Value {
        job_id_schema()
    }

    async fn execute(&self, args: Value) -> anyhow::Result<ToolResult> {
        let job_id = required_job_id(&args)?;
        let job = self.storage.get_scheduled_job(job_id).await?;
        let now = now_ms();
        if matches!(job.schedule, Schedule::At { at } if at <= now) {
            return Ok(failure(
                "expired At task cannot be enabled; update its schedule to a future time",
            ));
        }
        let next = next_run_for_schedule(&job.schedule, now)
            .ok_or_else(|| anyhow::anyhow!("could not compute next run"))?;
        self.storage
            .update_scheduled_job(job_id, ScheduledJobUpdate::default(), Some(next))
            .await?;
        self.storage.set_scheduled_job_enabled(job_id, true).await?;
        Ok(success(format!("Job {job_id} enabled.")))
    }
}

pub struct CronDisableTool {
    storage: Arc<Storage>,
}

impl CronDisableTool {
    pub fn new(storage: Arc<Storage>) -> Self {
        Self { storage }
    }
}

#[async_trait]
impl Tool for CronDisableTool {
    fn name(&self) -> &str {
        "cron_disable"
    }

    fn description(&self) -> &str {
        "Disable future occurrences of a scheduled task without cancelling its current run."
    }

    fn parameters_schema(&self) -> Value {
        job_id_schema()
    }

    async fn execute(&self, args: Value) -> anyhow::Result<ToolResult> {
        let job_id = required_job_id(&args)?;
        self.storage.get_scheduled_job(job_id).await?;
        self.storage
            .set_scheduled_job_enabled(job_id, false)
            .await?;
        Ok(success(format!("Job {job_id} disabled.")))
    }
}

pub struct CronUpdateTool {
    storage: Arc<Storage>,
    validator: ScheduledJobValidator,
}

impl CronUpdateTool {
    pub fn new(
        storage: Arc<Storage>,
        valid_channels: Vec<String>,
        catalog: Arc<AgentCatalog>,
    ) -> Self {
        Self {
            storage,
            validator: ScheduledJobValidator {
                valid_channels,
                catalog,
            },
        }
    }
}

#[async_trait]
impl Tool for CronUpdateTool {
    fn name(&self) -> &str {
        "cron_update"
    }

    fn description(&self) -> &str {
        "Update selected fields of a scheduled task: name, prompt, schedule, channel, chat_id, agent_id, or delivery_policy. Omit a field to leave it unchanged; pass agent_id null to switch back to the Root Agent. An At schedule must be in the future."
    }

    fn parameters_schema(&self) -> Value {
        json!({
            "type":"object",
            "properties":{
                "job_id":{"type":"string","minLength":1},
                "name":{"type":"string","minLength":1},
                "prompt":{"type":"string","minLength":1},
                "schedule":{"type":"object"},
                "agent_id":{"type":["string","null"]},
                "channel":{"type":"string","minLength":1},
                "chat_id":{"type":"string","minLength":1},
                "delivery_policy":{"type":"string","enum":["always","on_alert","never"]}
            },
            "required":["job_id"],
            "additionalProperties":false
        })
    }

    async fn execute(&self, args: Value) -> anyhow::Result<ToolResult> {
        let job_id = required_job_id(&args)?;
        self.storage.get_scheduled_job(job_id).await?;
        let now = now_ms();
        let schedule = match args.get("schedule") {
            Some(value) => Some(match parse_schedule(Some(value), now) {
                Ok(schedule) => schedule,
                Err(error) => return Ok(failure(error)),
            }),
            None => None,
        };
        let next_run_at = schedule
            .as_ref()
            .and_then(|schedule| next_run_for_schedule(schedule, now));
        let channel = args
            .get("channel")
            .and_then(Value::as_str)
            .map(str::trim)
            .map(str::to_string);
        if let Some(channel) = channel.as_deref()
            && let Err(error) = self.validator.validate_channel(channel)
        {
            return Ok(failure(error));
        }
        let agent_id = if args.get("agent_id").is_some() {
            Some(
                args.get("agent_id")
                    .and_then(Value::as_str)
                    .map(str::trim)
                    .map(str::to_string),
            )
        } else {
            None
        };
        if let Some(value) = agent_id.as_ref()
            && let Err(error) = self.validator.validate_agent(value.as_deref())
        {
            return Ok(failure(error));
        }
        let delivery_policy = match parse_policy(args.get("delivery_policy")) {
            Ok(policy) => policy,
            Err(error) => return Ok(failure(error)),
        };
        let name = string_field(&args, "name");
        let prompt = string_field(&args, "prompt");
        let chat_id = string_field(&args, "chat_id");
        for (field, value) in [
            ("name", name.as_deref()),
            ("prompt", prompt.as_deref()),
            ("chat_id", chat_id.as_deref()),
        ] {
            if value.is_some_and(str::is_empty) {
                return Ok(failure(format!("{field} must not be empty")));
            }
        }
        let update = ScheduledJobUpdate {
            name,
            prompt,
            schedule,
            agent_id,
            channel,
            chat_id,
            delivery_policy,
        };
        self.storage
            .update_scheduled_job(job_id, update, next_run_at)
            .await?;
        Ok(success(format!("Job {job_id} updated.")))
    }
}

fn parse_schedule(value: Option<&Value>, now: i64) -> Result<Schedule, String> {
    let value = value.ok_or_else(|| "schedule is required".to_string())?;
    let schedule: Schedule = serde_json::from_value(value.clone())
        .map_err(|error| format!("invalid schedule: {error}"))?;
    match &schedule {
        Schedule::At { at } if *at <= now => Err("At schedule must be in the future".to_string()),
        Schedule::Every { every_ms: 0 } => Err("every_ms must be greater than zero".to_string()),
        _ if next_run_for_schedule(&schedule, now).is_none() => {
            Err("schedule has no computable next run".to_string())
        }
        _ => Ok(schedule),
    }
}

fn parse_policy(value: Option<&Value>) -> Result<Option<DeliveryPolicy>, String> {
    match value.and_then(Value::as_str) {
        None if value.is_none() => Ok(None),
        Some("always") => Ok(Some(DeliveryPolicy::Always)),
        Some("on_alert") => Ok(Some(DeliveryPolicy::OnAlert)),
        Some("never") => Ok(Some(DeliveryPolicy::Never)),
        Some(other) => Err(format!("invalid delivery_policy: {other}")),
        None => Err("delivery_policy must be a string".to_string()),
    }
}

fn string_field(args: &Value, name: &str) -> Option<String> {
    args.get(name)
        .and_then(Value::as_str)
        .map(str::trim)
        .map(str::to_string)
}

fn scheduled_run_json(run: &crate::storage::JobRun, detail: bool) -> Value {
    let bounded = |value: Option<&str>, limit: usize| {
        value.map(|text| {
            if detail {
                text.to_string()
            } else {
                let mut output = text.chars().take(limit).collect::<String>();
                if text.chars().count() > limit {
                    output.push('…');
                }
                output
            }
        })
    };
    json!({
        "run_id": run.id,
        "job_id": run.job_id,
        "scheduled_for": run.scheduled_for,
        "agent_run_id": run.agent_run_id,
        "agent_id": run.agent_id,
        "started_at": run.started_at,
        "finished_at": run.finished_at,
        "status": run.status,
        "outcome": run.outcome,
        "message": bounded(run.message.as_deref(), 512),
        "diagnostic": bounded(run.diagnostic.as_deref(), 512),
        "duration_ms": run.duration_ms,
        "delivery_status": run.delivery_status,
        "delivery_attempts": run.delivery_attempts,
        "delivery_error": bounded(run.delivery_error.as_deref(), 512),
    })
}

fn required_job_id(args: &Value) -> anyhow::Result<&str> {
    let value = args
        .get("job_id")
        .and_then(Value::as_str)
        .unwrap_or_default()
        .trim();
    if value.is_empty() {
        anyhow::bail!("job_id is required");
    }
    Ok(value)
}

fn job_id_schema() -> Value {
    json!({
        "type":"object",
        "properties":{"job_id":{"type":"string","minLength":1}},
        "required":["job_id"],
        "additionalProperties":false
    })
}

fn success(output: String) -> ToolResult {
    ToolResult {
        success: true,
        output,
        error: None,
    }
}

fn failure(error: impl Into<String>) -> ToolResult {
    ToolResult {
        success: false,
        output: String::new(),
        error: Some(error.into()),
    }
}

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

    #[test]
    fn expired_at_is_rejected() {
        assert!(parse_schedule(Some(&json!({"type":"at","at":10})), 10).is_err());
    }

    #[test]
    fn policy_has_only_three_values() {
        assert_eq!(
            parse_policy(Some(&json!("on_alert"))).unwrap(),
            Some(DeliveryPolicy::OnAlert)
        );
        assert!(parse_policy(Some(&json!("direct"))).is_err());
    }

    #[tokio::test]
    async fn cron_runs_is_read_only() {
        let dir = tempfile::tempdir().unwrap();
        let storage = Arc::new(
            Storage::new(&dir.path().join("cron-runs.db"))
                .await
                .unwrap(),
        );
        let tool = CronRunsTool::new(storage);
        assert!(tool.read_only());
    }
}
