use std::collections::BTreeMap;

use chrono::{DateTime, Utc};
use picloud_shared::{
    AppId, ExecutionId, ExecutionLog, ExecutionSource, ExecutionStatus, Principal, RequestId,
    ScriptId, ScriptOwner, ScriptSandbox, TriggerEvent,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid;

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum InvocationType {
    Http,
    Function,
    Scheduled,
}

/// Everything the executor needs to run one invocation.
///
/// This type crosses process boundaries in cluster mode — keep it fully
/// serializable and don't add transient handles (DB pools, etc.).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecRequest {
    pub execution_id: ExecutionId,
    pub request_id: RequestId,
    pub script_id: ScriptId,
    pub script_name: String,
    pub invocation_type: InvocationType,
    pub path: String,

    /// HTTP method of the originating request, uppercased (`GET`, `POST`,
    /// …). Exposed to scripts as `ctx.request.method` so a single script
    /// can branch on the verb instead of needing one script per method.
    /// Empty for non-HTTP trigger invocations (cron/kv/docs/…), matching
    /// how `path`/`rest` are empty there.
    #[serde(default)]
    pub method: String,

    pub headers: BTreeMap<String, String>,
    pub body: serde_json::Value,

    /// Path-parameter captures from named-param routes (`/users/:id`).
    /// Exposed to scripts as `ctx.request.params`. Empty for exact and
    /// prefix routes.
    #[serde(default)]
    pub params: BTreeMap<String, String>,

    /// Query-string parameters, parsed once at request entry.
    /// Exposed as `ctx.request.query`.
    #[serde(default)]
    pub query: BTreeMap<String, String>,

    /// Suffix captured by a `prefix` route: `/greet/*` matched against
    /// `/greet/alice/morning` yields `"alice/morning"`. Empty for
    /// exact and param matches.
    #[serde(default)]
    pub rest: String,

    /// Per-script sandbox overrides resolved by the manager. The
    /// executor's default `Limits` get merged with these (per-field
    /// override) before the Rhai engine is built.
    #[serde(default)]
    pub sandbox_overrides: ScriptSandbox,

    /// Owning application. Source of truth for every `(app_id, …)`
    /// storage lookup the script makes via stateful SDK services.
    /// Internal-only; not surfaced via `ctx` (which the script sees).
    pub app_id: AppId,

    /// The **defining node** of the top-level script being executed —
    /// the resolved `Script`'s owner (Phase 4b). This is the lexical
    /// origin its `import` statements resolve against (§5.5): an inherited
    /// group endpoint's imports seal to the **group**, not the inheriting
    /// app, so a leaf can't shadow them. Distinct from `app_id`, which is
    /// always the execution boundary (where SDK calls scope). `None` (old
    /// payloads / cluster wire) falls back to `App(app_id)` in the engine.
    #[serde(default)]
    pub script_owner: Option<ScriptOwner>,

    /// Caller identity, when authenticated. `None` for unauthenticated
    /// data-plane HTTP requests (the common case for public scripts);
    /// `Some` when a bearer token or session cookie was resolved.
    /// Internal-only — exposed via `SdkCallCx` to service trait impls.
    ///
    /// `#[serde(skip)]`: `ExecRequest` is serializable so cluster mode
    /// (v1.3+) can ship invocations to remote executors over HTTP, but
    /// `Principal` has no wire derivation today. Skipping here keeps
    /// v1.1.0 compiling; the cluster-mode PR will introduce a wire-safe
    /// snapshot then.
    #[serde(skip)]
    pub principal: Option<Principal>,

    /// Triggers-framework depth. `0` for direct invocations. The
    /// dispatcher (v1.1.1) increments on each indirection to bound
    /// runaway feedback loops.
    #[serde(default)]
    pub trigger_depth: u32,

    /// Originating execution id of a trigger chain. Equal to
    /// `execution_id` for direct invocations; preserves the root
    /// across fan-out for audit log grouping.
    pub root_execution_id: ExecutionId,

    /// `true` only when the dispatcher resolved this invocation
    /// against a `dead_letter` trigger. The retry / dead-letter
    /// machinery short-circuits when this is set so handler failures
    /// cannot themselves be dead-lettered (design notes §4
    /// recursion-stop rule).
    #[serde(default)]
    pub is_dead_letter_handler: bool,

    /// The originating event for a triggered invocation. `None` for
    /// direct ingress (sync HTTP, manual admin run). Flattened into
    /// `ctx.event` by the executor's per-call ctx builder.
    #[serde(default)]
    pub event: Option<TriggerEvent>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecResponse {
    pub status_code: u16,
    pub headers: BTreeMap<String, String>,
    pub body: serde_json::Value,
    pub logs: Vec<LogEntry>,
    pub stats: ExecStats,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
    Trace,
    Info,
    Warn,
    Error,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogEntry {
    pub timestamp: DateTime<Utc>,
    pub level: LogLevel,
    pub message: String,
    pub data: Option<serde_json::Value>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub struct ExecStats {
    pub duration_ms: u64,
    pub operations: u64,
}

/// Serialize/Deserialize are derived for `RemoteExecutorClient` (cluster
/// mode v1.3+) — the executor returns this shape over the wire so the
/// orchestrator can reconstruct the variant rather than collapsing to a
/// generic string. Audit F-N-001 follow-up.
#[derive(Debug, Error, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum ExecError {
    #[error("script failed to parse: {0}")]
    Parse(String),

    #[error("script returned an invalid response shape: {0}")]
    InvalidResponse(String),

    #[error("execution timed out after {0}s")]
    Timeout(u32),

    #[error("execution exceeded operation budget")]
    OperationBudgetExceeded,

    #[error("script runtime error: {0}")]
    Runtime(String),

    /// Concurrency gate (orchestrator-core::ExecutionGate) refused
    /// admission. Surfaced as HTTP 503 with a `Retry-After` header.
    /// The gate enforces a global cap so a script storm can't park
    /// every blocking thread.
    #[error("execution declined: server at capacity (retry after {retry_after_secs}s)")]
    Overloaded { retry_after_secs: u32 },
}

/// Build an `ExecutionLog` row from one invocation's outcome.
///
/// Shared by every execution path so the log shape stays identical
/// regardless of who ran the script: the orchestrator's sync/direct HTTP
/// handlers pass `ExecutionSource::Http`, while the manager's trigger
/// dispatcher passes the trigger's kind (`Kv`, `Cron`, `Invoke`, …). That
/// `source` is what lets `pic logs` surface background runs that were
/// previously invisible. `Overloaded` is never logged — admission is
/// refused before a row exists — but it maps to `Error` defensively.
#[allow(clippy::too_many_arguments)]
#[must_use]
pub fn build_execution_log(
    app_id: AppId,
    script_id: ScriptId,
    request_id: RequestId,
    request_path: String,
    request_headers: BTreeMap<String, String>,
    request_body: serde_json::Value,
    source: ExecutionSource,
    outcome: &Result<ExecResponse, ExecError>,
    started: DateTime<Utc>,
    finished: DateTime<Utc>,
) -> ExecutionLog {
    let duration_ms = u64::try_from(
        finished
            .signed_duration_since(started)
            .num_milliseconds()
            .max(0),
    )
    .unwrap_or(0);

    let (status, response_code, response_body, script_logs) = match outcome {
        Ok(resp) => {
            let logs = serde_json::to_value(&resp.logs).unwrap_or(serde_json::Value::Array(vec![]));
            (
                ExecutionStatus::Success,
                Some(resp.status_code),
                Some(resp.body.clone()),
                logs,
            )
        }
        Err(e) => {
            let status = match e {
                ExecError::Timeout(_) => ExecutionStatus::Timeout,
                ExecError::OperationBudgetExceeded => ExecutionStatus::BudgetExceeded,
                _ => ExecutionStatus::Error,
            };
            (
                status,
                None,
                Some(serde_json::json!({ "error": e.to_string() })),
                serde_json::Value::Array(vec![]),
            )
        }
    };

    ExecutionLog {
        id: Uuid::new_v4(),
        app_id,
        script_id,
        request_id,
        request_path,
        request_headers,
        request_body,
        response_code,
        response_body,
        script_logs,
        duration_ms,
        status,
        source,
        created_at: started,
    }
}
