//! Authorized, validated, transactional linked-primitive construction.

#![allow(clippy::missing_errors_doc)]

use crate::backend::{AuthorizedToolBackend, BackendError, BackendFuture};
use crate::llm::{ToolDefinition, ToolSchema};
use crate::policy::{
    AllowedOrigins, ApprovalRule, AuthorizedAction, Capability, FixedCost, Idempotency,
    OriginClass, PolicyError, PolicyReasonCode, PolicyTool, ResourceCost, ResourceEstimator, Risk,
};
use crate::types::{BoundedText, MAX_BODY_BYTES, MAX_OBSERVABLE_DETAIL_BYTES, ToolCallOutcome};
use libremetaverse_types::{UUID, compat::CancellationToken};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::time::Duration;

pub const BUILD_DRY_RUN_TOOL: &str = "build_object_dry_run";
pub const BUILD_EXECUTE_TOOL: &str = "build_object_execute";
pub const BUILD_CLEANUP_TOOL: &str = "build_object_cleanup";

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BuildLimits {
    pub max_prims: usize,
    pub approval_free_prims: usize,
    pub max_script_bytes: usize,
    pub max_dimension_millimeters: u32,
    pub max_distance_millimeters: u32,
    pub step_timeout: Duration,
    pub cleanup_timeout: Duration,
    pub max_observations: usize,
}

impl Default for BuildLimits {
    fn default() -> Self {
        Self {
            max_prims: 16,
            approval_free_prims: 4,
            max_script_bytes: 32 * 1024,
            max_dimension_millimeters: 10_000,
            max_distance_millimeters: 20_000,
            step_timeout: Duration::from_secs(20),
            cleanup_timeout: Duration::from_secs(20),
            max_observations: 256,
        }
    }
}

impl BuildLimits {
    fn valid(self) -> bool {
        (1..=256).contains(&self.max_prims)
            && self.approval_free_prims <= self.max_prims
            && (256..=64 * 1024).contains(&self.max_script_bytes)
            && (100..=64_000).contains(&self.max_dimension_millimeters)
            && (1_000..=256_000).contains(&self.max_distance_millimeters)
            && !self.step_timeout.is_zero()
            && self.step_timeout <= Duration::from_mins(2)
            && !self.cleanup_timeout.is_zero()
            && self.cleanup_timeout <= Duration::from_mins(2)
            && (16..=4_096).contains(&self.max_observations)
    }
}

#[derive(Clone, Copy, Debug, Deserialize, Serialize, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum BuildShape {
    Box,
    Cylinder,
    Prism,
    Sphere,
    Torus,
    Tube,
    Ring,
}

#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct BuildPrim {
    pub id: String,
    pub parent: Option<String>,
    pub shape: BuildShape,
    pub position_millimeters: [i32; 3],
    pub scale_millimeters: [u32; 3],
    pub rotation_degrees: [f64; 3],
    pub color_rgba: [f64; 4],
    pub material: String,
    pub texture_inventory_id: Option<String>,
    pub name: String,
    pub description: String,
    pub script: Option<String>,
}

#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct BuildPlan {
    pub version: u32,
    pub region_id: String,
    pub prims: Vec<BuildPrim>,
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct BuildValidation {
    pub prim_count: usize,
    pub link_count: usize,
    pub script_bytes: usize,
    pub inventory_operations: usize,
    pub approval_required: bool,
    pub estimated_max_seconds: u64,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PrimReceipt {
    pub plan_id: String,
    pub object_id: UUID,
    pub local_id: u32,
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct BuildReceipt {
    pub transaction_id: String,
    pub root_object_id: String,
    pub object_ids: Vec<String>,
    pub orphan_ids: Vec<String>,
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct BuildCleanupReceipt {
    pub transaction_id: String,
    pub deleted_object_ids: Vec<String>,
    pub orphan_ids: Vec<String>,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BuildObservationKind {
    Validated,
    Creating,
    Created,
    Configured,
    Linked,
    ScriptInserted,
    Cleaning,
    Cleaned,
    Orphaned,
    Completed,
    Cancelled,
    Failed,
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct BuildObservation {
    pub transaction_id: String,
    pub correlation_id: String,
    pub kind: BuildObservationKind,
    pub plan_id: Option<String>,
    pub object_id: Option<String>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BuildError {
    UnsafeLimits,
    InvalidPlan,
    UnsupportedVersion,
    TooManyPrims,
    InvalidIdentifier,
    InvalidGeometry,
    OutOfRegion,
    InvalidTopology,
    InvalidMaterial,
    InvalidTexture,
    InvalidScript,
    LandDenied,
    Busy,
    Cancelled,
    TimedOut,
    GridOperation,
    UnknownTransaction,
    CleanupIncomplete,
}

impl fmt::Display for BuildError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            Self::UnsafeLimits => "unsafe build limits",
            Self::InvalidPlan => "invalid build plan",
            Self::UnsupportedVersion => "unsupported build plan version",
            Self::TooManyPrims => "build exceeds primitive limit",
            Self::InvalidIdentifier => "invalid build identifier or text",
            Self::InvalidGeometry => "invalid or unbounded geometry",
            Self::OutOfRegion => "build is outside the current region or distance limit",
            Self::InvalidTopology => "build link topology is invalid",
            Self::InvalidMaterial => "unsupported material",
            Self::InvalidTexture => "texture is not an owned inventory UUID",
            Self::InvalidScript => "script failed local LSL validation",
            Self::LandDenied => "agent lacks build rights at the requested location",
            Self::Busy => "another build transaction is active",
            Self::Cancelled => "build transaction was cancelled",
            Self::TimedOut => "build step timed out",
            Self::GridOperation => "native grid operation failed",
            Self::UnknownTransaction => "unknown completed build transaction",
            Self::CleanupIncomplete => {
                "cleanup was incomplete; recoverable orphan IDs were recorded"
            }
        })
    }
}
impl std::error::Error for BuildError {}

pub type BuildFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, BuildError>> + Send + 'a>>;

pub trait BuildGrid: Send + Sync + 'static {
    fn validate_land(
        &self,
        region_id: &str,
        positions: &[[i32; 3]],
        cancellation: CancellationToken,
    ) -> BuildFuture<'_, ()>;
    fn texture_is_owned(
        &self,
        texture: UUID,
        cancellation: CancellationToken,
    ) -> BuildFuture<'_, bool>;
    fn create_prim(
        &self,
        transaction_id: &str,
        prim: &BuildPrim,
        cancellation: CancellationToken,
    ) -> BuildFuture<'_, PrimReceipt>;
    fn configure_prim(
        &self,
        receipt: &PrimReceipt,
        prim: &BuildPrim,
        cancellation: CancellationToken,
    ) -> BuildFuture<'_, ()>;
    fn link_prims(
        &self,
        parent: &PrimReceipt,
        child: &PrimReceipt,
        cancellation: CancellationToken,
    ) -> BuildFuture<'_, ()>;
    fn insert_script(
        &self,
        receipt: &PrimReceipt,
        source: &str,
        cancellation: CancellationToken,
    ) -> BuildFuture<'_, ()>;
    fn confirm_prim(
        &self,
        receipt: &PrimReceipt,
        cancellation: CancellationToken,
    ) -> BuildFuture<'_, ()>;
    fn delete_owned_prim(
        &self,
        receipt: &PrimReceipt,
        cancellation: CancellationToken,
    ) -> BuildFuture<'_, ()>;
}

/// Type-erased cancellation hook used by the operator control plane.
pub trait BuildControl: Send + Sync {
    fn cancel_build(&self, transaction_id: &str) -> bool;
    fn active_build(&self) -> Option<String>;
    fn build_orphans(&self) -> Vec<String>;
    fn build_progress(&self) -> Option<String>;
}

struct State {
    active: Option<String>,
    cancelled: BTreeSet<String>,
    observations: VecDeque<BuildObservation>,
    orphans: BTreeMap<String, Vec<PrimReceipt>>,
    completed: BTreeMap<String, Vec<PrimReceipt>>,
}
pub struct BuildService<G: BuildGrid> {
    grid: Arc<G>,
    limits: BuildLimits,
    state: Mutex<State>,
}

impl<G: BuildGrid> BuildService<G> {
    pub fn new(grid: Arc<G>, limits: BuildLimits) -> Result<Self, BuildError> {
        if !limits.valid() {
            return Err(BuildError::UnsafeLimits);
        }
        Ok(Self {
            grid,
            limits,
            state: Mutex::new(State {
                active: None,
                cancelled: BTreeSet::new(),
                observations: VecDeque::new(),
                orphans: BTreeMap::new(),
                completed: BTreeMap::new(),
            }),
        })
    }
    #[must_use]
    pub fn observations(&self) -> Vec<BuildObservation> {
        lock(&self.state).observations.iter().cloned().collect()
    }
    #[must_use]
    pub fn orphan_ids(&self, transaction: &str) -> Vec<UUID> {
        lock(&self.state)
            .orphans
            .get(transaction)
            .map_or_else(Vec::new, |v| v.iter().map(|r| r.object_id).collect())
    }
    pub fn cancel(&self, transaction: &str) -> bool {
        let mut state = lock(&self.state);
        if state.active.as_deref() == Some(transaction) {
            state.cancelled.insert(transaction.to_owned());
            true
        } else {
            false
        }
    }
    fn observe(
        &self,
        transaction: &str,
        correlation: &str,
        kind: BuildObservationKind,
        plan: Option<&str>,
        object: Option<UUID>,
    ) {
        let mut state = lock(&self.state);
        if state.observations.len() == self.limits.max_observations {
            state.observations.pop_front();
        }
        state.observations.push_back(BuildObservation {
            transaction_id: transaction.to_owned(),
            correlation_id: correlation.to_owned(),
            kind,
            plan_id: plan.map(str::to_owned),
            object_id: object.map(|id| id.to_string()),
        });
    }
    fn cancelled(&self, transaction: &str, token: &CancellationToken) -> bool {
        token.is_cancellation_requested() || lock(&self.state).cancelled.contains(transaction)
    }
    pub fn validate(&self, plan: &BuildPlan) -> Result<BuildValidation, BuildError> {
        validate_plan(plan, self.limits)
    }
    pub async fn dry_run(
        &self,
        plan: &BuildPlan,
        cancellation: CancellationToken,
    ) -> Result<BuildValidation, BuildError> {
        if cancellation.is_cancellation_requested() {
            return Err(BuildError::Cancelled);
        }
        let validation = self.validate(plan)?;
        timed(
            self.limits.step_timeout,
            self.grid.validate_land(
                &plan.region_id,
                &plan
                    .prims
                    .iter()
                    .map(|p| p.position_millimeters)
                    .collect::<Vec<_>>(),
                cancellation.clone(),
            ),
        )
        .await?;
        for texture in plan
            .prims
            .iter()
            .filter_map(|p| p.texture_inventory_id.as_deref())
        {
            let id = UUID::new_with_string(texture.to_owned())
                .map_err(|_| BuildError::InvalidTexture)?;
            if !timed(
                self.limits.step_timeout,
                self.grid.texture_is_owned(id, cancellation.clone()),
            )
            .await?
            {
                return Err(BuildError::InvalidTexture);
            }
        }
        Ok(validation)
    }
    pub async fn execute(
        &self,
        transaction: &str,
        correlation: &str,
        plan: BuildPlan,
        cancellation: CancellationToken,
    ) -> Result<BuildReceipt, BuildError> {
        if transaction.is_empty() || transaction.len() > 128 {
            return Err(BuildError::InvalidIdentifier);
        }
        {
            let mut state = lock(&self.state);
            if state.active.is_some() || state.completed.len() >= self.limits.max_observations {
                return Err(BuildError::Busy);
            }
            state.active = Some(transaction.to_owned());
            state.cancelled.remove(transaction);
            state.orphans.remove(transaction);
        }
        let result = self
            .execute_inner(transaction, correlation, &plan, cancellation.clone())
            .await;
        let mut state = lock(&self.state);
        state.active = None;
        state.cancelled.remove(transaction);
        result
    }
    #[allow(clippy::too_many_lines)]
    async fn execute_inner(
        &self,
        transaction: &str,
        correlation: &str,
        plan: &BuildPlan,
        cancellation: CancellationToken,
    ) -> Result<BuildReceipt, BuildError> {
        self.dry_run(plan, cancellation.clone()).await?;
        self.observe(
            transaction,
            correlation,
            BuildObservationKind::Validated,
            None,
            None,
        );
        let mut made = Vec::new();
        let mut by_id = BTreeMap::new();
        let mutation = async {
            for prim in &plan.prims {
                if self.cancelled(transaction, &cancellation) {
                    return Err(BuildError::Cancelled);
                }
                self.observe(
                    transaction,
                    correlation,
                    BuildObservationKind::Creating,
                    Some(&prim.id),
                    None,
                );
                let receipt = timed(
                    self.limits.step_timeout,
                    self.grid
                        .create_prim(transaction, prim, cancellation.clone()),
                )
                .await?;
                made.push(receipt.clone());
                by_id.insert(prim.id.clone(), receipt.clone());
                self.observe(
                    transaction,
                    correlation,
                    BuildObservationKind::Created,
                    Some(&prim.id),
                    Some(receipt.object_id),
                );
                timed(
                    self.limits.step_timeout,
                    self.grid.confirm_prim(&receipt, cancellation.clone()),
                )
                .await?;
                timed(
                    self.limits.step_timeout,
                    self.grid
                        .configure_prim(&receipt, prim, cancellation.clone()),
                )
                .await?;
                timed(
                    self.limits.step_timeout,
                    self.grid.confirm_prim(&receipt, cancellation.clone()),
                )
                .await?;
                self.observe(
                    transaction,
                    correlation,
                    BuildObservationKind::Configured,
                    Some(&prim.id),
                    Some(receipt.object_id),
                );
                if let Some(source) = &prim.script {
                    timed(
                        self.limits.step_timeout,
                        self.grid
                            .insert_script(&receipt, source, cancellation.clone()),
                    )
                    .await?;
                    timed(
                        self.limits.step_timeout,
                        self.grid.confirm_prim(&receipt, cancellation.clone()),
                    )
                    .await?;
                    self.observe(
                        transaction,
                        correlation,
                        BuildObservationKind::ScriptInserted,
                        Some(&prim.id),
                        Some(receipt.object_id),
                    );
                }
            }
            for prim in &plan.prims {
                if let Some(parent_id) = &prim.parent {
                    let parent = by_id.get(parent_id).ok_or(BuildError::InvalidTopology)?;
                    let child = by_id.get(&prim.id).ok_or(BuildError::InvalidTopology)?;
                    timed(
                        self.limits.step_timeout,
                        self.grid.link_prims(parent, child, cancellation.clone()),
                    )
                    .await?;
                    timed(
                        self.limits.step_timeout,
                        self.grid.confirm_prim(child, cancellation.clone()),
                    )
                    .await?;
                    self.observe(
                        transaction,
                        correlation,
                        BuildObservationKind::Linked,
                        Some(&prim.id),
                        Some(child.object_id),
                    );
                }
            }
            Ok::<(), BuildError>(())
        }
        .await;
        if let Err(error) = mutation {
            let _ = self.cleanup(transaction, correlation, &made).await;
            self.observe(
                transaction,
                correlation,
                if error == BuildError::Cancelled {
                    BuildObservationKind::Cancelled
                } else {
                    BuildObservationKind::Failed
                },
                None,
                None,
            );
            return Err(if self.orphan_ids(transaction).is_empty() {
                error
            } else {
                BuildError::CleanupIncomplete
            });
        }
        let root = plan
            .prims
            .iter()
            .find(|p| p.parent.is_none())
            .and_then(|p| by_id.get(&p.id))
            .ok_or(BuildError::InvalidTopology)?;
        self.observe(
            transaction,
            correlation,
            BuildObservationKind::Completed,
            None,
            Some(root.object_id),
        );
        lock(&self.state)
            .completed
            .insert(transaction.to_owned(), made.clone());
        Ok(BuildReceipt {
            transaction_id: transaction.to_owned(),
            root_object_id: root.object_id.to_string(),
            object_ids: made.iter().map(|r| r.object_id.to_string()).collect(),
            orphan_ids: Vec::new(),
        })
    }
    pub async fn cleanup_completed(
        &self,
        transaction: &str,
        correlation: &str,
        cancellation: CancellationToken,
    ) -> Result<BuildCleanupReceipt, BuildError> {
        if transaction.is_empty() || transaction.len() > 128 {
            return Err(BuildError::InvalidIdentifier);
        }
        if cancellation.is_cancellation_requested() {
            return Err(BuildError::Cancelled);
        }
        let made = {
            let mut state = lock(&self.state);
            if state.active.is_some() {
                return Err(BuildError::Busy);
            }
            let made = state
                .completed
                .get(transaction)
                .cloned()
                .ok_or(BuildError::UnknownTransaction)?;
            state.active = Some(transaction.to_owned());
            made
        };
        let orphaned = self.cleanup(transaction, correlation, &made).await;
        let orphan_ids = orphaned
            .iter()
            .map(|receipt| receipt.object_id.to_string())
            .collect::<Vec<_>>();
        let orphan_set = orphaned
            .iter()
            .map(|receipt| receipt.object_id)
            .collect::<BTreeSet<_>>();
        let deleted_object_ids = made
            .iter()
            .filter(|receipt| !orphan_set.contains(&receipt.object_id))
            .map(|receipt| receipt.object_id.to_string())
            .collect();
        let mut state = lock(&self.state);
        state.active = None;
        state.cancelled.remove(transaction);
        if orphaned.is_empty() {
            state.completed.remove(transaction);
        } else {
            state.completed.insert(transaction.to_owned(), orphaned);
        }
        Ok(BuildCleanupReceipt {
            transaction_id: transaction.to_owned(),
            deleted_object_ids,
            orphan_ids,
        })
    }
    async fn cleanup(
        &self,
        transaction: &str,
        correlation: &str,
        made: &[PrimReceipt],
    ) -> Vec<PrimReceipt> {
        self.observe(
            transaction,
            correlation,
            BuildObservationKind::Cleaning,
            None,
            None,
        );
        let mut orphaned = Vec::new();
        for receipt in made.iter().rev() {
            if timed(
                self.limits.cleanup_timeout,
                self.grid
                    .delete_owned_prim(receipt, CancellationToken::default()),
            )
            .await
            .is_ok()
            {
                self.observe(
                    transaction,
                    correlation,
                    BuildObservationKind::Cleaned,
                    Some(&receipt.plan_id),
                    Some(receipt.object_id),
                );
            } else {
                orphaned.push(receipt.clone());
                self.observe(
                    transaction,
                    correlation,
                    BuildObservationKind::Orphaned,
                    Some(&receipt.plan_id),
                    Some(receipt.object_id),
                );
            }
        }
        let mut state = lock(&self.state);
        if orphaned.is_empty() {
            state.orphans.remove(transaction);
        } else {
            state
                .orphans
                .insert(transaction.to_owned(), orphaned.clone());
        }
        orphaned
    }
}

impl<G: BuildGrid> BuildControl for BuildService<G> {
    fn cancel_build(&self, transaction_id: &str) -> bool {
        self.cancel(transaction_id)
    }
    fn active_build(&self) -> Option<String> {
        lock(&self.state).active.clone()
    }
    fn build_orphans(&self) -> Vec<String> {
        lock(&self.state)
            .orphans
            .values()
            .flatten()
            .map(|receipt| receipt.object_id.to_string())
            .collect()
    }
    fn build_progress(&self) -> Option<String> {
        lock(&self.state)
            .observations
            .back()
            .map(|observation| format!("{:?}", observation.kind).to_ascii_lowercase())
    }
}

async fn timed<T>(duration: Duration, future: BuildFuture<'_, T>) -> Result<T, BuildError> {
    tokio::time::timeout(duration, future)
        .await
        .map_err(|_| BuildError::TimedOut)?
}
fn lock<T>(value: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
    value
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}

#[allow(clippy::too_many_lines)]
fn validate_plan(plan: &BuildPlan, limits: BuildLimits) -> Result<BuildValidation, BuildError> {
    if plan.version != 1 {
        return Err(BuildError::UnsupportedVersion);
    }
    if plan.region_id.is_empty() || plan.region_id.len() > 128 || plan.prims.is_empty() {
        return Err(BuildError::InvalidPlan);
    }
    if plan.prims.len() > limits.max_prims {
        return Err(BuildError::TooManyPrims);
    }
    let mut ids = BTreeSet::new();
    let mut script_bytes = 0usize;
    let mut inventory = 0usize;
    for prim in &plan.prims {
        if prim.id.is_empty()
            || prim.id.len() > 64
            || !prim
                .id
                .bytes()
                .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
            || !ids.insert(prim.id.clone())
            || prim.name.is_empty()
            || prim.name.len() > 63
            || prim.description.len() > 127
        {
            return Err(BuildError::InvalidIdentifier);
        }
        if prim
            .scale_millimeters
            .iter()
            .any(|v| *v == 0 || *v > limits.max_dimension_millimeters)
            || prim
                .position_millimeters
                .iter()
                .any(|v| v.unsigned_abs() > limits.max_distance_millimeters)
            || prim
                .rotation_degrees
                .iter()
                .any(|v| !v.is_finite() || v.abs() > 360.0)
            || prim
                .color_rgba
                .iter()
                .any(|v| !v.is_finite() || !(0.0..=1.0).contains(v))
        {
            return Err(BuildError::InvalidGeometry);
        }
        if !matches!(
            prim.material.as_str(),
            "wood" | "metal" | "glass" | "stone" | "plastic" | "rubber" | "flesh"
        ) {
            return Err(BuildError::InvalidMaterial);
        }
        if let Some(texture) = &prim.texture_inventory_id {
            UUID::new_with_string(texture.clone()).map_err(|_| BuildError::InvalidTexture)?;
            inventory += 1;
        }
        if let Some(script) = &prim.script {
            let candidate = crate::script_delivery::GeneratedScript {
                name: prim.name.clone(),
                description: if prim.description.is_empty() {
                    "Object script".to_owned()
                } else {
                    prim.description.clone()
                },
                source: script.clone(),
            };
            if crate::script_delivery::validate_script(&candidate, limits.max_script_bytes).is_err()
            {
                return Err(BuildError::InvalidScript);
            }
            script_bytes += script.len();
            inventory += 1;
        }
    }
    let roots = plan.prims.iter().filter(|p| p.parent.is_none()).count();
    if roots != 1 {
        return Err(BuildError::InvalidTopology);
    }
    for prim in &plan.prims {
        if let Some(parent) = &prim.parent {
            if parent == &prim.id || !ids.contains(parent) {
                return Err(BuildError::InvalidTopology);
            }
            let mut cursor = Some(parent.as_str());
            let mut seen = BTreeSet::new();
            while let Some(id) = cursor {
                if !seen.insert(id) {
                    return Err(BuildError::InvalidTopology);
                }
                cursor = plan
                    .prims
                    .iter()
                    .find(|p| p.id == id)
                    .and_then(|p| p.parent.as_deref());
            }
        }
    }
    Ok(BuildValidation {
        prim_count: plan.prims.len(),
        link_count: plan.prims.len() - 1,
        script_bytes,
        inventory_operations: inventory,
        approval_required: plan.prims.len() > limits.approval_free_prims,
        estimated_max_seconds: u64::try_from(plan.prims.len())
            .unwrap_or(u64::MAX)
            .saturating_mul(6)
            .saturating_sub(1)
            .saturating_mul(limits.step_timeout.as_secs().max(1)),
    })
}

#[derive(Clone)]
struct BuildEstimator {
    limits: BuildLimits,
}
impl ResourceEstimator for BuildEstimator {
    fn estimate(&self, arguments: &Value) -> Result<ResourceCost, PolicyReasonCode> {
        let plan: BuildPlan = serde_json::from_value(
            arguments
                .get("plan")
                .cloned()
                .ok_or(PolicyReasonCode::InvalidArguments)?,
        )
        .map_err(|_| PolicyReasonCode::InvalidArguments)?;
        let valid =
            validate_plan(&plan, self.limits).map_err(|_| PolicyReasonCode::InvalidArguments)?;
        Ok(ResourceCost {
            tool_calls: 1,
            inventory_operations: valid.inventory_operations as u64,
            build_prims: valid.prim_count as u64,
            ..ResourceCost::default()
        })
    }
}

#[allow(clippy::too_many_lines)]
pub fn build_policy_tools(limits: BuildLimits) -> Result<Vec<PolicyTool>, PolicyError> {
    if !limits.valid() {
        return Err(PolicyError::InvalidRegistration);
    }
    let vector = |length| ToolSchema::Array {
        items: Box::new(ToolSchema::Number),
        max_items: length,
    };
    let prim_properties = BTreeMap::from([
        ("id".into(), ToolSchema::String),
        (
            "parent".into(),
            ToolSchema::Nullable(Box::new(ToolSchema::String)),
        ),
        ("shape".into(), ToolSchema::String),
        ("position_millimeters".into(), vector(3)),
        ("scale_millimeters".into(), vector(3)),
        ("rotation_degrees".into(), vector(3)),
        ("color_rgba".into(), vector(4)),
        ("material".into(), ToolSchema::String),
        (
            "texture_inventory_id".into(),
            ToolSchema::Nullable(Box::new(ToolSchema::String)),
        ),
        ("name".into(), ToolSchema::String),
        ("description".into(), ToolSchema::String),
        (
            "script".into(),
            ToolSchema::Nullable(Box::new(ToolSchema::String)),
        ),
    ]);
    let prim_required = BTreeSet::from([
        "id".into(),
        "shape".into(),
        "position_millimeters".into(),
        "scale_millimeters".into(),
        "rotation_degrees".into(),
        "color_rgba".into(),
        "material".into(),
        "name".into(),
        "description".into(),
    ]);
    let plan_schema = ToolSchema::Object {
        properties: BTreeMap::from([
            ("version".into(), ToolSchema::Integer),
            ("region_id".into(), ToolSchema::String),
            (
                "prims".into(),
                ToolSchema::Array {
                    items: Box::new(ToolSchema::Object {
                        properties: prim_properties,
                        required: prim_required,
                        additional_properties: false,
                    }),
                    max_items: limits.max_prims,
                },
            ),
        ]),
        required: BTreeSet::from(["version".into(), "region_id".into(), "prims".into()]),
        additional_properties: false,
    };
    let plan = ToolSchema::Object {
        properties: BTreeMap::from([("plan".into(), plan_schema)]),
        required: BTreeSet::from(["plan".into()]),
        additional_properties: false,
    };
    let cleanup = ToolSchema::Object {
        properties: BTreeMap::from([("transaction_id".into(), ToolSchema::String)]),
        required: BTreeSet::from(["transaction_id".into()]),
        additional_properties: false,
    };
    let origins = || AllowedOrigins::new([OriginClass::AuthorizedIm, OriginClass::LocalOperator]);
    let max = ResourceCost {
        tool_calls: 1,
        inventory_operations: (limits.max_prims * 2) as u64,
        build_prims: limits.max_prims as u64,
        ..ResourceCost::default()
    };
    let threshold = ResourceCost {
        tool_calls: 1,
        inventory_operations: max.inventory_operations,
        build_prims: limits.approval_free_prims as u64,
        ..ResourceCost::default()
    };
    Ok(vec![
        PolicyTool::new(
            ToolDefinition {
                name: BoundedText::new("tool.name", BUILD_DRY_RUN_TOOL)?,
                description: BoundedText::new(
                    "tool.description",
                    "Validate a versioned bounded linked-primitive build and return its exact resource estimate without mutation",
                )?,
                schema: plan.clone(),
                mutating: false,
            },
            Capability::Informational,
            Risk::ReadOnly,
            origins()?,
            max,
            Idempotency::Idempotent,
            ApprovalRule::Never,
            false,
            Arc::new(BuildEstimator { limits }),
        )?,
        PolicyTool::new(
            ToolDefinition {
                name: BoundedText::new("tool.name", BUILD_EXECUTE_TOOL)?,
                description: BoundedText::new(
                    "tool.description",
                    "Execute one validated transactional linked-primitive build; larger plans receive an autonomous safety review",
                )?,
                schema: plan,
                mutating: true,
            },
            Capability::Build,
            Risk::Build,
            origins()?,
            max,
            Idempotency::NonIdempotent,
            ApprovalRule::WhenExceeds(threshold),
            false,
            Arc::new(BuildEstimator { limits }),
        )?,
        PolicyTool::new(
            ToolDefinition {
                name: BoundedText::new("tool.name", BUILD_CLEANUP_TOOL)?,
                description: BoundedText::new(
                    "tool.description",
                    "Delete only objects retained from one completed build transaction and report any recoverable failures",
                )?,
                schema: cleanup,
                mutating: true,
            },
            Capability::Build,
            Risk::Build,
            origins()?,
            ResourceCost::one_call(),
            Idempotency::NonIdempotent,
            ApprovalRule::Never,
            false,
            Arc::new(FixedCost(ResourceCost::one_call())),
        )?,
    ])
}

pub struct BuildToolBackend<G: BuildGrid> {
    service: Arc<BuildService<G>>,
}
impl<G: BuildGrid> BuildToolBackend<G> {
    #[must_use]
    pub fn new(service: Arc<BuildService<G>>) -> Self {
        Self { service }
    }
}
impl<G: BuildGrid> AuthorizedToolBackend for BuildToolBackend<G> {
    fn apply(
        &self,
        action: AuthorizedAction,
        cancellation: CancellationToken,
    ) -> BackendFuture<'_, Result<ToolCallOutcome, BackendError>> {
        Box::pin(async move {
            #[derive(Deserialize)]
            #[serde(deny_unknown_fields)]
            struct BuildArgs {
                plan: BuildPlan,
            }
            #[derive(Deserialize)]
            #[serde(deny_unknown_fields)]
            struct CleanupArgs {
                transaction_id: String,
            }
            let call_id = action.call().call_id.clone();
            let transaction = format!("build-{}", action.authorization_id());
            let result = match action.call().name.as_str() {
                BUILD_DRY_RUN_TOOL => {
                    match serde_json::from_str::<BuildArgs>(action.call().arguments_json.as_str()) {
                        Ok(args) => self
                            .service
                            .dry_run(&args.plan, cancellation)
                            .await
                            .and_then(|value| {
                                serde_json::to_string(&value).map_err(|_| BuildError::InvalidPlan)
                            }),
                        Err(_) => Err(BuildError::InvalidPlan),
                    }
                }
                BUILD_EXECUTE_TOOL => {
                    match serde_json::from_str::<BuildArgs>(action.call().arguments_json.as_str()) {
                        Ok(args) => self
                            .service
                            .execute(&transaction, call_id.as_str(), args.plan, cancellation)
                            .await
                            .and_then(|value| {
                                serde_json::to_string(&value).map_err(|_| BuildError::InvalidPlan)
                            }),
                        Err(_) => Err(BuildError::InvalidPlan),
                    }
                }
                BUILD_CLEANUP_TOOL => {
                    match serde_json::from_str::<CleanupArgs>(action.call().arguments_json.as_str())
                    {
                        Ok(args) => self
                            .service
                            .cleanup_completed(&args.transaction_id, call_id.as_str(), cancellation)
                            .await
                            .and_then(|value| {
                                serde_json::to_string(&value).map_err(|_| BuildError::InvalidPlan)
                            }),
                        Err(_) => Err(BuildError::InvalidPlan),
                    }
                }
                _ => Err(BuildError::InvalidPlan),
            };
            Ok(match result {
                Ok(value) => ToolCallOutcome::Completed {
                    call_id,
                    result: BoundedText::<MAX_BODY_BYTES>::new("build.result", value).map_err(
                        |_| BackendError::Operation {
                            operation: "bounded build result",
                        },
                    )?,
                },
                Err(error) => {
                    let orphans = self.service.orphan_ids(&transaction);
                    let reason = if orphans.is_empty() {
                        error.to_string()
                    } else {
                        format!(
                            "{error}; orphan_ids={}",
                            orphans
                                .iter()
                                .map(ToString::to_string)
                                .collect::<Vec<_>>()
                                .join(",")
                        )
                    };
                    ToolCallOutcome::Rejected {
                        call_id,
                        reason: BoundedText::<MAX_OBSERVABLE_DETAIL_BYTES>::new(
                            "build.rejection",
                            reason,
                        )
                        .map_err(|_| BackendError::Operation {
                            operation: "bounded build rejection",
                        })?,
                    }
                }
            })
        })
    }
}

/// Live adapter over the original compatibility surface. All orchestration,
/// policy, validation, correlation, and cleanup ownership remain in `MetaCrate`.
#[cfg(feature = "live-grid")]
pub struct LibremetaverseBuildGrid {
    client: libremetaverse::GridClient,
    agent: Arc<libremetaverse::AgentManager>,
    objects: libremetaverse::ObjectManager,
    inventory: libremetaverse::InventoryManager,
    resolved_textures: Mutex<BTreeMap<UUID, UUID>>,
}

#[cfg(feature = "live-grid")]
#[allow(clippy::cast_precision_loss)]
impl LibremetaverseBuildGrid {
    #[must_use]
    pub fn new(owner: &crate::backend::LibremetaverseClientOwner) -> Self {
        Self {
            client: owner.client().clone(),
            agent: owner.agent(),
            objects: owner.client().objects(),
            inventory: owner.client().inventory(),
            resolved_textures: Mutex::new(BTreeMap::new()),
        }
    }
    fn simulator(&self) -> Result<libremetaverse::Simulator, BuildError> {
        self.client
            .network()
            .current_sim()
            .ok_or(BuildError::GridOperation)
    }
    fn world_position(&self, prim: &BuildPrim) -> libremetaverse_types::Vector3 {
        let base = self.agent.sim_position();
        libremetaverse_types::Vector3 {
            x: base.x + prim.position_millimeters[0] as f32 / 1000.0,
            y: base.y + prim.position_millimeters[1] as f32 / 1000.0,
            z: base.z + prim.position_millimeters[2] as f32 / 1000.0,
        }
    }
}

#[cfg(feature = "live-grid")]
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
impl BuildGrid for LibremetaverseBuildGrid {
    fn validate_land(
        &self,
        region_id: &str,
        positions: &[[i32; 3]],
        cancellation: CancellationToken,
    ) -> BuildFuture<'_, ()> {
        let region = region_id.to_owned();
        let positions = positions.to_vec();
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err(BuildError::Cancelled);
            }
            let simulator = self.simulator()?;
            if simulator.region_id.to_string() != region {
                return Err(BuildError::OutOfRegion);
            }
            let base = self.agent.sim_position();
            let (region_size_x, region_size_y) = simulator.region_size();
            let reported_bounds_contain_agent =
                base.x <= region_size_x as f32 && base.y <= region_size_y as f32;
            for offset in positions {
                let point = libremetaverse_types::Vector3 {
                    x: base.x + offset[0] as f32 / 1000.0,
                    y: base.y + offset[1] as f32 / 1000.0,
                    z: base.z + offset[2] as f32 / 1000.0,
                };
                if point.x < 0.0
                    || point.y < 0.0
                    || (reported_bounds_contain_agent
                        && (point.x > region_size_x as f32 || point.y > region_size_y as f32))
                {
                    return Err(BuildError::OutOfRegion);
                }
                let parcels = simulator
                    .parcels
                    .read()
                    .unwrap_or_else(std::sync::PoisonError::into_inner);
                let parcel = parcels
                    .values()
                    .filter(|parcel| {
                        point.x >= parcel.aabb_min.x
                            && point.x <= parcel.aabb_max.x
                            && point.y >= parcel.aabb_min.y
                            && point.y <= parcel.aabb_max.y
                    })
                    .min_by_key(|parcel| parcel.local_id);
                // Some OpenSim varregions expose only the legacy 256 m parcel
                // cache; the server remains authoritative for the bounded rez.
                let allowed = parcel.map_or(!reported_bounds_contain_agent, |parcel| {
                    parcel.owner_id == self.agent.agent_id()
                        || parcel.flags.0 & libremetaverse::ParcelFlags::CREATE_OBJECTS.0 != 0
                        || (parcel.group_id != UUID::zero()
                            && parcel.group_id == self.agent.active_group()
                            && parcel.flags.0 & libremetaverse::ParcelFlags::CREATE_GROUP_OBJECTS.0
                                != 0)
                });
                if !allowed {
                    return Err(BuildError::LandDenied);
                }
            }
            Ok(())
        })
    }
    fn texture_is_owned(
        &self,
        texture: UUID,
        cancellation: CancellationToken,
    ) -> BuildFuture<'_, bool> {
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err(BuildError::Cancelled);
            }
            let store = self.inventory.store().ok_or(BuildError::InvalidTexture)?;
            let node = store
                .get_node_for(texture)
                .map_err(|_| BuildError::InvalidTexture)?;
            let object = node.data().ok_or(BuildError::InvalidTexture)?;
            let item = object.inventory_item().ok_or(BuildError::InvalidTexture)?;
            if item.asset_type() != libremetaverse_types::AssetType::Texture
                || object.inventory_base().owner_id() != self.agent.agent_id()
            {
                return Ok(false);
            }
            lock(&self.resolved_textures).insert(texture, item.asset_uuid());
            Ok(true)
        })
    }
    fn create_prim(
        &self,
        _: &str,
        prim: &BuildPrim,
        cancellation: CancellationToken,
    ) -> BuildFuture<'_, PrimReceipt> {
        let prim = prim.clone();
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err(BuildError::Cancelled);
            }
            let simulator = self.simulator()?;
            let target = self.world_position(&prim);
            let z_tolerance = prim.scale_millimeters[2] as f32 / 2_000.0 + 0.1;
            let (sender, receiver) = tokio::sync::oneshot::channel();
            let sender = Arc::new(Mutex::new(Some(sender)));
            let callback_sender = Arc::clone(&sender);
            let _subscription = self.objects.subscribe_object_update(Arc::new(move |event| {
                let found = event.prim();
                if event.is_new()
                    && (found.position.x - target.x).abs() < 0.05
                    && (found.position.y - target.y).abs() < 0.05
                    && (found.position.z - target.z).abs() <= z_tolerance
                    && let Some(sender) = lock(&callback_sender).take()
                {
                    let _ = sender.send(PrimReceipt {
                        plan_id: prim.id.clone(),
                        object_id: found.id,
                        local_id: found.local_id,
                    });
                }
            }));
            let shape = match prim.shape {
                BuildShape::Box => libremetaverse_types::PrimType::Box,
                BuildShape::Cylinder => libremetaverse_types::PrimType::Cylinder,
                BuildShape::Prism => libremetaverse_types::PrimType::Prism,
                BuildShape::Sphere => libremetaverse_types::PrimType::Sphere,
                BuildShape::Torus => libremetaverse_types::PrimType::Torus,
                BuildShape::Tube => libremetaverse_types::PrimType::Tube,
                BuildShape::Ring => libremetaverse_types::PrimType::Ring,
            };
            let construction = libremetaverse::ObjectManager::build_basic_shape(shape)
                .map_err(|_| BuildError::GridOperation)?;
            let scale = libremetaverse_types::Vector3 {
                x: prim.scale_millimeters[0] as f32 / 1000.0,
                y: prim.scale_millimeters[1] as f32 / 1000.0,
                z: prim.scale_millimeters[2] as f32 / 1000.0,
            };
            let rotation =
                libremetaverse_types::Quaternion::create_from_eulers_with_single_single_single(
                    prim.rotation_degrees[0].to_radians() as f32,
                    prim.rotation_degrees[1].to_radians() as f32,
                    prim.rotation_degrees[2].to_radians() as f32,
                )
                .map_err(|_| BuildError::InvalidGeometry)?;
            self.objects
                .add_prim_with_simulator_construction_data_uuid_vector3_vector3_quaternion(
                    simulator,
                    construction,
                    self.agent.active_group(),
                    target,
                    scale,
                    rotation,
                )
                .map_err(|_| BuildError::GridOperation)?;
            receiver.await.map_err(|_| BuildError::GridOperation)
        })
    }
    fn configure_prim(
        &self,
        receipt: &PrimReceipt,
        prim: &BuildPrim,
        cancellation: CancellationToken,
    ) -> BuildFuture<'_, ()> {
        let receipt = receipt.clone();
        let prim = prim.clone();
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err(BuildError::Cancelled);
            }
            let simulator = self.simulator()?;
            self.objects
                .set_name(simulator.clone(), receipt.local_id, prim.name.clone())
                .map_err(|_| BuildError::GridOperation)?;
            self.objects
                .set_description(
                    simulator.clone(),
                    receipt.local_id,
                    prim.description.clone(),
                )
                .map_err(|_| BuildError::GridOperation)?;
            let material = match prim.material.as_str() {
                "wood" => libremetaverse_types::Material::Wood,
                "metal" => libremetaverse_types::Material::Metal,
                "glass" => libremetaverse_types::Material::Glass,
                "stone" => libremetaverse_types::Material::Stone,
                "plastic" => libremetaverse_types::Material::Plastic,
                "rubber" => libremetaverse_types::Material::Rubber,
                "flesh" => libremetaverse_types::Material::Flesh,
                _ => return Err(BuildError::InvalidMaterial),
            };
            self.objects
                .set_material(simulator.clone(), receipt.local_id, material)
                .map_err(|_| BuildError::GridOperation)?;
            let texture = prim
                .texture_inventory_id
                .as_ref()
                .and_then(|id| UUID::new_with_string(id.clone()).ok())
                .and_then(|id| lock(&self.resolved_textures).get(&id).copied())
                .unwrap_or_else(libremetaverse::PrimitiveTextureEntry::white_texture);
            let mut entry = libremetaverse::PrimitiveTextureEntry::new_with_uuid(texture)
                .map_err(|_| BuildError::GridOperation)?;
            if let Some(face) = entry.default_texture.as_mut() {
                face.set_rgba(libremetaverse_types::Color4 {
                    r: prim.color_rgba[0] as f32,
                    g: prim.color_rgba[1] as f32,
                    b: prim.color_rgba[2] as f32,
                    a: prim.color_rgba[3] as f32,
                });
            }
            self.objects
                .set_textures_with_simulator_u_int32_texture_entry(
                    simulator.clone(),
                    receipt.local_id,
                    entry,
                )
                .map_err(|_| BuildError::GridOperation)?;
            for who in [
                libremetaverse::PermissionWho::EVERYONE,
                libremetaverse::PermissionWho::GROUP,
                libremetaverse::PermissionWho::NEXT_OWNER,
            ] {
                self.objects
                    .set_permissions(
                        simulator.clone(),
                        vec![receipt.local_id],
                        who,
                        libremetaverse::PermissionMask::ALL,
                        false,
                    )
                    .map_err(|_| BuildError::GridOperation)?;
            }
            Ok(())
        })
    }
    fn link_prims(
        &self,
        parent: &PrimReceipt,
        child: &PrimReceipt,
        cancellation: CancellationToken,
    ) -> BuildFuture<'_, ()> {
        let parent = parent.clone();
        let child = child.clone();
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err(BuildError::Cancelled);
            }
            self.objects
                .link_prims(self.simulator()?, vec![parent.local_id, child.local_id])
                .map_err(|_| BuildError::GridOperation)
        })
    }
    fn insert_script(
        &self,
        receipt: &PrimReceipt,
        source: &str,
        cancellation: CancellationToken,
    ) -> BuildFuture<'_, ()> {
        let receipt = receipt.clone();
        let source = source.as_bytes().to_vec();
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err(BuildError::Cancelled);
            }
            let item_id = UUID::random().map_err(|_| BuildError::GridOperation)?;
            let mut item = libremetaverse::InventoryItem::new_with_inventory_type_uuid(
                libremetaverse_types::InventoryType::LSL,
                item_id,
            )
            .map_err(|_| BuildError::GridOperation)?;
            item.set_description("Validated MetaCrate object script".into());
            item.set_asset_type(libremetaverse_types::AssetType::Script);
            item.set_permissions(libremetaverse::Permissions::full_permissions());
            let task_id = self
                .inventory
                .update_task_inventory(receipt.local_id, item, Some(self.simulator()?), Some(false))
                .map_err(|_| BuildError::GridOperation)?;
            let result = self
                .inventory
                .request_update_script_task(
                    source,
                    item_id,
                    receipt.object_id,
                    true,
                    true,
                    Some(cancellation),
                    None,
                )
                .await
                .map_err(|_| BuildError::GridOperation)?;
            if !result.0 || task_id == UUID::zero() {
                return Err(BuildError::GridOperation);
            }
            Ok(())
        })
    }
    fn confirm_prim(
        &self,
        receipt: &PrimReceipt,
        cancellation: CancellationToken,
    ) -> BuildFuture<'_, ()> {
        let receipt = receipt.clone();
        Box::pin(async move {
            if cancellation.is_cancellation_requested() {
                return Err(BuildError::Cancelled);
            }
            let primitive = self
                .objects
                .get_primitive(
                    self.simulator()?,
                    receipt.local_id,
                    receipt.object_id,
                    false,
                )
                .map_err(|_| BuildError::GridOperation)?;
            if primitive.id != receipt.object_id {
                return Err(BuildError::GridOperation);
            }
            Ok(())
        })
    }
    fn delete_owned_prim(
        &self,
        receipt: &PrimReceipt,
        _: CancellationToken,
    ) -> BuildFuture<'_, ()> {
        let receipt = receipt.clone();
        Box::pin(async move {
            self.inventory
                .request_de_rez_to_inventory_with_u_int32_de_rez_destination_uuid_uuid(
                    receipt.local_id,
                    libremetaverse::DeRezDestination::TrashFolder,
                    UUID::zero(),
                    UUID::random().map_err(|_| BuildError::GridOperation)?,
                )
                .map_err(|_| BuildError::GridOperation)
        })
    }
}
