use std::collections::BTreeMap;

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::{AppId, RequestId, ScriptId};

/// One row in the `execution_logs` table. Same shape flows through the
/// `ExecutionLogSink` trait and the `GET /scripts/{id}/logs` response.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionLog {
    pub id: Uuid,
    /// Owning app at the time of execution. Materialized at write time
    /// so a future "move script to another app" doesn't retag history.
    pub app_id: AppId,
    pub script_id: ScriptId,
    pub request_id: RequestId,

    pub request_path: String,
    pub request_headers: BTreeMap<String, String>,
    pub request_body: serde_json::Value,

    pub response_code: Option<u16>,
    pub response_body: Option<serde_json::Value>,

    /// `log::*` entries captured during the execution, serialized as a
    /// JSON array of `{timestamp, level, message, data}` objects.
    pub script_logs: serde_json::Value,

    pub duration_ms: u64,
    pub status: ExecutionStatus,

    /// What dispatched this execution: `http` for direct data-plane
    /// ingress, or one of the trigger kinds (`kv`, `cron`, `queue`,
    /// `invoke`, …) for background runs. Materialized so `pic logs`
    /// can surface — and filter by — the origin of every run, not just
    /// the HTTP ones. Defaults to `http` for rows written before the
    /// column existed (migration 0043).
    #[serde(default)]
    pub source: ExecutionSource,

    pub created_at: DateTime<Utc>,
}

/// Origin of an execution. Wire strings mirror
/// `manager-core::OutboxSourceKind` (plus `Queue`, which the queue
/// consumer dispatches outside the outbox) so a trigger's source kind
/// maps straight through to its execution-log row. Keep the variants and
/// the `source` CHECK constraint in migration 0043 in sync.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionSource {
    #[default]
    Http,
    Kv,
    Docs,
    DeadLetter,
    Cron,
    Files,
    Pubsub,
    Email,
    Invoke,
    Queue,
    Workflow,
}

impl ExecutionSource {
    #[must_use]
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Http => "http",
            Self::Kv => "kv",
            Self::Docs => "docs",
            Self::DeadLetter => "dead_letter",
            Self::Cron => "cron",
            Self::Files => "files",
            Self::Pubsub => "pubsub",
            Self::Email => "email",
            Self::Invoke => "invoke",
            Self::Queue => "queue",
            Self::Workflow => "workflow",
        }
    }

    /// Parse a wire string back into a variant. Returns `None` for an
    /// unknown value (callers treat that as "no filter" or a 422).
    #[must_use]
    pub fn from_wire(s: &str) -> Option<Self> {
        match s {
            "http" => Some(Self::Http),
            "kv" => Some(Self::Kv),
            "docs" => Some(Self::Docs),
            "dead_letter" => Some(Self::DeadLetter),
            "cron" => Some(Self::Cron),
            "files" => Some(Self::Files),
            "pubsub" => Some(Self::Pubsub),
            "email" => Some(Self::Email),
            "invoke" => Some(Self::Invoke),
            "queue" => Some(Self::Queue),
            "workflow" => Some(Self::Workflow),
            _ => None,
        }
    }
}

/// Matches the CHECK constraint on `execution_logs.status`. Keep the
/// serde rename in sync with the migration.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionStatus {
    Success,
    Error,
    Timeout,
    BudgetExceeded,
}

impl ExecutionStatus {
    #[must_use]
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Success => "success",
            Self::Error => "error",
            Self::Timeout => "timeout",
            Self::BudgetExceeded => "budget_exceeded",
        }
    }
}
