use std::path::PathBuf;

use anyhow::Result;
use axum::extract::{Path as AxumPath, State};
use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::{Json, Router};
use include_dir::{Dir, include_dir};
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::auth::oauth2::OAuthManager;
use crate::config::{self, Config};
use crate::expression::binder::bind_arguments;
use crate::expression::parser::parse_expression;
use crate::http::builder::build_prepared_request;
use crate::http::executor::execute_prepared_request;
use crate::output::human::render_human_output;
use crate::secrets::SecretManager;
use crate::template::catalog::{TemplateCatalogEntry, TemplateScope};
use crate::template::loader::load_catalog;
use crate::template::schema::{CommandMode, OperationProtocol, ParamSpec};

static WEB_DIST: Dir<'static> = include_dir!("$OUT_DIR/web-build/web/dist");

#[derive(Debug, Clone)]
pub struct WebState {
    pub cwd: PathBuf,
}

pub fn build_router(state: WebState) -> Router {
    Router::new()
        .route("/api/health", get(api_health))
        .route("/api/tools", get(api_list_tools))
        .route("/api/tools/{key}", get(api_get_tool))
        .route("/api/execute", post(api_execute))
        .route("/", get(serve_index))
        .route("/{*path}", get(serve_static_or_index))
        .with_state(state)
}

async fn api_health() -> StatusCode {
    StatusCode::NO_CONTENT
}

async fn api_list_tools(
    State(state): State<WebState>,
) -> Result<Json<Vec<ToolSummaryResponse>>, ApiError> {
    let catalog = load_catalog(&state.cwd).map_err(|err| {
        ApiError::execution_error(format!("failed loading template catalog: {err:#}"))
    })?;

    let mut tools: Vec<_> = catalog.values().map(tool_summary_from_entry).collect();
    tools.sort_by(|left, right| left.key.cmp(&right.key));

    Ok(Json(tools))
}

async fn api_get_tool(
    State(state): State<WebState>,
    AxumPath(key): AxumPath<String>,
) -> Result<Json<ToolDetailResponse>, ApiError> {
    let catalog = load_catalog(&state.cwd).map_err(|err| {
        ApiError::execution_error(format!("failed loading template catalog: {err:#}"))
    })?;

    let entry = catalog
        .get(&key)
        .ok_or_else(|| ApiError::unknown_command(format!("unknown command `{key}`")))?;

    let summary = tool_summary_from_entry(entry);
    let operation = serde_json::to_value(&entry.template.operation)
        .map_err(|err| ApiError::execution_error(format!("failed serializing operation: {err}")))?;

    Ok(Json(ToolDetailResponse { summary, operation }))
}

async fn api_execute(
    State(state): State<WebState>,
    Json(payload): Json<ExecuteRequest>,
) -> Result<Json<ExecuteSuccessResponse>, ApiError> {
    let expression = parse_expression(&payload.expression)
        .map_err(|err| ApiError::parse_error(err.to_string()))?;

    let catalog = load_catalog(&state.cwd).map_err(|err| {
        ApiError::execution_error(format!("failed loading template catalog: {err:#}"))
    })?;

    let key = expression.command_key();
    let entry = catalog
        .get(&key)
        .ok_or_else(|| ApiError::unknown_command(format!("unknown command `{key}`")))?;

    let args = bind_arguments(&expression, &entry.template.params)
        .map_err(|err| ApiError::bind_error(err.to_string()))?;

    if entry.mode == CommandMode::Write && !payload.confirm_write {
        return Err(ApiError::write_confirmation_required(format!(
            "command `{}` is write-enabled and requires confirm_write=true",
            entry.key
        )));
    }

    let cfg = config::load_config()
        .map_err(|err| ApiError::execution_error(format!("failed loading config: {err:#}")))?;

    let success = execute_entry(entry, args, cfg)
        .await
        .map_err(|err| ApiError::execution_error(err.to_string()))?;

    Ok(Json(success))
}

async fn execute_entry(
    entry: &TemplateCatalogEntry,
    args: serde_json::Map<String, Value>,
    cfg: Config,
) -> Result<ExecuteSuccessResponse> {
    let allow_rules = cfg.network.allow.clone();
    let allow_cli_commands = cfg.network.allow_cli_commands.clone();
    let proxy_profiles = cfg.network.proxy_profiles.clone();

    let secret_manager = SecretManager::new();
    let oauth_manager = OAuthManager::new(cfg, SecretManager::new())?;

    let prepared = build_prepared_request(
        entry,
        args,
        &secret_manager,
        &oauth_manager,
        &allow_rules,
        &allow_cli_commands,
        &proxy_profiles,
    )
    .await?;

    let execution = execute_prepared_request(&prepared).await?;
    let human_output =
        render_human_output(&prepared.result_template, &prepared.args, &execution.result)?;

    Ok(ExecuteSuccessResponse {
        key: entry.key.clone(),
        mode: entry.mode.as_str().to_string(),
        status: execution.status,
        url: prepared.redactor.redact(&execution.url),
        result: prepared.redactor.redact_json(&execution.result),
        decoded: prepared.redactor.redact_json(&execution.decoded),
        human_output: prepared.redactor.redact(&human_output),
    })
}

async fn serve_index() -> Response {
    serve_embedded_file("index.html", true)
}

async fn serve_static_or_index(AxumPath(path): AxumPath<String>) -> Response {
    let path = path.trim_start_matches('/');

    if path.is_empty() {
        return serve_embedded_file("index.html", true);
    }

    if WEB_DIST.get_file(path).is_some() {
        return serve_embedded_file(path, false);
    }

    serve_embedded_file("index.html", true)
}

fn serve_embedded_file(path: &str, is_index: bool) -> Response {
    let Some(file) = WEB_DIST.get_file(path) else {
        return StatusCode::NOT_FOUND.into_response();
    };

    let mut headers = HeaderMap::new();
    let content_type = mime_guess::from_path(path).first_or_octet_stream();
    headers.insert(
        header::CONTENT_TYPE,
        HeaderValue::from_str(content_type.as_ref())
            .unwrap_or_else(|_| HeaderValue::from_static("application/octet-stream")),
    );

    if is_index {
        headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
    } else if path.starts_with("assets/") {
        headers.insert(
            header::CACHE_CONTROL,
            HeaderValue::from_static("public, max-age=31536000, immutable"),
        );
    }

    (headers, file.contents().to_vec()).into_response()
}

fn tool_summary_from_entry(entry: &TemplateCatalogEntry) -> ToolSummaryResponse {
    ToolSummaryResponse {
        key: entry.key.clone(),
        provider: entry.provider.clone(),
        command: entry.command.clone(),
        title: entry.title.clone(),
        summary: entry.summary.clone(),
        description: entry.description.clone(),
        mode: entry.mode.as_str().to_string(),
        protocol: protocol_label(entry.template.operation.protocol()).to_string(),
        categories: entry.categories.clone(),
        params: entry.template.params.clone(),
        source: ToolSourceResponse {
            scope: template_scope_label(entry.source.scope).to_string(),
            path: entry.source.path.display().to_string(),
        },
        example_expression: build_example_expression(entry),
    }
}

fn build_example_expression(entry: &TemplateCatalogEntry) -> String {
    if entry.template.params.is_empty() {
        return format!("{}()", entry.key);
    }

    let args = entry
        .template
        .params
        .iter()
        .map(example_argument)
        .collect::<Vec<_>>()
        .join(", ");

    format!("{}({args})", entry.key)
}

fn example_argument(param: &ParamSpec) -> String {
    let value = param
        .default
        .as_ref()
        .map(example_value)
        .unwrap_or_else(|| placeholder_value(param.r#type));
    format!("{}={value}", param.name)
}

fn example_value(value: &Value) -> String {
    match value {
        Value::String(text) => serde_json::to_string(text).unwrap_or_else(|_| "\"\"".to_string()),
        _ => value.to_string(),
    }
}

fn placeholder_value(param_type: crate::template::schema::ParamType) -> String {
    match param_type {
        crate::template::schema::ParamType::String => "\"\"".to_string(),
        crate::template::schema::ParamType::Integer => "0".to_string(),
        crate::template::schema::ParamType::Number => "0".to_string(),
        crate::template::schema::ParamType::Boolean => "false".to_string(),
        crate::template::schema::ParamType::Null => "null".to_string(),
        crate::template::schema::ParamType::Array => "[]".to_string(),
        crate::template::schema::ParamType::Object => "{}".to_string(),
    }
}

fn template_scope_label(scope: TemplateScope) -> &'static str {
    match scope {
        TemplateScope::Local => "local",
        TemplateScope::Global => "global",
    }
}

fn protocol_label(protocol: OperationProtocol) -> &'static str {
    match protocol {
        OperationProtocol::Http => "http",
        OperationProtocol::Graphql => "graphql",
        OperationProtocol::Grpc => "grpc",
        OperationProtocol::Cli => "cli",
    }
}

#[derive(Debug, Serialize)]
struct ToolSummaryResponse {
    key: String,
    provider: String,
    command: String,
    title: String,
    summary: String,
    description: String,
    mode: String,
    protocol: String,
    categories: Vec<String>,
    params: Vec<ParamSpec>,
    source: ToolSourceResponse,
    example_expression: String,
}

#[derive(Debug, Serialize)]
struct ToolDetailResponse {
    #[serde(flatten)]
    summary: ToolSummaryResponse,
    operation: Value,
}

#[derive(Debug, Serialize)]
struct ToolSourceResponse {
    scope: String,
    path: String,
}

#[derive(Debug, Deserialize)]
struct ExecuteRequest {
    expression: String,
    #[serde(default)]
    confirm_write: bool,
}

#[derive(Debug, Serialize)]
struct ExecuteSuccessResponse {
    key: String,
    mode: String,
    status: u16,
    url: String,
    result: Value,
    decoded: Value,
    human_output: String,
}

#[derive(Debug)]
struct ApiError {
    status: StatusCode,
    code: &'static str,
    message: String,
}

impl ApiError {
    fn parse_error(message: String) -> Self {
        Self {
            status: StatusCode::BAD_REQUEST,
            code: "parse_error",
            message,
        }
    }

    fn unknown_command(message: String) -> Self {
        Self {
            status: StatusCode::NOT_FOUND,
            code: "unknown_command",
            message,
        }
    }

    fn bind_error(message: String) -> Self {
        Self {
            status: StatusCode::BAD_REQUEST,
            code: "bind_error",
            message,
        }
    }

    fn write_confirmation_required(message: String) -> Self {
        Self {
            status: StatusCode::FORBIDDEN,
            code: "write_confirmation_required",
            message,
        }
    }

    fn execution_error(message: String) -> Self {
        Self {
            status: StatusCode::BAD_REQUEST,
            code: "execution_error",
            message,
        }
    }
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        (
            self.status,
            Json(serde_json::json!({
                "error": {
                    "code": self.code,
                    "message": self.message,
                }
            })),
        )
            .into_response()
    }
}

#[cfg(test)]
mod tests {
    use std::fs;
    use std::path::Path;
    use std::sync::{Mutex, OnceLock};

    use axum::body::{Body, to_bytes};
    use axum::http::Request;
    use serde_json::Value;
    use tower::util::ServiceExt;

    use super::{WebState, build_router};

    const READ_TEMPLATE: &str = r#"
version = 1
provider = "demo"

command "echo" {
  title = "Echo"
  summary = "Echo text"
  description = "Prints the provided value."

  annotations {
    mode = "read"
    secrets = []
  }

  param "value" {
    type = "string"
    required = true
  }

  operation {
    protocol = "cli"
    cli {
      command = "printf"
      args = ["%s", "{{ args.value }}"]
    }
  }

  result {
    decode = "text"
    output = "{{ result }}"
  }
}
"#;

    const WRITE_TEMPLATE: &str = r#"
version = 1
provider = "demo"

command "write_echo" {
  title = "Write Echo"
  summary = "Write operation"
  description = "Write mode command."

  annotations {
    mode = "write"
    secrets = []
  }

  operation {
    protocol = "cli"
    cli {
      command = "printf"
      args = ["%s", "ok"]
    }
  }

  result {
    decode = "text"
    output = "{{ result }}"
  }
}
"#;

    #[tokio::test]
    async fn api_tools_returns_expected_schema() {
        let cwd = tempfile::tempdir().unwrap();
        write_template(cwd.path(), "demo.hcl", READ_TEMPLATE);

        let app = build_router(WebState {
            cwd: cwd.path().to_path_buf(),
        });

        let response = app
            .oneshot(
                Request::builder()
                    .uri("/api/tools")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(response.status(), 200);

        let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
        let parsed: Value = serde_json::from_slice(&body).unwrap();

        let tools = parsed.as_array().unwrap();
        assert_eq!(tools.len(), 1);
        assert_eq!(tools[0]["key"], "demo.echo");
        assert_eq!(tools[0]["protocol"], "cli");
        assert!(
            tools[0]["example_expression"]
                .as_str()
                .unwrap()
                .contains("demo.echo")
        );
    }

    #[tokio::test]
    async fn execute_rejects_write_mode_without_confirmation() {
        let _guard = env_lock();
        let cwd = tempfile::tempdir().unwrap();
        let home = tempfile::tempdir().unwrap();
        write_template(cwd.path(), "demo.hcl", WRITE_TEMPLATE);
        write_config(
            home.path(),
            "[network]\nallow_cli_commands = [\"printf\"]\n",
        );

        with_home(home.path(), || async {
            let app = build_router(WebState {
                cwd: cwd.path().to_path_buf(),
            });

            let response = app
                .oneshot(
                    Request::builder()
                        .method("POST")
                        .uri("/api/execute")
                        .header("content-type", "application/json")
                        .body(Body::from(
                            serde_json::json!({
                                "expression": "demo.write_echo()"
                            })
                            .to_string(),
                        ))
                        .unwrap(),
                )
                .await
                .unwrap();

            assert_eq!(response.status(), 403);
            let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
            let parsed: Value = serde_json::from_slice(&body).unwrap();
            assert_eq!(parsed["error"]["code"], "write_confirmation_required");
        })
        .await;
    }

    #[tokio::test]
    async fn execute_returns_human_output_for_read_command() {
        let _guard = env_lock();
        let cwd = tempfile::tempdir().unwrap();
        let home = tempfile::tempdir().unwrap();
        write_template(cwd.path(), "demo.hcl", READ_TEMPLATE);
        write_config(
            home.path(),
            "[network]\nallow_cli_commands = [\"printf\"]\n",
        );

        with_home(home.path(), || async {
            let app = build_router(WebState {
                cwd: cwd.path().to_path_buf(),
            });

            let response = app
                .oneshot(
                    Request::builder()
                        .method("POST")
                        .uri("/api/execute")
                        .header("content-type", "application/json")
                        .body(Body::from(
                            serde_json::json!({
                                "expression": "demo.echo(value=\"hello\")",
                                "confirm_write": false
                            })
                            .to_string(),
                        ))
                        .unwrap(),
                )
                .await
                .unwrap();

            assert_eq!(response.status(), 200);
            let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
            let parsed: Value = serde_json::from_slice(&body).unwrap();

            assert_eq!(parsed["key"], "demo.echo");
            assert_eq!(parsed["mode"], "read");
            assert_eq!(parsed["human_output"], "hello");
            assert_eq!(parsed["decoded"], "hello");
        })
        .await;
    }

    async fn with_home<F, Fut>(path: &Path, run: F)
    where
        F: FnOnce() -> Fut,
        Fut: std::future::Future<Output = ()>,
    {
        let previous = std::env::var_os("HOME");
        // SAFETY: tests serialize HOME mutations with a global mutex.
        unsafe { std::env::set_var("HOME", path) };
        run().await;
        match previous {
            // SAFETY: tests serialize HOME mutations with a global mutex.
            Some(home) => unsafe { std::env::set_var("HOME", home) },
            // SAFETY: tests serialize HOME mutations with a global mutex.
            None => unsafe { std::env::remove_var("HOME") },
        }
    }

    fn write_template(cwd: &Path, name: &str, content: &str) {
        let template_dir = cwd.join("templates");
        fs::create_dir_all(&template_dir).unwrap();
        fs::write(template_dir.join(name), content).unwrap();
    }

    fn write_config(home: &Path, content: &str) {
        let config_dir = home.join(".config/earl");
        fs::create_dir_all(&config_dir).unwrap();
        fs::write(config_dir.join("config.toml"), content).unwrap();
    }

    fn env_lock() -> std::sync::MutexGuard<'static, ()> {
        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
        LOCK.get_or_init(|| Mutex::new(())).lock().unwrap()
    }
}
