// rust-style-allow: large-file - tool inspection keeps local manifest
// resolution, fixture fallback, provenance, and JSON projection in one
// read-only diagnostic surface.
use std::collections::BTreeMap;
use std::fs;
use std::path::{Component, Path, PathBuf};

use runx_contracts::tools::{
    JsonPayload, RuntimeCommand, ToolBuildStatus, ToolInput, ToolInspectImportedFrom,
    ToolInspectOrigin, ToolInspectProvenance, ToolInspectReport, ToolInspectResult,
    ToolInspectRunx,
};

use runx_contracts::sha256_hex;

use super::error::ToolCatalogError;
use super::search::{FixtureTool, fixture_catalog_allowed, fixture_tool};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ToolInspectOptions {
    pub root: PathBuf,
    pub tool_ref: String,
    pub source: Option<String>,
    pub search_from_directory: PathBuf,
    pub tool_roots: Vec<PathBuf>,
    pub fixture_catalog_enabled: bool,
    pub allow_explicit_manifest_path: bool,
}

#[derive(Clone, Debug)]
pub struct LocalToolResolution {
    pub manifest_path: PathBuf,
    pub tool: runx_parser::ValidatedTool,
}

pub fn inspect_tool(options: &ToolInspectOptions) -> Result<ToolInspectReport, ToolCatalogError> {
    match resolve_local_manifest(options) {
        Ok(manifest_path) => {
            let tool = read_local_tool_manifest(&manifest_path)?;
            return Ok(ToolInspectReport {
                status: ToolBuildStatus::Success,
                tool: inspect_local_tool(options, &manifest_path, tool)?,
            });
        }
        Err(ToolCatalogError::NotFound(_)) => {}
        Err(error) => return Err(error),
    }

    if let Some(tool) = resolve_fixture_tool(options) {
        return Ok(ToolInspectReport {
            status: ToolBuildStatus::Success,
            tool: inspect_fixture_tool(&options.tool_ref, &tool, &options.root),
        });
    }

    Err(ToolCatalogError::NotFound(format!(
        "Tool '{}' was not found in configured tool roots.",
        options.tool_ref
    )))
}

pub fn resolve_local_tool(
    options: &ToolInspectOptions,
) -> Result<LocalToolResolution, ToolCatalogError> {
    let manifest_path = resolve_local_manifest(options)?;
    let tool = read_local_tool_manifest(&manifest_path)?;
    Ok(LocalToolResolution {
        manifest_path,
        tool,
    })
}

fn resolve_fixture_tool(options: &ToolInspectOptions) -> Option<FixtureTool> {
    let normalized_source = options
        .source
        .as_deref()
        .map(|source| source.trim().to_ascii_lowercase());

    if !fixture_catalog_allowed(
        options.fixture_catalog_enabled,
        normalized_source.as_deref(),
    ) {
        return None;
    }
    fixture_tool(&options.tool_ref)
}

fn read_local_tool_manifest(
    manifest_path: &Path,
) -> Result<runx_parser::ValidatedTool, ToolCatalogError> {
    let manifest_source = fs::read_to_string(manifest_path)
        .map_err(|error| ToolCatalogError::io("reading tool manifest", manifest_path, error))?;
    let raw = runx_parser::parse_tool_manifest_json(&manifest_source).map_err(|error| {
        ToolCatalogError::InvalidManifest {
            path: manifest_path.to_path_buf(),
            message: error.to_string(),
        }
    })?;
    runx_parser::validate_tool_manifest(raw).map_err(|error| ToolCatalogError::InvalidManifest {
        path: manifest_path.to_path_buf(),
        message: error.to_string(),
    })
}

fn inspect_local_tool(
    options: &ToolInspectOptions,
    manifest_path: &Path,
    tool: runx_parser::ValidatedTool,
) -> Result<ToolInspectResult, ToolCatalogError> {
    Ok(ToolInspectResult {
        tool_ref: options.tool_ref.clone(),
        name: tool.name,
        description: tool.description,
        execution_source_type: tool.source.source_type.as_str().to_owned(),
        inputs: convert_inputs(tool.inputs)?,
        scopes: tool.scopes,
        mutating: tool.mutating,
        runtime: convert_optional_runtime(tool.runtime)?,
        risk: convert_optional_json(tool.risk)?,
        runx: convert_optional_object(tool.runx)?,
        reference_path: display_path(manifest_path),
        skill_directory: manifest_path
            .parent()
            .map(display_path)
            .unwrap_or_else(|| ".".to_owned()),
        provenance: local_provenance(),
    })
}

fn local_provenance() -> ToolInspectProvenance {
    ToolInspectProvenance {
        origin: ToolInspectOrigin::Local,
        source: None,
        source_label: None,
        source_type: None,
        namespace: None,
        external_name: None,
        catalog_ref: None,
        tool_id: None,
        tags: None,
    }
}

fn inspect_fixture_tool(tool_ref: &str, tool: &FixtureTool, root: &Path) -> ToolInspectResult {
    ToolInspectResult {
        tool_ref: tool_ref.to_owned(),
        name: tool.qualified_name(),
        description: tool.description.map(str::to_owned),
        execution_source_type: "catalog".to_owned(),
        inputs: fixture_inputs(tool),
        scopes: vec![tool.qualified_name()],
        mutating: None,
        runtime: None,
        risk: None,
        runx: Some(imported_runx(tool)),
        reference_path: format!("catalog:{}:{}", tool.source, tool.qualified_name()),
        skill_directory: display_path(root),
        provenance: ToolInspectProvenance {
            origin: ToolInspectOrigin::Imported,
            source: Some(tool.source.to_owned()),
            source_label: Some(tool.source_label.to_owned()),
            source_type: Some(tool.source_type.to_owned()),
            namespace: Some(tool.namespace.to_owned()),
            external_name: Some(tool.external_name.to_owned()),
            catalog_ref: Some(tool.catalog_ref()),
            tool_id: Some(tool.tool_id()),
            tags: Some(tool.tags.iter().map(|tag| (*tag).to_owned()).collect()),
        },
    }
}

fn fixture_inputs(tool: &FixtureTool) -> BTreeMap<String, ToolInput> {
    tool.inputs
        .iter()
        .map(|input| {
            (
                input.name.to_owned(),
                ToolInput {
                    input_type: input.input_type.to_owned(),
                    required: input.required,
                    description: input.description.map(str::to_owned),
                    default: None,
                    artifact: None,
                },
            )
        })
        .collect()
}

fn imported_runx(tool: &FixtureTool) -> ToolInspectRunx {
    let digest_payload = format!(
        r#"{{"source":"{}","namespace":"{}","external_name":"{}","source_type":"{}"}}"#,
        tool.source, tool.namespace, tool.external_name, tool.source_type
    );
    ToolInspectRunx::Imported {
        imported_from: ToolInspectImportedFrom {
            source: tool.source.to_owned(),
            source_label: tool.source_label.to_owned(),
            source_type: tool.source_type.to_owned(),
            namespace: tool.namespace.to_owned(),
            external_name: tool.external_name.to_owned(),
            digest: sha256_hex(digest_payload.as_bytes()),
        },
    }
}

fn resolve_local_manifest(options: &ToolInspectOptions) -> Result<PathBuf, ToolCatalogError> {
    if options.allow_explicit_manifest_path
        && let Some(path) =
            explicit_manifest_path(&options.tool_ref, &options.search_from_directory)?
    {
        return Ok(path);
    }

    let segments = tool_ref_segments(&options.tool_ref)?;
    for root in resolve_tool_roots(options) {
        let manifest = root
            .join(segments.iter().collect::<PathBuf>())
            .join("manifest.json");
        if manifest.exists() {
            return Ok(manifest);
        }
    }

    Err(ToolCatalogError::NotFound(format!(
        "Tool '{}' was not found in configured tool roots.",
        options.tool_ref
    )))
}

fn explicit_manifest_path(
    tool_ref: &str,
    search_from_directory: &Path,
) -> Result<Option<PathBuf>, ToolCatalogError> {
    let candidate = Path::new(tool_ref);
    if candidate.is_absolute()
        || candidate
            .components()
            .any(|component| matches!(component, Component::ParentDir))
    {
        return Err(ToolCatalogError::InvalidRequest(
            "Explicit tool manifest paths must be relative and must not contain '..'.".to_owned(),
        ));
    }
    let resolved = search_from_directory.join(candidate);
    if resolved.is_file() {
        return Ok(Some(resolved));
    }
    let manifest = resolved.join("manifest.json");
    if manifest.is_file() {
        return Ok(Some(manifest));
    }
    Ok(None)
}

fn tool_ref_segments(tool_ref: &str) -> Result<Vec<&str>, ToolCatalogError> {
    let segments = tool_ref
        .split('.')
        .filter(|segment| !segment.is_empty())
        .collect::<Vec<_>>();
    if segments.len() < 2 {
        return Err(ToolCatalogError::InvalidRequest(format!(
            "Tool '{tool_ref}' must include a namespace, for example fs.read."
        )));
    }
    Ok(segments)
}

fn resolve_tool_roots(options: &ToolInspectOptions) -> Vec<PathBuf> {
    let mut roots = Vec::new();
    push_existing_dirs(&mut roots, options.tool_roots.iter().cloned());
    push_existing_dirs(&mut roots, [options.root.join("tools")]);
    let root = options.root.as_path();
    let mut current = options.search_from_directory.clone();
    if !current.starts_with(root) {
        return roots;
    }
    loop {
        push_existing_dirs(&mut roots, [current.join(".runx/tools")]);
        if current == root {
            break;
        }
        let Some(parent) = current.parent().map(Path::to_path_buf) else {
            break;
        };
        if parent == current {
            break;
        }
        current = parent;
    }
    roots
}

fn push_existing_dirs(roots: &mut Vec<PathBuf>, candidates: impl IntoIterator<Item = PathBuf>) {
    for candidate in candidates {
        if candidate.is_dir() && !roots.iter().any(|root| root == &candidate) {
            roots.push(candidate);
        }
    }
}

fn convert_inputs(
    inputs: BTreeMap<String, runx_parser::SkillInput>,
) -> Result<BTreeMap<String, ToolInput>, ToolCatalogError> {
    inputs
        .into_iter()
        .map(|(name, input)| {
            Ok((
                name,
                ToolInput {
                    input_type: input.input_type,
                    required: input.required,
                    description: input.description,
                    default: convert_optional_json(input.default)?,
                    artifact: None,
                },
            ))
        })
        .collect()
}

fn convert_optional_object(
    value: Option<runx_contracts::JsonObject>,
) -> Result<Option<ToolInspectRunx>, ToolCatalogError> {
    value
        .map(|value| convert_json(runx_contracts::JsonValue::Object(value)))
        .transpose()?
        .map(|value| match value {
            JsonPayload::Object(object) => Ok(ToolInspectRunx::Object(object)),
            _ => Err(ToolCatalogError::InvalidRequest(
                "expected JSON object while converting tool metadata".to_owned(),
            )),
        })
        .transpose()
}

fn convert_optional_runtime(
    value: Option<runx_contracts::JsonValue>,
) -> Result<Option<RuntimeCommand>, ToolCatalogError> {
    value
        .map(|value| {
            let json = serde_json::to_string(&value)
                .map_err(|error| ToolCatalogError::InvalidRequest(error.to_string()))?;
            serde_json::from_str(&json)
                .map_err(|error| ToolCatalogError::InvalidRequest(error.to_string()))
        })
        .transpose()
}

fn convert_optional_json(
    value: Option<runx_contracts::JsonValue>,
) -> Result<Option<JsonPayload>, ToolCatalogError> {
    value.map(convert_json).transpose()
}

fn convert_json(value: runx_contracts::JsonValue) -> Result<JsonPayload, ToolCatalogError> {
    let json = serde_json::to_string(&value)
        .map_err(|error| ToolCatalogError::InvalidRequest(error.to_string()))?;
    serde_json::from_str(&json).map_err(|error| ToolCatalogError::InvalidRequest(error.to_string()))
}

fn display_path(path: &Path) -> String {
    path.to_string_lossy().replace('\\', "/")
}
