use std::sync::Arc;

use axum::body::{to_bytes, Body};
use axum::http::{header, HeaderValue, Method, Request, StatusCode};
use axum::response::Response;
use serde_json::{json, Value};

use crate::db::{
    get_events, get_node_docker_statuses, get_node_system_statuses, get_nodes, get_task, get_tasks,
};
use crate::performance::with_operation;
use crate::state::AppState;

pub type HttpResponse = Response;

pub fn json_response(value: Value, status: u16) -> Response {
    let status = StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
    let mut response = Response::builder()
        .status(status)
        .header(header::CONTENT_TYPE, "application/json; charset=utf-8")
        .header("access-control-allow-origin", "*")
        .header(
            "access-control-allow-methods",
            "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS",
        )
        .header(
            "access-control-allow-headers",
            "content-type,x-provider-token",
        )
        .body(Body::from(
            serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_string()),
        ))
        .unwrap();
    response.headers_mut().insert(
        header::CONTENT_TYPE,
        HeaderValue::from_static("application/json; charset=utf-8"),
    );
    response
}

pub fn text_response(text: impl Into<String>, status: u16) -> Response {
    Response::builder()
        .status(StatusCode::from_u16(status).unwrap_or(StatusCode::OK))
        .header(header::CONTENT_TYPE, "text/plain; charset=utf-8")
        .header("access-control-allow-origin", "*")
        .body(Body::from(text.into()))
        .unwrap()
}

pub fn error_json(error: &anyhow::Error) -> Value {
    json!({ "name": "Error", "message": error.to_string() })
}

fn is_dev_identity(state: &AppState) -> bool {
    state.config.identity.environment == "dev" || state.config.identity.namespace == "unidesk-dev"
}

fn health_payload(state: &AppState) -> Value {
    let base = json!({
        "ok": true,
        "service": "unidesk-core",
        "dbReady": state.db_ready(),
        "startedAt": state.service_started_at.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
    });
    if !is_dev_identity(state) {
        return base;
    }
    let identity = &state.config.identity;
    json!({
        "ok": true,
        "service": "unidesk-core",
        "dbReady": state.db_ready(),
        "startedAt": state.service_started_at.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
        "environment": identity.environment.clone(),
        "namespace": identity.namespace.clone(),
        "databaseName": identity.database_name.clone(),
        "serviceId": identity.service_id.clone(),
        "deployRef": identity.deploy_ref.clone(),
        "deploy": {
            "repo": identity.repo.clone(),
            "commit": identity.commit.clone(),
            "requestedCommit": identity.requested_commit.clone(),
        },
    })
}

pub async fn route_api(state: Arc<AppState>, request: Request<Body>) -> Response {
    let uri = request.uri().clone();
    let path = uri.path().to_string();
    let method = request.method().clone();
    if method == Method::OPTIONS {
        return json_response(json!({ "ok": true }), 200);
    }
    let result = route_api_inner(state.clone(), request).await;
    match result {
        Ok(response) => response,
        Err(error) => {
            state.log(
                "error",
                "request_failed",
                Some(json!({ "path": path, "error": error.to_string() })),
            );
            json_response(json!({ "ok": false, "error": error.to_string() }), 500)
        }
    }
}

async fn route_api_inner(state: Arc<AppState>, request: Request<Body>) -> anyhow::Result<Response> {
    let method = request.method().clone();
    let uri = request.uri().clone();
    let path = uri.path().to_string();
    let query = uri.query().unwrap_or("");
    if !state.db_ready() && path.starts_with("/api/") {
        return Ok(json_response(
            json!({ "ok": false, "error": "database not ready" }),
            503,
        ));
    }
    let url = ParsedUrl::new(&path, query);
    match (method.as_str(), path.as_str()) {
        ("GET", "/health") | ("GET", "/") => Ok(json_response(health_payload(&state), 200)),
        ("GET", "/api/overview") => {
            let value = with_operation(
                &state,
                "core",
                "overview",
                &path,
                crate::overview::get_overview(&state),
            )
            .await?;
            Ok(json_response(value, 200))
        }
        ("GET", "/api/nodes") => {
            let nodes = with_operation(&state, "core", "nodes", &path, get_nodes(&state)).await?;
            Ok(json_response(json!({ "ok": true, "nodes": nodes }), 200))
        }
        ("GET", "/api/nodes/system-status") => {
            let limit = read_limit(&url, 60);
            let statuses = with_operation(
                &state,
                "core",
                "node_system_status",
                query,
                get_node_system_statuses(&state, limit),
            )
            .await?;
            Ok(json_response(
                json!({ "ok": true, "systemStatuses": statuses }),
                200,
            ))
        }
        ("GET", "/api/nodes/docker-status") => {
            let statuses = with_operation(
                &state,
                "core",
                "node_docker_status",
                &path,
                get_node_docker_statuses(&state),
            )
            .await?;
            Ok(json_response(
                json!({ "ok": true, "dockerStatuses": statuses }),
                200,
            ))
        }
        ("GET", "/api/events") => {
            let events = with_operation(
                &state,
                "core",
                "events",
                query,
                get_events(&state, read_limit(&url, 100)),
            )
            .await?;
            Ok(json_response(json!({ "ok": true, "events": events }), 200))
        }
        ("GET", "/api/tasks") => {
            let lite = truthy(url.param("lite"));
            let summary = truthy(url.param("summary"));
            let status = url.param("status").unwrap_or("all");
            let tasks = with_operation(
                &state,
                "core",
                "tasks",
                query,
                get_tasks(&state, read_limit(&url, 100), status, lite, summary),
            )
            .await?;
            Ok(json_response(json!({ "ok": true, "tasks": tasks }), 200))
        }
        ("GET", "/api/microservices") => {
            let microservices = with_operation(
                &state,
                "core",
                "microservices",
                &path,
                crate::microservice_proxy::get_microservices(&state),
            )
            .await?;
            Ok(json_response(
                json!({ "ok": true, "microservices": microservices }),
                200,
            ))
        }
        ("GET", "/api/performance") => Ok(json_response(
            crate::performance::get_performance(&state).await?,
            200,
        )),
        ("GET", "/logs") => Ok(json_response(
            json!({ "ok": true, "logs": state.logger.recent(read_limit(&url, 100) as usize) }),
            200,
        )),
        ("GET", "/favicon.ico") => Ok(text_response("", 204)),
        _ if path.starts_with("/api/tasks/") && method == Method::GET => {
            let task_id = percent_decode(&path["/api/tasks/".len()..]);
            if task_id.is_empty() || task_id.contains('/') {
                return Ok(json_response(
                    json!({ "ok": false, "error": "invalid task id" }),
                    400,
                ));
            }
            let task = with_operation(
                &state,
                "core",
                "task_detail",
                &task_id,
                get_task(&state, &task_id),
            )
            .await?;
            match task {
                Some(task) => Ok(json_response(json!({ "ok": true, "task": task }), 200)),
                None => Ok(json_response(
                    json!({ "ok": false, "error": format!("task not found: {task_id}") }),
                    404,
                )),
            }
        }
        _ if path == "/api/schedules" || path.starts_with("/api/schedules/") => {
            let response = with_operation(
                &state,
                "scheduler",
                "schedules",
                &path,
                crate::scheduler::scheduled_task_route(
                    &state,
                    method,
                    &path,
                    query,
                    body_bytes(request).await?,
                ),
            )
            .await?;
            Ok(response)
        }
        _ if path == "/api/code-queue-load-test"
            && (method == Method::GET || method == Method::POST) =>
        {
            let body = body_bytes(request).await?;
            let response = with_operation(
                &state,
                "performance",
                "code_queue_load_test",
                &path,
                crate::overview::codex_queue_load_test(&state, method, body),
            )
            .await?;
            Ok(response)
        }
        _ if path.starts_with("/api/microservices/") => {
            let response = with_operation(
                &state,
                "microservices",
                "route",
                &path,
                crate::microservice_proxy::microservice_route(&state, method, uri, request),
            )
            .await?;
            Ok(response)
        }
        _ if path == "/api/dispatch" && method == Method::POST => {
            let body = body_bytes(request).await?;
            let response = with_operation(&state, "scheduler", "dispatch", &path, async {
                Ok(crate::task_dispatcher::dispatch_task(&state, body).await)
            })
            .await?;
            Ok(response)
        }
        _ => Ok(json_response(
            json!({ "ok": false, "error": "not found", "path": path }),
            404,
        )),
    }
}

async fn body_bytes(request: Request<Body>) -> anyhow::Result<bytes::Bytes> {
    Ok(to_bytes(request.into_body(), 16 * 1024 * 1024).await?)
}

pub fn response_with_body(status: u16, content_type: &str, body: impl Into<Body>) -> Response {
    Response::builder()
        .status(StatusCode::from_u16(status).unwrap_or(StatusCode::OK))
        .header(header::CONTENT_TYPE, content_type)
        .body(body.into())
        .unwrap()
}

pub fn add_header(response: &mut Response, name: &'static str, value: &str) {
    if let Ok(value) = HeaderValue::from_str(value) {
        response.headers_mut().insert(name, value);
    }
}

pub async fn response_to_text(response: Response) -> anyhow::Result<(u16, String, String)> {
    let status = response.status().as_u16();
    let content_type = response
        .headers()
        .get(header::CONTENT_TYPE)
        .and_then(|value| value.to_str().ok())
        .unwrap_or("application/octet-stream")
        .to_string();
    let bytes = to_bytes(response.into_body(), 16 * 1024 * 1024).await?;
    let body_text = String::from_utf8_lossy(&bytes).to_string();
    Ok((status, content_type, body_text))
}

pub fn read_limit(url: &ParsedUrl, default_limit: i64) -> i64 {
    url.param("limit")
        .and_then(|raw| raw.parse::<i64>().ok())
        .filter(|value| *value > 0)
        .map(|value| value.min(500))
        .unwrap_or(default_limit)
}

pub fn truthy(value: Option<&str>) -> bool {
    value.is_some_and(|value| matches!(value.to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
}

pub fn percent_decode(value: &str) -> String {
    percent_encoding::percent_decode_str(value)
        .decode_utf8_lossy()
        .to_string()
}

#[derive(Debug, Clone)]
pub struct ParsedUrl {
    pub path: String,
    pub params: Vec<(String, String)>,
}

impl ParsedUrl {
    pub fn new(path: &str, query: &str) -> Self {
        let params = url::form_urlencoded::parse(query.as_bytes())
            .map(|(key, value)| (key.into_owned(), value.into_owned()))
            .collect();
        Self {
            path: path.to_string(),
            params,
        }
    }

    pub fn param(&self, name: &str) -> Option<&str> {
        self.params
            .iter()
            .find(|(key, _)| key == name)
            .map(|(_, value)| value.as_str())
    }

    pub fn all(&self, name: &str) -> Vec<&str> {
        self.params
            .iter()
            .filter(|(key, _)| key == name)
            .map(|(_, value)| value.as_str())
            .collect()
    }
}

pub async fn internal_fetch_json(
    url: &str,
    method: &str,
    body: Option<Value>,
    max_response_bytes: usize,
) -> anyhow::Result<Value> {
    let client = reqwest::Client::new();
    let mut request = client.request(method.parse()?, url);
    if let Some(body) = body {
        request = request.json(&body);
    }
    let response = request.send().await?;
    let status = response.status().as_u16();
    let ok = response.status().is_success();
    let content_length = response.content_length();
    let bytes = response.bytes().await?;
    let response_truncated = bytes.len() > max_response_bytes;
    let kept = if response_truncated {
        &bytes[..max_response_bytes]
    } else {
        &bytes[..]
    };
    let text = String::from_utf8_lossy(kept).to_string();
    let body = if response_truncated {
        json!({ "_unideskResponseTruncated": true, "maxResponseBytes": max_response_bytes, "bytesRead": kept.len(), "contentLength": content_length, "textPreview": text })
    } else {
        serde_json::from_str(&text).unwrap_or_else(|_| json!({ "text": text }))
    };
    Ok(
        json!({ "ok": ok, "status": status, "responseTruncated": response_truncated, "responseBytesRead": kept.len(), "responseContentLength": content_length, "body": body }),
    )
}

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

    #[test]
    fn parsed_url_preserves_path_and_repeated_query_values() {
        let parsed = ParsedUrl::new("/api/tasks", "limit=999&limit=10&flag=yes&name=a%20b");

        assert_eq!(parsed.path, "/api/tasks");
        assert_eq!(parsed.param("name"), Some("a b"));
        assert_eq!(parsed.all("limit"), vec!["999", "10"]);
        assert_eq!(read_limit(&parsed, 30), 500);
        assert!(truthy(parsed.param("flag")));
    }
}
