use std::collections::BTreeMap;
use std::future::Future;
use std::sync::Arc;
use std::time::Duration;

use chrono::Utc;
use fabro_slack::config::{
    SlackCredentialResolution,
    resolve_credentials_status_with_lookup as resolve_slack_credentials_status_with_lookup,
};
use fabro_static::EnvVars;
use fabro_types::settings::server::GithubIntegrationSettings;
use fabro_vault::Vault;
use tokio::time::timeout;

use super::super::{
    AggregateBilling, AggregateBillingTotals, ApiError, AppState, BilledTokenCounts,
    BillingByModel, DfParams, FABRO_VERSION, GithubIntegrationStrategy, IntegrationConnectionState,
    IntegrationProvider, IntegrationStatus, IntoResponse, Json, Path, PruneRunsRequest,
    PruneRunsResponse, Query, RequiredUser, Response, Router, RunStatus, State, StatusCode,
    SystemInfoResponse, SystemIntegrationStatus, SystemIntegrationsResponse, SystemRepairRunIssue,
    SystemRepairRunsResponse, SystemRunCounts, build_disk_usage_response, build_prune_plan,
    counts_toward_scheduler_capacity, delete_run_internal, diagnostics, get, post,
    resource_sampler, spawn_blocking, system_sandbox_provider, to_i64,
};

const SERVER_DIAGNOSTICS_TIMEOUT: Duration = Duration::from_secs(25);

pub(super) fn routes() -> Router<Arc<AppState>> {
    Router::new()
        .route("/repos/github/{owner}/{name}", get(get_github_repo))
        .route("/health", get(health))
        .route("/health/diagnostics", post(run_diagnostics))
        .route("/settings", get(get_server_settings))
        .route("/system/info", get(get_system_info))
        .route("/system/integrations", get(get_system_integrations))
        .route("/system/resources", get(get_system_resources))
        .route("/system/df", get(get_system_df))
        .route("/system/repair/runs", get(get_system_repair_runs))
        .route("/system/prune/runs", post(prune_runs))
        .route("/billing", get(get_aggregate_billing))
}

pub(in crate::server) async fn health() -> Response {
    Json(serde_json::json!({
        "status": "ok",
    }))
    .into_response()
}

async fn get_server_settings(_auth: RequiredUser, State(state): State<Arc<AppState>>) -> Response {
    (
        StatusCode::OK,
        Json(state.server_settings().as_ref().clone()),
    )
        .into_response()
}

async fn get_system_info(_auth: RequiredUser, State(state): State<Arc<AppState>>) -> Response {
    let manifest_run_settings = state.manifest_run_settings();
    let (total_runs, active_runs, scheduler_slots_used) = {
        let runs = state.runs.lock().expect("runs lock poisoned");
        let active = runs
            .values()
            .filter(|run| {
                matches!(
                    run.status,
                    RunStatus::Pending { .. }
                        | RunStatus::Runnable
                        | RunStatus::Starting
                        | RunStatus::Running
                        | RunStatus::Blocked { .. }
                        | RunStatus::Paused { .. }
                )
            })
            .count();
        let scheduler_slots_used = runs
            .values()
            .filter(|run| counts_toward_scheduler_capacity(run.status))
            .count();
        (runs.len(), active, scheduler_slots_used)
    };

    let response = SystemInfoResponse {
        version:          Some(FABRO_VERSION.to_string()),
        server_url:       Some(state.effective_web_url()),
        git_sha:          option_env!("FABRO_GIT_SHA").map(str::to_string),
        build_date:       option_env!("FABRO_BUILD_DATE").map(str::to_string),
        profile:          option_env!("FABRO_BUILD_PROFILE").map(str::to_string),
        os:               Some(std::env::consts::OS.to_string()),
        arch:             Some(std::env::consts::ARCH.to_string()),
        storage_engine:   Some("slatedb".to_string()),
        storage_dir:      Some(state.server_storage_dir().display().to_string()),
        uptime_secs:      Some(to_i64(state.started_at.elapsed().as_secs())),
        runs:             Some(SystemRunCounts {
            total:                Some(to_i64(total_runs)),
            active:               Some(to_i64(active_runs)),
            scheduler_slots_used: Some(to_i64(scheduler_slots_used)),
        }),
        sandbox_provider: Some(system_sandbox_provider(&manifest_run_settings)),
    };
    (StatusCode::OK, Json(response)).into_response()
}

async fn get_system_integrations(
    _auth: RequiredUser,
    State(state): State<Arc<AppState>>,
) -> Response {
    let settings = state.server_settings();
    let vault = match state.stores.vault.snapshot().await {
        Ok(vault) => vault,
        Err(err) => return secret_store_failure(&err),
    };
    let github = github_integration_status(&settings.server.integrations.github, &vault);
    let slack = slack_integration_status(state.as_ref(), &vault);
    let response = SystemIntegrationsResponse {
        data: vec![github, slack],
    };
    (StatusCode::OK, Json(response)).into_response()
}

fn github_integration_status(
    settings: &GithubIntegrationSettings,
    vault: &Vault,
) -> SystemIntegrationStatus {
    let mut metadata = BTreeMap::new();
    metadata.insert(
        "strategy".to_string(),
        match settings.strategy {
            GithubIntegrationStrategy::Token => "token",
            GithubIntegrationStrategy::App => "app",
        }
        .to_string(),
    );
    if let Some(slug) = settings.slug.as_ref() {
        metadata.insert("slug".to_string(), slug.clone());
    }
    if let Some(app_id) = settings.app_id.as_ref() {
        metadata.insert("app_id".to_string(), app_id.clone());
    }

    if !settings.enabled {
        return integration_status(
            IntegrationProvider::Github,
            false,
            false,
            IntegrationStatus::Disabled,
            Vec::new(),
            metadata,
        );
    }

    let mut missing = Vec::new();
    match settings.strategy {
        GithubIntegrationStrategy::Token => {
            if missing_vault_secret(vault, EnvVars::GITHUB_TOKEN) {
                missing.push(EnvVars::GITHUB_TOKEN.to_string());
            }
        }
        GithubIntegrationStrategy::App => {
            if settings.app_id.is_none() {
                missing.push("server.integrations.github.app_id".to_string());
            }
            if settings.client_id.is_none() {
                missing.push("server.integrations.github.client_id".to_string());
            }
            if missing_vault_secret(vault, EnvVars::GITHUB_APP_CLIENT_SECRET) {
                missing.push(EnvVars::GITHUB_APP_CLIENT_SECRET.to_string());
            }
            if missing_vault_secret(vault, EnvVars::GITHUB_APP_PRIVATE_KEY) {
                missing.push(EnvVars::GITHUB_APP_PRIVATE_KEY.to_string());
            }
        }
    }
    missing.sort();

    let configured = missing.is_empty();
    integration_status(
        IntegrationProvider::Github,
        true,
        configured,
        if configured {
            IntegrationStatus::Configured
        } else {
            IntegrationStatus::MissingCredentials
        },
        missing,
        metadata,
    )
}

fn slack_integration_status(state: &AppState, vault: &Vault) -> SystemIntegrationStatus {
    let settings = &state.server_settings().server.integrations.slack;
    let mut metadata = BTreeMap::new();
    if let Some(default_channel) = settings.default_channel.as_ref() {
        metadata.insert("default_channel".to_string(), default_channel.clone());
    }

    if !settings.enabled {
        return integration_status(
            IntegrationProvider::Slack,
            false,
            false,
            IntegrationStatus::Disabled,
            Vec::new(),
            metadata,
        );
    }

    let mut missing = match resolve_slack_credentials_status_with_lookup(|name| {
        vault.get(name).map(str::to_string)
    }) {
        SlackCredentialResolution::Configured(_) => Vec::new(),
        SlackCredentialResolution::Missing { env_vars } => {
            env_vars.into_iter().map(str::to_string).collect()
        }
    };
    missing.sort();
    if !missing.is_empty() {
        return integration_status(
            IntegrationProvider::Slack,
            true,
            false,
            IntegrationStatus::MissingCredentials,
            missing,
            metadata,
        );
    }

    let connection = state
        .slack_service
        .as_ref()
        .map(|service| service.connection_status());
    let status = connection
        .as_ref()
        .map_or(
            IntegrationStatus::Configured,
            |connection| match connection.status {
                IntegrationConnectionState::Connecting => IntegrationStatus::Connecting,
                IntegrationConnectionState::Connected => IntegrationStatus::Connected,
                IntegrationConnectionState::Error => IntegrationStatus::Error,
            },
        );

    SystemIntegrationStatus {
        provider: IntegrationProvider::Slack,
        enabled: true,
        configured: true,
        status,
        missing_credentials: Vec::new(),
        connection,
        metadata,
    }
}

fn integration_status(
    provider: IntegrationProvider,
    enabled: bool,
    configured: bool,
    status: IntegrationStatus,
    missing_credentials: Vec<String>,
    metadata: BTreeMap<String, String>,
) -> SystemIntegrationStatus {
    SystemIntegrationStatus {
        provider,
        enabled,
        configured,
        status,
        missing_credentials,
        connection: None,
        metadata,
    }
}

fn missing_vault_secret(vault: &Vault, name: &str) -> bool {
    vault.get(name).map(str::trim).is_none_or(str::is_empty)
}

fn secret_store_failure(err: &fabro_vault::SecretStoreError) -> Response {
    tracing::error!(error = ?err, "Loading integration secrets failed");
    ApiError::new(
        StatusCode::INTERNAL_SERVER_ERROR,
        "secret store operation failed",
    )
    .into_response()
}

async fn get_system_resources(_auth: RequiredUser, State(state): State<Arc<AppState>>) -> Response {
    match resource_sampler::sample_system_resources(&state).await {
        Ok(response) => (StatusCode::OK, Json(response)).into_response(),
        Err(err) => {
            ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response()
        }
    }
}

async fn get_system_df(
    _auth: RequiredUser,
    State(state): State<Arc<AppState>>,
    Query(params): Query<DfParams>,
) -> Response {
    let storage_dir = state.server_storage_dir();
    let summaries = match state
        .stores
        .runs
        .list_runs(&fabro_store::ListRunsQuery::default(), Utc::now())
        .await
    {
        Ok(summaries) => summaries,
        Err(err) => {
            return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
                .into_response();
        }
    };

    let response = match spawn_blocking(move || {
        build_disk_usage_response(&summaries, &storage_dir, params.verbose)
    })
    .await
    {
        Ok(Ok(response)) => response,
        Ok(Err(err)) => {
            return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
                .into_response();
        }
        Err(err) => {
            return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
                .into_response();
        }
    };

    (StatusCode::OK, Json(response)).into_response()
}

async fn get_system_repair_runs(
    _auth: RequiredUser,
    State(state): State<Arc<AppState>>,
) -> Response {
    let issues = match state.stores.runs.list_unreadable_runs().await {
        Ok(issues) => issues,
        Err(err) => {
            return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
                .into_response();
        }
    };
    let total_count = to_i64(issues.len());
    let runs = issues
        .into_iter()
        .map(|issue| SystemRepairRunIssue {
            run_id:     issue.run_id.to_string(),
            created_at: issue.created_at,
            error:      issue.error,
        })
        .collect();

    (
        StatusCode::OK,
        Json(SystemRepairRunsResponse { runs, total_count }),
    )
        .into_response()
}

async fn prune_runs(
    _auth: RequiredUser,
    State(state): State<Arc<AppState>>,
    Json(body): Json<PruneRunsRequest>,
) -> Response {
    let storage_dir = state.server_storage_dir();
    let summaries = match state
        .stores
        .runs
        .list_runs(&fabro_store::ListRunsQuery::default(), Utc::now())
        .await
    {
        Ok(summaries) => summaries,
        Err(err) => {
            return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
                .into_response();
        }
    };

    let dry_run = body.dry_run;
    let body_for_plan = body.clone();
    let prune_plan =
        match spawn_blocking(move || build_prune_plan(&body_for_plan, &summaries, &storage_dir))
            .await
        {
            Ok(Ok(plan)) => plan,
            Ok(Err(err)) => {
                return ApiError::new(StatusCode::BAD_REQUEST, err.to_string()).into_response();
            }
            Err(err) => {
                return ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
                    .into_response();
            }
        };

    if dry_run {
        return (
            StatusCode::OK,
            Json(PruneRunsResponse {
                dry_run:          Some(true),
                runs:             Some(prune_plan.rows),
                total_count:      Some(to_i64(prune_plan.run_ids.len())),
                total_size_bytes: Some(to_i64(prune_plan.total_size_bytes)),
                deleted_count:    Some(0),
                freed_bytes:      Some(0),
            }),
        )
            .into_response();
    }

    for run_id in &prune_plan.run_ids {
        if let Err(error) = delete_run_internal(state.as_ref(), *run_id, true).await {
            return error.into_response();
        }
    }

    (
        StatusCode::OK,
        Json(PruneRunsResponse {
            dry_run:          Some(false),
            runs:             None,
            total_count:      Some(to_i64(prune_plan.run_ids.len())),
            total_size_bytes: Some(to_i64(prune_plan.total_size_bytes)),
            deleted_count:    Some(to_i64(prune_plan.run_ids.len())),
            freed_bytes:      Some(to_i64(prune_plan.total_size_bytes)),
        }),
    )
        .into_response()
}

#[derive(serde::Deserialize)]
struct GitHubRepoResponse {
    default_branch: String,
    private:        bool,
    permissions:    Option<serde_json::Value>,
}

/// Reject owner/repo path segments that could rewrite the GitHub API endpoint
/// via `..` traversal after URL normalization. Conservative compared to
/// GitHub's real rules, which is fine for server-side input validation.
#[allow(
    clippy::result_large_err,
    reason = "GitHub slug validation returns HTTP 400 responses directly."
)]
pub(in crate::server) fn validate_github_slug(
    kind: &str,
    value: &str,
    max_len: usize,
) -> Result<(), Response> {
    if value.is_empty() || value.len() > max_len || matches!(value, "." | "..") {
        return Err(ApiError::bad_request(format!("invalid github {kind}")).into_response());
    }
    if !value
        .bytes()
        .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.'))
    {
        return Err(ApiError::bad_request(format!("invalid github {kind}")).into_response());
    }
    Ok(())
}

async fn get_github_repo(
    _auth: RequiredUser,
    State(state): State<Arc<AppState>>,
    Path((owner, name)): Path<(String, String)>,
) -> Response {
    if let Err(response) = validate_github_slug("owner", &owner, 39) {
        return response;
    }
    if let Err(response) = validate_github_slug("repo", &name, 100) {
        return response;
    }
    let settings = state.server_settings();
    let github_settings = &settings.server.integrations.github;
    let base_url = fabro_github::github_api_base_url();
    let (token, client) = match github_settings.strategy {
        GithubIntegrationStrategy::App => {
            if github_settings.app_id.is_none() {
                return ApiError::new(
                    StatusCode::SERVICE_UNAVAILABLE,
                    "server.integrations.github.app_id is not configured",
                )
                .into_response();
            }
            let creds = match state.github_credentials(github_settings).await {
                Ok(Some(fabro_github::GitHubCredentials::App(creds))) => creds,
                Ok(Some(_)) => unreachable!("app strategy should not return token credentials"),
                Ok(None) => {
                    return ApiError::new(
                        StatusCode::SERVICE_UNAVAILABLE,
                        "GITHUB_APP_PRIVATE_KEY is not configured",
                    )
                    .into_response();
                }
                Err(err) => {
                    tracing::error!(error = ?err, "Loading GitHub credentials failed");
                    return ApiError::new(
                        StatusCode::SERVICE_UNAVAILABLE,
                        "GitHub credentials are unavailable",
                    )
                    .into_response();
                }
            };

            let jwt = match fabro_github::sign_app_jwt(&creds.app_id, &creds.private_key_pem) {
                Ok(jwt) => jwt,
                Err(err) => {
                    tracing::error!(error = ?err, "failed to sign GitHub App JWT");
                    return ApiError::new(StatusCode::SERVICE_UNAVAILABLE, err.to_string())
                        .into_response();
                }
            };
            let install_url = creds.installation_url(&owner).unwrap_or_else(|| {
                format!("https://github.com/organizations/{owner}/settings/installations")
            });

            let client = match state.http_client() {
                Ok(http) => http,
                Err(err) => {
                    return ApiError::new(StatusCode::SERVICE_UNAVAILABLE, err.to_string())
                        .into_response();
                }
            };
            let installed =
                match fabro_github::check_app_installed(&client, &jwt, &owner, &name, &base_url)
                    .await
                {
                    Ok(installed) => installed,
                    Err(err) => {
                        tracing::error!(error = ?err, "failed to check GitHub App installation");
                        return ApiError::new(StatusCode::BAD_GATEWAY, err.to_string())
                            .into_response();
                    }
                };

            if !installed {
                return (
                    StatusCode::OK,
                    Json(serde_json::json!({
                        "owner": owner,
                        "name": name,
                        "accessible": false,
                        "default_branch": null,
                        "private": null,
                        "permissions": null,
                        "install_url": install_url,
                    })),
                )
                    .into_response();
            }

            match fabro_github::create_installation_access_token_with_permissions_and_install_url(
                &client,
                &jwt,
                &owner,
                &name,
                &base_url,
                serde_json::json!({ "contents": "write", "pull_requests": "write" }),
                Some(&install_url),
            )
            .await
            {
                Ok(token) => (token, client),
                Err(err) => {
                    tracing::error!(
                        error = ?err,
                        "failed to create GitHub App installation token"
                    );
                    return ApiError::new(StatusCode::BAD_GATEWAY, err.to_string()).into_response();
                }
            }
        }
        GithubIntegrationStrategy::Token => {
            let token = match state.github_credentials(github_settings).await {
                Ok(Some(fabro_github::GitHubCredentials::Pat(token))) => token,
                Ok(Some(fabro_github::GitHubCredentials::Installation(token))) => {
                    match token.valid_token() {
                        Ok(token) => token.to_string(),
                        Err(err) => {
                            return ApiError::new(StatusCode::SERVICE_UNAVAILABLE, err.to_string())
                                .into_response();
                        }
                    }
                }
                Ok(Some(_)) => unreachable!("token strategy should not return app credentials"),
                Ok(None) => {
                    return ApiError::new(
                        StatusCode::SERVICE_UNAVAILABLE,
                        "GITHUB_TOKEN is not configured -- run fabro install or run fabro secret set GITHUB_TOKEN",
                    )
                    .into_response();
                }
                Err(err) => {
                    tracing::error!(error = ?err, "Loading GitHub credentials failed");
                    return ApiError::new(
                        StatusCode::SERVICE_UNAVAILABLE,
                        "GitHub credentials are unavailable",
                    )
                    .into_response();
                }
            };
            let client = match state.http_client() {
                Ok(http) => http,
                Err(err) => {
                    return ApiError::new(StatusCode::SERVICE_UNAVAILABLE, err.to_string())
                        .into_response();
                }
            };
            (token, client)
        }
    };
    let repo_response = match client
        .get(format!("{base_url}/repos/{owner}/{name}"))
        .header("Authorization", format!("Bearer {token}"))
        .header("Accept", "application/vnd.github+json")
        .header("User-Agent", "fabro-server")
        .send()
        .await
    {
        Ok(response) if response.status().is_success() => response,
        Ok(response)
            if github_settings.strategy == GithubIntegrationStrategy::Token
                && matches!(
                    response.status(),
                    fabro_http::StatusCode::FORBIDDEN | fabro_http::StatusCode::NOT_FOUND
                ) =>
        {
            return (
                StatusCode::OK,
                Json(serde_json::json!({
                    "owner": owner,
                    "name": name,
                    "accessible": false,
                    "default_branch": null,
                    "private": null,
                    "permissions": null,
                    "install_url": serde_json::Value::Null,
                })),
            )
                .into_response();
        }
        Ok(response)
            if github_settings.strategy == GithubIntegrationStrategy::Token
                && response.status() == fabro_http::StatusCode::UNAUTHORIZED =>
        {
            return ApiError::new(
                StatusCode::SERVICE_UNAVAILABLE,
                "Stored GitHub token is invalid -- run fabro install or run fabro secret set GITHUB_TOKEN",
            )
            .into_response();
        }
        Ok(response) => {
            return ApiError::new(
                StatusCode::BAD_GATEWAY,
                format!("GitHub repo lookup failed: {}", response.status()),
            )
            .into_response();
        }
        Err(err) => return ApiError::new(StatusCode::BAD_GATEWAY, err.to_string()).into_response(),
    };

    let repo = match repo_response.json::<GitHubRepoResponse>().await {
        Ok(repo) => repo,
        Err(err) => {
            return ApiError::new(
                StatusCode::BAD_GATEWAY,
                format!("Failed to parse GitHub repo response: {err}"),
            )
            .into_response();
        }
    };

    (
        StatusCode::OK,
        Json(serde_json::json!({
            "owner": owner,
            "name": name,
            "accessible": true,
            "default_branch": repo.default_branch,
            "private": repo.private,
            "permissions": repo.permissions,
            "install_url": serde_json::Value::Null,
        })),
    )
        .into_response()
}

async fn run_diagnostics(_auth: RequiredUser, State(state): State<Arc<AppState>>) -> Response {
    diagnostics_response_with_timeout(
        Box::pin(diagnostics::run_all(state.as_ref())),
        SERVER_DIAGNOSTICS_TIMEOUT,
    )
    .await
}

async fn diagnostics_response_with_timeout<F>(
    diagnostics: F,
    operation_timeout: Duration,
) -> Response
where
    F: Future<Output = diagnostics::DiagnosticsReport>,
{
    let Ok(report) = timeout(operation_timeout, diagnostics).await else {
        tracing::warn!(
            timeout_secs = operation_timeout.as_secs(),
            "server diagnostics timed out"
        );
        return ApiError::with_code(
            StatusCode::GATEWAY_TIMEOUT,
            "Server diagnostics timed out.",
            "diagnostics_timeout",
        )
        .into_response();
    };

    (StatusCode::OK, Json(report)).into_response()
}

pub(in crate::server) async fn openapi_spec() -> Response {
    let yaml = include_str!("../../../../../../docs/public/api-reference/fabro-api.yaml");
    let value: serde_json::Value =
        serde_yaml::from_str(yaml).expect("embedded OpenAPI YAML is invalid");
    Json(value).into_response()
}

async fn get_aggregate_billing(
    _auth: RequiredUser,
    State(state): State<Arc<AppState>>,
) -> Response {
    let agg = state
        .aggregate_billing
        .lock()
        .expect("aggregate_billing lock poisoned");
    let by_model: Vec<BillingByModel> = agg
        .by_model
        .iter()
        .map(|(model, totals)| BillingByModel {
            billing: totals.billing.clone(),
            model:   model.clone(),
            stages:  totals.stages,
        })
        .collect();
    let total_billing =
        agg.by_model
            .values()
            .fold(BilledTokenCounts::default(), |mut acc, totals| {
                acc.add_counts(&totals.billing);
                acc
            });
    let response = AggregateBilling {
        totals: AggregateBillingTotals {
            cache_read_tokens:  total_billing.cache_read_tokens,
            cache_write_tokens: total_billing.cache_write_tokens,
            input_tokens:       total_billing.input_tokens,
            output_tokens:      total_billing.output_tokens,
            reasoning_tokens:   total_billing.reasoning_tokens,
            runs:               agg.total_runs,
            timing:             agg.total_timing,
            total_tokens:       total_billing.total_tokens,
            total_usd_micros:   total_billing.total_usd_micros,
        },
        by_model,
    };
    (StatusCode::OK, Json(response)).into_response()
}

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

    #[tokio::test]
    async fn diagnostics_response_returns_gateway_timeout_before_client_deadline() {
        let response = diagnostics_response_with_timeout(
            std::future::pending::<diagnostics::DiagnosticsReport>(),
            Duration::from_millis(1),
        )
        .await;

        let body = fabro_test::expect_axum_json(
            response,
            StatusCode::GATEWAY_TIMEOUT,
            "GET /api/v1/system/diagnostics timeout",
        )
        .await;
        assert_eq!(body["errors"][0]["code"], "diagnostics_timeout");
        assert_eq!(body["errors"][0]["detail"], "Server diagnostics timed out.");
    }
}
