use std::collections::BTreeMap;
use std::fs;
use std::future::Future;
use std::path::Path;

use anyhow::{Context, Result, bail};
use base64::Engine;
use serde_json::{Map, Value};
use url::Url;

use crate::auth::oauth2::OAuthManager;
use crate::config::ProxyProfile;
use crate::secrets::SecretManager;
use crate::secrets::store::require_secret;
use crate::security::redact::Redactor;
use crate::template::catalog::TemplateCatalogEntry;
use crate::template::render::{render_json_value, render_string_raw};
use crate::template::schema::{
    AllowRule, ApiKeyLocation, AuthTemplate, BodyTemplate, CliCommandTemplate, CommandMode,
    GraphqlTemplate, GrpcTemplate, HttpOperationTemplate, MultipartPartTemplate, OperationTemplate,
};

use super::transport::{ResolvedTransport, resolve_transport};

#[derive(Debug, Clone)]
pub struct PreparedRequest {
    pub key: String,
    pub mode: CommandMode,
    pub protocol: PreparedProtocol,
    pub method: reqwest::Method,
    pub url: Url,
    pub query: Vec<(String, String)>,
    pub headers: Vec<(String, String)>,
    pub cookies: Vec<(String, String)>,
    pub body: PreparedBody,
    pub grpc: Option<PreparedGrpcRequest>,
    pub cli: Option<PreparedCliCommand>,
    pub allow_rules: Vec<crate::template::schema::AllowRule>,
    pub transport: ResolvedTransport,
    pub result_template: crate::template::schema::ResultTemplate,
    pub args: Map<String, Value>,
    pub redactor: Redactor,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PreparedProtocol {
    Http,
    Graphql,
    Grpc,
    Cli,
}

#[derive(Debug, Clone)]
pub enum PreparedBody {
    Empty,
    Json(Value),
    Form(Vec<(String, String)>),
    Multipart(Vec<PreparedMultipartPart>),
    RawBytes {
        bytes: Vec<u8>,
        content_type: Option<String>,
    },
}

#[derive(Debug, Clone)]
pub struct PreparedMultipartPart {
    pub name: String,
    pub bytes: Vec<u8>,
    pub content_type: Option<String>,
    pub filename: Option<String>,
}

#[derive(Debug, Clone)]
pub struct PreparedCliCommand {
    pub command: String,
    pub args: Vec<String>,
    pub env: Vec<(String, String)>,
    pub cwd: Option<String>,
    pub stdin: Option<String>,
}

#[derive(Debug, Clone)]
pub struct PreparedGrpcRequest {
    pub service: String,
    pub method: String,
    pub descriptor_set: Option<Vec<u8>>,
}

pub async fn build_prepared_request(
    entry: &TemplateCatalogEntry,
    args: Map<String, Value>,
    secret_manager: &SecretManager,
    oauth_manager: &OAuthManager,
    allow_rules: &[AllowRule],
    allow_cli_commands: &[String],
    proxy_profiles: &BTreeMap<String, ProxyProfile>,
) -> Result<PreparedRequest> {
    build_prepared_request_with_token_provider(
        entry,
        args,
        secret_manager,
        |profile| async move { oauth_manager.access_token_for_profile(&profile).await },
        allow_rules,
        allow_cli_commands,
        proxy_profiles,
    )
    .await
}

pub async fn build_prepared_request_with_token_provider<F, Fut>(
    entry: &TemplateCatalogEntry,
    args: Map<String, Value>,
    secret_manager: &SecretManager,
    mut oauth_token_provider: F,
    allow_rules: &[AllowRule],
    allow_cli_commands: &[String],
    proxy_profiles: &BTreeMap<String, ProxyProfile>,
) -> Result<PreparedRequest>
where
    F: FnMut(String) -> Fut,
    Fut: Future<Output = Result<String>>,
{
    let mut secret_values = Vec::new();
    let mut secrets_context = Map::new();

    for secret_key in &entry.template.annotations.secrets {
        let secret = require_secret(secret_manager.store(), secret_key)?;
        insert_dotted_key(
            &mut secrets_context,
            secret_key,
            Value::String(secret.clone()),
        );
        secret_values.push(secret);
    }

    let context = Value::Object(Map::from_iter([
        ("args".to_string(), Value::Object(args.clone())),
        (
            "secrets".to_string(),
            Value::Object(secrets_context.clone()),
        ),
    ]));

    let operation = &entry.template.operation;
    let transport = resolve_transport(operation.transport(), proxy_profiles)?;

    match operation {
        OperationTemplate::Http(http) => {
            let (url, mut query, mut headers, mut cookies) = render_http_primitives(
                entry,
                &http.url,
                http.path.as_ref(),
                &http.query,
                &http.headers,
                &http.cookies,
                &context,
            )?;

            if let Some(auth) = &http.auth {
                let mut outputs = AuthOutputs {
                    headers: &mut headers,
                    query: &mut query,
                    cookies: &mut cookies,
                    secret_values: &mut secret_values,
                };
                apply_auth_inner(
                    auth,
                    &context,
                    secret_manager,
                    &mut oauth_token_provider,
                    &mut outputs,
                )
                .await?;
            }

            let body = render_http_body(http, &context, &entry.key)?;

            Ok(PreparedRequest {
                key: entry.key.clone(),
                mode: entry.mode,
                protocol: PreparedProtocol::Http,
                method: parse_http_method(&http.method, None)?,
                url,
                query,
                headers,
                cookies,
                body,
                grpc: None,
                cli: None,
                allow_rules: allow_rules.to_vec(),
                transport,
                result_template: entry.template.result.clone(),
                args,
                redactor: Redactor::new(secret_values),
            })
        }
        OperationTemplate::Graphql(graphql) => {
            let (url, mut query, mut headers, mut cookies) = render_http_primitives(
                entry,
                &graphql.url,
                graphql.path.as_ref(),
                &graphql.query,
                &graphql.headers,
                &graphql.cookies,
                &context,
            )?;

            if let Some(auth) = &graphql.auth {
                let mut outputs = AuthOutputs {
                    headers: &mut headers,
                    query: &mut query,
                    cookies: &mut cookies,
                    secret_values: &mut secret_values,
                };
                apply_auth_inner(
                    auth,
                    &context,
                    secret_manager,
                    &mut oauth_token_provider,
                    &mut outputs,
                )
                .await?;
            }

            ensure_header_default(&mut headers, "Accept", "application/json");
            ensure_header_default(&mut headers, "Content-Type", "application/json");

            Ok(PreparedRequest {
                key: entry.key.clone(),
                mode: entry.mode,
                protocol: PreparedProtocol::Graphql,
                method: parse_http_method(&graphql.method, Some("POST"))?,
                url,
                query,
                headers,
                cookies,
                body: PreparedBody::Json(render_graphql_body(&graphql.graphql, &context)?),
                grpc: None,
                cli: None,
                allow_rules: allow_rules.to_vec(),
                transport,
                result_template: entry.template.result.clone(),
                args,
                redactor: Redactor::new(secret_values),
            })
        }
        OperationTemplate::Cli(cli_operation) => {
            let cli = render_cli_request(&cli_operation.cli, &context)?;
            ensure_cli_command_allowed(&cli.command, allow_cli_commands, &entry.key)?;

            Ok(PreparedRequest {
                key: entry.key.clone(),
                mode: entry.mode,
                protocol: PreparedProtocol::Cli,
                method: reqwest::Method::GET,
                url: Url::parse("http://localhost").expect("static URL"),
                query: Vec::new(),
                headers: Vec::new(),
                cookies: Vec::new(),
                body: PreparedBody::Empty,
                grpc: None,
                cli: Some(cli),
                allow_rules: Vec::new(),
                transport,
                result_template: entry.template.result.clone(),
                args,
                redactor: Redactor::new(secret_values),
            })
        }
        OperationTemplate::Grpc(grpc_operation) => {
            let url_text = render_string_raw(&grpc_operation.url, &context)?;
            let url = Url::parse(&url_text).with_context(|| {
                format!(
                    "template `{}` rendered invalid operation URL `{url_text}`",
                    entry.key
                )
            })?;
            let mut headers = render_key_value_map(grpc_operation.headers.as_ref(), &context)?;
            let mut query = Vec::new();
            let mut cookies = Vec::new();

            if let Some(auth) = &grpc_operation.auth {
                let mut outputs = AuthOutputs {
                    headers: &mut headers,
                    query: &mut query,
                    cookies: &mut cookies,
                    secret_values: &mut secret_values,
                };
                apply_auth_inner(
                    auth,
                    &context,
                    secret_manager,
                    &mut oauth_token_provider,
                    &mut outputs,
                )
                .await?;
            }

            if !query.is_empty() || !cookies.is_empty() {
                bail!(
                    "template `{}` gRPC auth must use header-based credentials (query/cookie auth is unsupported)",
                    entry.key
                );
            }

            let grpc = render_grpc_request(&grpc_operation.grpc, &context, &entry.key)?;

            Ok(PreparedRequest {
                key: entry.key.clone(),
                mode: entry.mode,
                protocol: PreparedProtocol::Grpc,
                method: reqwest::Method::POST,
                url,
                query: Vec::new(),
                headers,
                cookies: Vec::new(),
                body: PreparedBody::Json(render_grpc_body(&grpc_operation.grpc, &context)?),
                grpc: Some(grpc),
                cli: None,
                allow_rules: allow_rules.to_vec(),
                transport,
                result_template: entry.template.result.clone(),
                args,
                redactor: Redactor::new(secret_values),
            })
        }
    }
}

type RenderedHttpPrimitives = (
    Url,
    Vec<(String, String)>,
    Vec<(String, String)>,
    Vec<(String, String)>,
);

fn render_http_primitives(
    entry: &TemplateCatalogEntry,
    url_template: &str,
    path_template: Option<&String>,
    query_template: &Option<BTreeMap<String, Value>>,
    headers_template: &Option<BTreeMap<String, Value>>,
    cookies_template: &Option<BTreeMap<String, Value>>,
    context: &Value,
) -> Result<RenderedHttpPrimitives> {
    let url_text = render_string_raw(url_template, context)?;
    let mut url = Url::parse(&url_text).with_context(|| {
        format!(
            "template `{}` rendered invalid operation URL `{url_text}`",
            entry.key
        )
    })?;

    if let Some(path_template) = path_template {
        let rendered_path = render_string_raw(path_template, context)?;
        if rendered_path.starts_with('/') {
            url.set_path(&rendered_path);
        } else {
            let current = url.path().trim_end_matches('/');
            let next = format!("{current}/{rendered_path}");
            url.set_path(&next);
        }
    }

    let query = render_key_value_map(query_template.as_ref(), context)?;
    let headers = render_key_value_map(headers_template.as_ref(), context)?;
    let cookies = render_key_value_map(cookies_template.as_ref(), context)?;

    Ok((url, query, headers, cookies))
}

fn render_http_body(
    http: &HttpOperationTemplate,
    context: &Value,
    command_key: &str,
) -> Result<PreparedBody> {
    match http.body.as_ref().unwrap_or(&BodyTemplate::None) {
        BodyTemplate::None => Ok(PreparedBody::Empty),
        BodyTemplate::Json { value } => Ok(PreparedBody::Json(render_json_value(value, context)?)),
        BodyTemplate::FormUrlencoded { fields } => Ok(PreparedBody::Form(render_key_value_map(
            Some(fields),
            context,
        )?)),
        BodyTemplate::Multipart { parts } => Ok(PreparedBody::Multipart(render_multipart_parts(
            parts, context,
        )?)),
        BodyTemplate::RawText {
            value,
            content_type,
        } => {
            let rendered = render_string_raw(value, context)?;
            Ok(PreparedBody::RawBytes {
                bytes: rendered.into_bytes(),
                content_type: content_type
                    .clone()
                    .or_else(|| Some("text/plain".to_string())),
            })
        }
        BodyTemplate::RawBytesBase64 {
            value,
            content_type,
        } => {
            let rendered = render_string_raw(value, context)?;
            let bytes = base64::engine::general_purpose::STANDARD
                .decode(rendered)
                .context("invalid base64 in raw_bytes_base64 body")?;
            Ok(PreparedBody::RawBytes {
                bytes,
                content_type: content_type.clone(),
            })
        }
        BodyTemplate::FileStream { path, content_type } => {
            let rendered_path = render_string_raw(path, context)?;
            let bytes = fs::read(&rendered_path).with_context(|| {
                format!(
                    "failed reading file_stream body data from `{rendered_path}` for command {command_key}"
                )
            })?;
            Ok(PreparedBody::RawBytes {
                bytes,
                content_type: content_type.clone(),
            })
        }
    }
}

async fn apply_auth_inner<F, Fut>(
    auth: &AuthTemplate,
    context: &Value,
    secret_manager: &SecretManager,
    oauth_token_provider: &mut F,
    outputs: &mut AuthOutputs<'_>,
) -> Result<()>
where
    F: FnMut(String) -> Fut,
    Fut: Future<Output = Result<String>>,
{
    match auth {
        AuthTemplate::None => {}
        AuthTemplate::ApiKey {
            location,
            name,
            secret,
        } => {
            let value = require_secret(secret_manager.store(), secret)?;
            outputs.secret_values.push(value.clone());
            match location {
                ApiKeyLocation::Header => outputs.headers.push((name.clone(), value)),
                ApiKeyLocation::Query => outputs.query.push((name.clone(), value)),
                ApiKeyLocation::Cookie => outputs.cookies.push((name.clone(), value)),
            }
        }
        AuthTemplate::Bearer { secret } => {
            let token = require_secret(secret_manager.store(), secret)?;
            outputs.secret_values.push(token.clone());
            outputs
                .headers
                .push(("Authorization".to_string(), format!("Bearer {token}")));
        }
        AuthTemplate::Basic {
            username,
            password_secret,
        } => {
            let user = render_string_raw(username, context)?;
            let password = require_secret(secret_manager.store(), password_secret)?;
            outputs.secret_values.push(password.clone());
            let encoded =
                base64::engine::general_purpose::STANDARD.encode(format!("{user}:{password}"));
            outputs
                .headers
                .push(("Authorization".to_string(), format!("Basic {encoded}")));
        }
        AuthTemplate::OAuth2Profile { profile } => {
            let token = oauth_token_provider(profile.clone()).await?;
            outputs.secret_values.push(token.clone());
            outputs
                .headers
                .push(("Authorization".to_string(), format!("Bearer {token}")));
        }
    }

    Ok(())
}

struct AuthOutputs<'a> {
    headers: &'a mut Vec<(String, String)>,
    query: &'a mut Vec<(String, String)>,
    cookies: &'a mut Vec<(String, String)>,
    secret_values: &'a mut Vec<String>,
}

fn render_key_value_map(
    input: Option<&BTreeMap<String, Value>>,
    context: &Value,
) -> Result<Vec<(String, String)>> {
    let mut out = Vec::new();
    let Some(input) = input else {
        return Ok(out);
    };

    for (key, value) in input {
        let rendered_key = render_string_raw(key, context)?;
        let rendered_value = render_json_value(value, context)?;

        match rendered_value {
            Value::Array(values) => {
                for value in values {
                    out.push((rendered_key.clone(), value_to_string(value)?));
                }
            }
            other => out.push((rendered_key, value_to_string(other)?)),
        }
    }

    Ok(out)
}

fn render_multipart_parts(
    parts: &[MultipartPartTemplate],
    context: &Value,
) -> Result<Vec<PreparedMultipartPart>> {
    let mut out = Vec::new();
    for part in parts {
        let bytes = if let Some(value) = &part.value {
            render_string_raw(value, context)?.into_bytes()
        } else if let Some(value) = &part.bytes_base64 {
            let rendered = render_string_raw(value, context)?;
            base64::engine::general_purpose::STANDARD
                .decode(rendered)
                .context("invalid multipart bytes_base64")?
        } else if let Some(path) = &part.file_path {
            let rendered_path = render_string_raw(path, context)?;
            fs::read(&rendered_path)
                .with_context(|| format!("failed reading multipart file `{rendered_path}`"))?
        } else {
            bail!(
                "multipart part `{}` does not contain any data source",
                part.name
            );
        };

        let filename = part
            .filename
            .as_ref()
            .map(|name| render_string_raw(name, context))
            .transpose()?;

        out.push(PreparedMultipartPart {
            name: part.name.clone(),
            bytes,
            content_type: part.content_type.clone(),
            filename,
        });
    }
    Ok(out)
}

fn render_graphql_body(graphql: &GraphqlTemplate, context: &Value) -> Result<Value> {
    let mut payload = Map::new();
    payload.insert(
        "query".to_string(),
        Value::String(render_string_raw(&graphql.query, context)?),
    );

    if let Some(operation_name) = &graphql.operation_name {
        payload.insert(
            "operationName".to_string(),
            Value::String(render_string_raw(operation_name, context)?),
        );
    }

    if let Some(variables) = &graphql.variables {
        payload.insert(
            "variables".to_string(),
            render_json_value(variables, context)?,
        );
    }

    Ok(Value::Object(payload))
}

fn render_grpc_body(grpc: &GrpcTemplate, context: &Value) -> Result<Value> {
    let Some(body) = &grpc.body else {
        return Ok(Value::Object(Map::new()));
    };
    render_json_value(body, context)
}

fn render_grpc_request(
    grpc: &GrpcTemplate,
    context: &Value,
    command_key: &str,
) -> Result<PreparedGrpcRequest> {
    let service = render_string_raw(&grpc.service, context)?;
    let service = service.trim().to_string();
    if service.is_empty() {
        bail!("operation.grpc.service rendered empty");
    }
    let method = render_string_raw(&grpc.method, context)?;
    let method = method.trim().to_string();
    if method.is_empty() {
        bail!("operation.grpc.method rendered empty");
    }

    let descriptor_set = grpc
        .descriptor_set_file
        .as_ref()
        .map(|path| render_string_raw(path, context))
        .transpose()?
        .map(|rendered_path| {
            fs::read(&rendered_path).with_context(|| {
                format!(
                    "failed reading operation.grpc.descriptor_set_file `{rendered_path}` for command {command_key}"
                )
            })
        })
        .transpose()?;

    Ok(PreparedGrpcRequest {
        service,
        method,
        descriptor_set,
    })
}

fn render_cli_request(cli: &CliCommandTemplate, context: &Value) -> Result<PreparedCliCommand> {
    let command = render_string_raw(&cli.command, context)?;
    if command.trim().is_empty() {
        bail!("operation.cli.command rendered empty");
    }
    let args = render_cli_args(&cli.args, context)?;
    let env = render_key_value_map(cli.env.as_ref(), context)?;
    let cwd = cli
        .cwd
        .as_ref()
        .map(|value| render_string_raw(value, context))
        .transpose()?
        .filter(|value| !value.trim().is_empty());
    let stdin = cli
        .stdin
        .as_ref()
        .map(|value| render_string_raw(value, context))
        .transpose()?;

    Ok(PreparedCliCommand {
        command,
        args,
        env,
        cwd,
        stdin,
    })
}

fn ensure_cli_command_allowed(
    command: &str,
    allow_cli_commands: &[String],
    command_key: &str,
) -> Result<()> {
    if allow_cli_commands.is_empty() {
        bail!(
            "CLI operation `{}` is blocked: no network.allow_cli_commands configured",
            command_key
        );
    }

    let executable_name = Path::new(command)
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or(command);

    let allowed = allow_cli_commands.iter().any(|candidate| {
        let candidate = candidate.trim();
        if candidate.is_empty() {
            return false;
        }

        if candidate.contains('/') || candidate.contains('\\') {
            command == candidate
        } else {
            executable_name.eq_ignore_ascii_case(candidate)
        }
    });

    if !allowed {
        bail!(
            "CLI operation `{}` is blocked: command `{}` is not in network.allow_cli_commands",
            command_key,
            command
        );
    }

    Ok(())
}

fn render_cli_args(args: &[Value], context: &Value) -> Result<Vec<String>> {
    let mut rendered = Vec::with_capacity(args.len());
    for arg in args {
        rendered.push(value_to_string(render_json_value(arg, context)?)?);
    }
    Ok(rendered)
}

fn parse_http_method(method: &str, fallback: Option<&str>) -> Result<reqwest::Method> {
    let raw = method.trim();
    let method = if raw.is_empty() {
        fallback.unwrap_or("")
    } else {
        raw
    };

    method
        .parse::<reqwest::Method>()
        .with_context(|| format!("invalid HTTP method `{method}`"))
}

fn ensure_header_default(headers: &mut Vec<(String, String)>, name: &str, value: &str) {
    if headers.iter().any(|(k, _)| k.eq_ignore_ascii_case(name)) {
        return;
    }
    headers.push((name.to_string(), value.to_string()));
}

fn value_to_string(value: Value) -> Result<String> {
    let out = match value {
        Value::Null => String::new(),
        Value::Bool(v) => v.to_string(),
        Value::Number(v) => v.to_string(),
        Value::String(v) => v,
        Value::Array(_) | Value::Object(_) => serde_json::to_string(&value)?,
    };
    Ok(out)
}

fn insert_dotted_key(root: &mut Map<String, Value>, dotted_key: &str, value: Value) {
    let parts: Vec<&str> = dotted_key.split('.').filter(|p| !p.is_empty()).collect();
    if parts.is_empty() {
        return;
    }

    let mut current = root;
    for (index, part) in parts.iter().enumerate() {
        if index == parts.len() - 1 {
            current.insert((*part).to_string(), value.clone());
            break;
        }

        let child = current
            .entry((*part).to_string())
            .or_insert_with(|| Value::Object(Map::new()));

        if !child.is_object() {
            *child = Value::Object(Map::new());
        }

        current = child.as_object_mut().expect("object ensured above");
    }
}
