use std::{
    collections::{BTreeMap, BTreeSet},
    fmt::Write,
    fs::create_dir_all,
    io,
    path::{Path, PathBuf},
};

use brk_types::TreeNode;
use oas3::Spec;
use serde_json::Value;

use crate::{ClientMetadata, Endpoint, ResponseKind, TypeSchemas};

use super::write_if_changed;

mod manifest;

const BASE_URL: &str = "https://bitview.space";
const MCP_URL: &str = "https://mcp.bitview.space/";

pub fn generate_llm_clients(
    metadata: &ClientMetadata,
    spec: &Spec,
    endpoints: &[Endpoint],
    schemas: &TypeSchemas,
    roots: &[PathBuf],
    manifest_path: Option<&Path>,
) -> io::Result<()> {
    let metric_count = count_metrics(&metadata.catalog);
    let generated = endpoints
        .iter()
        .filter(|endpoint| endpoint.should_generate())
        .collect::<Vec<_>>();
    let llms = render_llms(
        &spec.info.title,
        &spec.info.version,
        metric_count,
        &generated,
    );
    let llms_full = render_llms_full(
        &spec.info.title,
        &spec.info.version,
        metric_count,
        &generated,
        schemas,
    );

    for root in roots {
        create_dir_all(root)?;
        write_output(&root.join("llms.txt"), &llms)?;
        write_output(&root.join("llms-full.txt"), &llms_full)?;
    }
    if let Some(path) = manifest_path {
        if let Some(parent) = path.parent() {
            create_dir_all(parent)?;
        }
        manifest::generate_tool_manifest(endpoints, schemas, path)?;
    }
    Ok(())
}

fn write_output(path: &Path, content: &str) -> io::Result<()> {
    if let Some(parent) = path.parent() {
        create_dir_all(parent)?;
    }
    write_if_changed(path, content)
}

fn count_metrics(node: &TreeNode) -> usize {
    match node {
        TreeNode::Leaf(_) => 1,
        TreeNode::Branch(children) => children.values().map(count_metrics).sum(),
    }
}

fn render_llms(title: &str, version: &str, metric_count: usize, endpoints: &[&Endpoint]) -> String {
    format!(
        "# {title} (BRK)\n\n\
> Free, open-source Bitcoin analytics API and block explorer. {metric_count} on-chain time-series and {} API operations. No authentication required.\n\n\
## API\n\n\
- Version: `{version}`\n\
- Base URL: {BASE_URL}\n\
- [Full plain-text reference]({BASE_URL}/llms-full.txt)\n\
- [Compact OpenAPI]({BASE_URL}/api.json)\n\
- [Full OpenAPI]({BASE_URL}/openapi.json)\n\
- [Series catalog]({BASE_URL}/api/series)\n\
- [Interactive documentation]({BASE_URL}/api)\n\n\
Use OpenAPI for tool construction, `/api/series` for complete series metadata, and `llms-full.txt` for a readable reference.\n\n\
## MCP\n\n\
- Endpoint: {MCP_URL}\n\
- Transport: Streamable HTTP\n\
- Authentication: None\n\n\
The MCP server is stateless and read-only. Its tools are generated from these OpenAPI operations.\n\n\
## Clients\n\n\
- [JavaScript](https://www.npmjs.com/package/brk-client)\n\
- [Python](https://pypi.org/project/brk-client/)\n\
- [Rust](https://crates.io/crates/brk_client)\n\n\
## Source\n\n\
- [GitHub](https://github.com/bitcoinresearchkit/brk)\n\
- MIT licensed\n",
        endpoints.len(),
    )
}

fn render_llms_full(
    title: &str,
    version: &str,
    metric_count: usize,
    endpoints: &[&Endpoint],
    schemas: &TypeSchemas,
) -> String {
    let mut output = String::new();
    writeln!(output, "# {title} (BRK) — Full API Reference\n").unwrap();
    writeln!(
        output,
        "> Generated from BRK's OpenAPI specification and metric tree. Do not edit this file manually.\n"
    )
    .unwrap();
    writeln!(output, "- Version: `{version}`").unwrap();
    writeln!(output, "- Base URL: {BASE_URL}").unwrap();
    writeln!(output, "- MCP endpoint: {MCP_URL}").unwrap();
    writeln!(output, "- Metrics: {metric_count}").unwrap();
    writeln!(output, "- Operations: {}\n", endpoints.len()).unwrap();
    writeln!(
        output,
        "For machine-readable tool construction, use [{BASE_URL}/openapi.json]({BASE_URL}/openapi.json). For the complete source-derived series tree, use [{BASE_URL}/api/series]({BASE_URL}/api/series).\n"
    )
    .unwrap();

    let mut groups = BTreeMap::<String, Vec<&Endpoint>>::new();
    for endpoint in endpoints {
        groups
            .entry(endpoint_group(&endpoint.path))
            .or_default()
            .push(endpoint);
    }
    for group in groups.values_mut() {
        group.sort_unstable_by(|left, right| {
            left.path
                .cmp(&right.path)
                .then_with(|| left.method.cmp(&right.method))
        });
    }

    writeln!(output, "## Operations\n").unwrap();
    for (group, operations) in groups {
        writeln!(output, "### {group}\n").unwrap();
        for endpoint in operations {
            render_endpoint(&mut output, endpoint);
        }
    }

    let referenced = referenced_schemas(endpoints, schemas);
    if !referenced.is_empty() {
        writeln!(output, "## Schemas\n").unwrap();
        for name in referenced {
            let Some(schema) = schemas.get(&name) else {
                continue;
            };
            render_schema(&mut output, &name, schema);
        }
    }
    output
}

fn endpoint_group(path: &str) -> String {
    let segment = path
        .split('/')
        .find(|part| !part.is_empty() && *part != "api" && *part != "v1")
        .unwrap_or("server");
    title_case(segment)
}

fn title_case(value: &str) -> String {
    value
        .split(['-', '_'])
        .filter(|part| !part.is_empty())
        .map(|part| {
            let mut chars = part.chars();
            chars
                .next()
                .map(|first| first.to_uppercase().collect::<String>() + chars.as_str())
                .unwrap_or_default()
        })
        .collect::<Vec<_>>()
        .join(" ")
}

fn render_endpoint(output: &mut String, endpoint: &Endpoint) {
    writeln!(output, "#### {} `{}`\n", endpoint.method, endpoint.path).unwrap();
    if let Some(summary) = endpoint
        .summary
        .as_deref()
        .filter(|value| !value.trim().is_empty())
    {
        writeln!(output, "{}\n", one_line(summary)).unwrap();
    }
    if let Some(description) = endpoint
        .description
        .as_deref()
        .filter(|value| !value.trim().is_empty())
    {
        let description = one_line(description);
        if endpoint.summary.as_deref().map(one_line).as_deref() != Some(description.as_str()) {
            writeln!(output, "{description}\n").unwrap();
        }
    }

    let parameters = endpoint
        .path_params
        .iter()
        .map(|parameter| ("path", parameter))
        .chain(
            endpoint
                .query_params
                .iter()
                .map(|parameter| ("query", parameter)),
        )
        .collect::<Vec<_>>();
    if !parameters.is_empty() {
        writeln!(output, "Parameters:").unwrap();
        for (location, parameter) in parameters {
            let requirement = if parameter.required {
                "required"
            } else {
                "optional"
            };
            write!(
                output,
                "- `{}` ({location}, {}, {requirement})",
                parameter.name, parameter.param_type
            )
            .unwrap();
            if let Some(description) = parameter
                .description
                .as_deref()
                .filter(|value| !value.trim().is_empty())
            {
                write!(output, ": {}", one_line(description)).unwrap();
            }
            writeln!(output).unwrap();
        }
        writeln!(output).unwrap();
    }

    if let Some(body) = &endpoint.request_body {
        writeln!(
            output,
            "Request body: `{}` ({})\n",
            body.body_type,
            if body.required {
                "required"
            } else {
                "optional"
            }
        )
        .unwrap();
    }
    writeln!(
        output,
        "Returns: {}\n",
        response_label(&endpoint.response_kind)
    )
    .unwrap();
    writeln!(output, "```bash").unwrap();
    writeln!(output, "{}", curl_example(endpoint)).unwrap();
    writeln!(output, "```\n").unwrap();
}

fn response_label(response: &ResponseKind) -> String {
    match response {
        ResponseKind::Json(name) => format!("JSON `{name}`"),
        ResponseKind::Text(Some(schema)) => format!("text `{}`", schema.name),
        ResponseKind::Text(None) => "text".to_owned(),
        ResponseKind::Binary => "binary data".to_owned(),
    }
}

fn curl_example(endpoint: &Endpoint) -> String {
    let mut path = endpoint.path.clone();
    for parameter in &endpoint.path_params {
        path = path.replace(
            &format!("{{{}}}", parameter.name),
            &format!("<{}>", parameter.name),
        );
    }
    if !endpoint.query_params.is_empty() {
        path.push('?');
        path.push_str(
            &endpoint
                .query_params
                .iter()
                .map(|parameter| format!("{}=<{}>", parameter.name, parameter.name))
                .collect::<Vec<_>>()
                .join("&"),
        );
    }
    if let Some(body) = &endpoint.request_body {
        format!(
            "curl -s -X {} --data '<{}>' \"{BASE_URL}{path}\"",
            endpoint.method, body.body_type
        )
    } else {
        format!("curl -s \"{BASE_URL}{path}\"")
    }
}

fn referenced_schemas(endpoints: &[&Endpoint], schemas: &TypeSchemas) -> BTreeSet<String> {
    let mut names = BTreeSet::new();
    for endpoint in endpoints {
        if let Some(name) = endpoint.schema_name().filter(|name| *name != "*") {
            names.insert(name.to_owned());
        }
        if let Some(body) = &endpoint.request_body
            && schemas.contains_key(&body.body_type)
        {
            names.insert(body.body_type.clone());
        }
    }

    let mut pending = names.iter().cloned().collect::<Vec<_>>();
    while let Some(name) = pending.pop() {
        let Some(schema) = schemas.get(&name) else {
            continue;
        };
        let mut refs = BTreeSet::new();
        collect_refs(schema, &mut refs);
        for referenced in refs {
            if schemas.contains_key(&referenced) && names.insert(referenced.clone()) {
                pending.push(referenced);
            }
        }
    }
    names
}

fn collect_refs(value: &Value, refs: &mut BTreeSet<String>) {
    match value {
        Value::Object(object) => {
            if let Some(reference) = object.get("$ref").and_then(Value::as_str)
                && let Some(name) = reference.rsplit('/').next()
            {
                refs.insert(name.to_owned());
            }
            for child in object.values() {
                collect_refs(child, refs);
            }
        }
        Value::Array(values) => {
            for child in values {
                collect_refs(child, refs);
            }
        }
        _ => {}
    }
}

fn render_schema(output: &mut String, name: &str, schema: &Value) {
    writeln!(output, "### `{name}`\n").unwrap();
    let required = schema
        .get("required")
        .and_then(Value::as_array)
        .map(|values| {
            values
                .iter()
                .filter_map(Value::as_str)
                .collect::<BTreeSet<_>>()
        })
        .unwrap_or_default();
    if let Some(properties) = schema.get("properties").and_then(Value::as_object) {
        for (property, shape) in properties {
            write!(
                output,
                "- `{property}`: `{}`{}",
                schema_type(shape),
                if required.contains(property.as_str()) {
                    " (required)"
                } else {
                    ""
                }
            )
            .unwrap();
            if let Some(description) = shape
                .get("description")
                .and_then(Value::as_str)
                .filter(|value| !value.trim().is_empty())
            {
                write!(output, " — {}", one_line(description)).unwrap();
            }
            writeln!(output).unwrap();
        }
    } else {
        writeln!(output, "`{}`", schema_type(schema)).unwrap();
    }
    writeln!(output).unwrap();
}

fn schema_type(schema: &Value) -> String {
    if let Some(reference) = schema.get("$ref").and_then(Value::as_str) {
        return reference.rsplit('/').next().unwrap_or(reference).to_owned();
    }
    if let Some(values) = schema.get("enum").and_then(Value::as_array) {
        return values
            .iter()
            .map(|value| {
                value
                    .as_str()
                    .map_or_else(|| value.to_string(), str::to_owned)
            })
            .collect::<Vec<_>>()
            .join(" | ");
    }
    if let Some(kind) = schema.get("type").and_then(Value::as_str) {
        if kind == "array" {
            return format!(
                "{}[]",
                schema
                    .get("items")
                    .map(schema_type)
                    .unwrap_or_else(|| "value".to_owned())
            );
        }
        return kind.to_owned();
    }
    for key in ["oneOf", "anyOf", "allOf"] {
        if let Some(values) = schema.get(key).and_then(Value::as_array) {
            return values
                .iter()
                .map(schema_type)
                .collect::<Vec<_>>()
                .join(" | ");
        }
    }
    "object".to_owned()
}

fn one_line(value: &str) -> String {
    value.split_whitespace().collect::<Vec<_>>().join(" ")
}

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

    fn endpoint(path: &str, method: &str) -> Endpoint {
        Endpoint {
            method: method.to_owned(),
            path: path.to_owned(),
            operation_id: None,
            summary: Some("Read a thing".to_owned()),
            description: None,
            path_params: vec![Parameter {
                name: "id".to_owned(),
                required: true,
                param_type: "string".to_owned(),
                description: Some("Thing identifier".to_owned()),
                schema: serde_json::json!({ "type": "string" }),
            }],
            query_params: Vec::new(),
            request_body: None,
            response_kind: ResponseKind::Json("Thing".to_owned()),
            json_response_schema: Some(serde_json::json!({
                "$ref": "#/components/schemas/Thing"
            })),
            deprecated: false,
            supports_csv: false,
        }
    }

    #[test]
    fn full_reference_lists_every_operation_once() {
        let first = endpoint("/api/thing/{id}", "GET");
        let second = endpoint("/api/thing/{id}/status", "GET");
        let endpoints = [&first, &second];
        let output = render_llms_full("BRK", "v1", 12, &endpoints, &BTreeMap::new());

        assert_eq!(output.matches("#### GET `/api/thing/{id}`\n").count(), 1);
        assert_eq!(
            output
                .matches("#### GET `/api/thing/{id}/status`\n")
                .count(),
            1
        );
        assert!(output.contains("- MCP endpoint: https://mcp.bitview.space/"));
        assert!(output.contains("curl -s \"https://bitview.space/api/thing/<id>\""));
    }

    #[test]
    fn discovery_lists_the_official_mcp_endpoint() {
        let first = endpoint("/api/thing/{id}", "GET");
        let endpoints = [&first];
        let output = render_llms("BRK", "v1", 12, &endpoints);

        assert!(output.contains("- Endpoint: https://mcp.bitview.space/"));
        assert!(output.contains("- Transport: Streamable HTTP"));
        assert!(output.contains("- Authentication: None"));
    }

    #[test]
    fn schema_renderer_keeps_required_fields_and_references() {
        let schema = serde_json::json!({
            "type": "object",
            "required": ["tx"],
            "properties": {
                "tx": { "$ref": "#/components/schemas/Transaction" },
                "height": { "type": "integer" }
            }
        });
        let mut output = String::new();

        render_schema(&mut output, "Result", &schema);

        assert!(output.contains("`tx`: `Transaction` (required)"));
        assert!(output.contains("`height`: `integer`"));
    }
}
