// AI Gateway-administrasjon (oppgave 15.4)
//
// Admin-API for modelloversikt, ruting-regler, fallback-kjeder og forbruksoversikt.
// PG er single source of truth — LiteLLM er stateløs proxy.
//
// Ref: docs/infra/ai_gateway.md

use axum::{extract::State, http::StatusCode, Json};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use uuid::Uuid;

use crate::auth::AdminUser;
use crate::AppState;

// =============================================================================
// Datatyper
// =============================================================================

#[derive(Serialize, sqlx::FromRow)]
pub struct AiModelAlias {
    pub id: Uuid,
    pub alias: String,
    pub description: Option<String>,
    pub is_active: bool,
    pub created_at: DateTime<Utc>,
}

#[derive(Serialize, sqlx::FromRow)]
pub struct AiModelProvider {
    pub id: Uuid,
    pub alias_id: Uuid,
    pub provider: String,
    pub model: String,
    pub api_key_env: String,
    pub priority: i16,
    pub is_active: bool,
}

#[derive(Serialize, sqlx::FromRow)]
pub struct AiJobRouting {
    pub job_type: String,
    pub alias: String,
    pub description: Option<String>,
}

#[derive(Serialize, sqlx::FromRow)]
pub struct AiUsageSummary {
    pub collection_node_id: Option<Uuid>,
    pub collection_title: Option<String>,
    pub model_alias: String,
    pub job_type: Option<String>,
    pub total_prompt_tokens: i64,
    pub total_completion_tokens: i64,
    pub total_tokens: i64,
    pub estimated_cost: f64,
    pub call_count: i64,
}

#[derive(Serialize)]
pub struct ApiKeyStatus {
    pub env_var: String,
    pub is_set: bool,
}

#[derive(Serialize)]
pub struct AiOverviewResponse {
    pub aliases: Vec<AiModelAlias>,
    pub providers: Vec<AiModelProvider>,
    pub routing: Vec<AiJobRouting>,
    pub usage: Vec<AiUsageSummary>,
    pub api_key_status: Vec<ApiKeyStatus>,
}

#[derive(Serialize)]
pub struct ErrorResponse {
    pub error: String,
}

fn bad_request(msg: &str) -> (StatusCode, Json<ErrorResponse>) {
    (StatusCode::BAD_REQUEST, Json(ErrorResponse { error: msg.to_string() }))
}

fn internal_error(msg: &str) -> (StatusCode, Json<ErrorResponse>) {
    (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: msg.to_string() }))
}

// =============================================================================
// Modelloppslag fra ai_job_routing — brukes av jobbkø-dispatchers
// =============================================================================

/// Slå opp modellalias fra ai_job_routing for en gitt jobbtype/kontekst.
/// Returnerer None hvis ingen regel er konfigurert.
pub async fn resolve_routing(db: &PgPool, job_type: &str) -> Result<Option<String>, sqlx::Error> {
    let row: Option<(String,)> =
        sqlx::query_as("SELECT alias FROM ai_job_routing WHERE job_type = $1")
            .bind(job_type)
            .fetch_optional(db)
            .await?;
    Ok(row.map(|(alias,)| alias))
}

/// Slå opp modellalias med fallback til standard-alias.
/// Brukes av dispatchers som trenger en modell uansett.
pub async fn resolve_routing_or_default(db: &PgPool, job_type: &str) -> String {
    match resolve_routing(db, job_type).await {
        Ok(Some(alias)) => {
            tracing::debug!(job_type, alias = %alias, "Modell fra ai_job_routing");
            alias
        }
        Ok(None) => {
            tracing::warn!(job_type, "Ingen rutingregel — bruker synops/low");
            "synops/low".to_string()
        }
        Err(e) => {
            tracing::error!(job_type, error = %e, "Feil ved oppslag i ai_job_routing — bruker fallback");
            "synops/low".to_string()
        }
    }
}

// =============================================================================
// GET /admin/ai — oversikt
// =============================================================================

pub async fn ai_overview(
    State(state): State<AppState>,
    _admin: AdminUser,
) -> Result<Json<AiOverviewResponse>, (StatusCode, Json<ErrorResponse>)> {
    let aliases = sqlx::query_as::<_, AiModelAlias>(
        "SELECT id, alias, description, is_active, created_at FROM ai_model_aliases ORDER BY alias"
    )
    .fetch_all(&state.db)
    .await
    .map_err(|e| internal_error(&format!("Feil ved henting av aliaser: {e}")))?;

    let providers = sqlx::query_as::<_, AiModelProvider>(
        "SELECT id, alias_id, provider, model, api_key_env, priority, is_active
         FROM ai_model_providers ORDER BY alias_id, priority"
    )
    .fetch_all(&state.db)
    .await
    .map_err(|e| internal_error(&format!("Feil ved henting av providers: {e}")))?;

    let routing = sqlx::query_as::<_, AiJobRouting>(
        "SELECT job_type, alias, description FROM ai_job_routing ORDER BY job_type"
    )
    .fetch_all(&state.db)
    .await
    .map_err(|e| internal_error(&format!("Feil ved henting av ruting: {e}")))?;

    let usage = fetch_usage_summary(&state.db, 30).await
        .map_err(|e| internal_error(&format!("Feil ved henting av forbruk: {e}")))?;

    // Sjekk hvilke API-nøkler som er satt i miljøet
    let env_vars: Vec<String> = providers.iter()
        .map(|p| p.api_key_env.clone())
        .collect::<std::collections::HashSet<_>>()
        .into_iter()
        .collect();
    let mut api_key_status: Vec<ApiKeyStatus> = env_vars.into_iter().map(|env_var| {
        let is_set = std::env::var(&env_var).map(|v| !v.is_empty()).unwrap_or(false);
        ApiKeyStatus { env_var, is_set }
    }).collect();
    api_key_status.sort_by(|a, b| a.env_var.cmp(&b.env_var));

    Ok(Json(AiOverviewResponse {
        aliases,
        providers,
        routing,
        usage,
        api_key_status,
    }))
}

async fn fetch_usage_summary(db: &PgPool, days: i32) -> Result<Vec<AiUsageSummary>, sqlx::Error> {
    sqlx::query_as::<_, AiUsageSummary>(
        r#"
        SELECT
            u.collection_node_id,
            n.title AS collection_title,
            u.model_alias,
            u.job_type,
            COALESCE(SUM(u.prompt_tokens), 0)::BIGINT AS total_prompt_tokens,
            COALESCE(SUM(u.completion_tokens), 0)::BIGINT AS total_completion_tokens,
            COALESCE(SUM(u.total_tokens), 0)::BIGINT AS total_tokens,
            COALESCE(SUM(u.estimated_cost)::FLOAT8, 0.0) AS estimated_cost,
            COUNT(*)::BIGINT AS call_count
        FROM ai_usage_log u
        LEFT JOIN nodes n ON n.id = u.collection_node_id
        WHERE u.created_at >= now() - make_interval(days := $1)
        GROUP BY u.collection_node_id, n.title, u.model_alias, u.job_type
        ORDER BY total_tokens DESC
        "#,
    )
    .bind(days)
    .fetch_all(db)
    .await
}

// =============================================================================
// POST /admin/ai/update_alias — oppdater alias
// =============================================================================

#[derive(Deserialize)]
pub struct UpdateAliasRequest {
    pub id: Uuid,
    pub description: Option<String>,
    pub is_active: bool,
}

pub async fn update_alias(
    State(state): State<AppState>,
    _admin: AdminUser,
    Json(req): Json<UpdateAliasRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorResponse>)> {
    let rows = sqlx::query(
        "UPDATE ai_model_aliases SET description = $1, is_active = $2 WHERE id = $3"
    )
    .bind(&req.description)
    .bind(req.is_active)
    .bind(req.id)
    .execute(&state.db)
    .await
    .map_err(|e| internal_error(&format!("Feil ved oppdatering av alias: {e}")))?;

    if rows.rows_affected() == 0 {
        return Err(bad_request("Alias ikke funnet"));
    }

    tracing::info!(alias_id = %req.id, user = %_admin.node_id, "Admin: alias oppdatert");
    Ok(Json(serde_json::json!({ "success": true })))
}

// =============================================================================
// POST /admin/ai/create_alias — opprett nytt alias
// =============================================================================

#[derive(Deserialize)]
pub struct CreateAliasRequest {
    pub alias: String,
    pub description: Option<String>,
}

pub async fn create_alias(
    State(state): State<AppState>,
    _admin: AdminUser,
    Json(req): Json<CreateAliasRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorResponse>)> {
    if req.alias.trim().is_empty() {
        return Err(bad_request("Alias-navn kan ikke være tomt"));
    }

    let id = sqlx::query_scalar::<_, Uuid>(
        "INSERT INTO ai_model_aliases (alias, description) VALUES ($1, $2) RETURNING id"
    )
    .bind(&req.alias)
    .bind(&req.description)
    .fetch_one(&state.db)
    .await
    .map_err(|e| {
        if e.to_string().contains("unique") || e.to_string().contains("duplicate") {
            bad_request(&format!("Alias '{}' finnes allerede", req.alias))
        } else {
            internal_error(&format!("Feil ved opprettelse av alias: {e}"))
        }
    })?;

    tracing::info!(alias = %req.alias, user = %_admin.node_id, "Admin: alias opprettet");
    Ok(Json(serde_json::json!({ "id": id, "success": true })))
}

// =============================================================================
// POST /admin/ai/update_provider — oppdater provider
// =============================================================================

#[derive(Deserialize)]
pub struct UpdateProviderRequest {
    pub id: Uuid,
    pub priority: Option<i16>,
    pub is_active: Option<bool>,
}

pub async fn update_provider(
    State(state): State<AppState>,
    _admin: AdminUser,
    Json(req): Json<UpdateProviderRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorResponse>)> {
    if let Some(priority) = req.priority {
        sqlx::query("UPDATE ai_model_providers SET priority = $1 WHERE id = $2")
            .bind(priority)
            .bind(req.id)
            .execute(&state.db)
            .await
            .map_err(|e| internal_error(&format!("Feil ved oppdatering av provider priority: {e}")))?;
    }
    if let Some(is_active) = req.is_active {
        sqlx::query("UPDATE ai_model_providers SET is_active = $1 WHERE id = $2")
            .bind(is_active)
            .bind(req.id)
            .execute(&state.db)
            .await
            .map_err(|e| internal_error(&format!("Feil ved oppdatering av provider status: {e}")))?;
    }

    tracing::info!(provider_id = %req.id, user = %_admin.node_id, "Admin: provider oppdatert");
    Ok(Json(serde_json::json!({ "success": true })))
}

// =============================================================================
// POST /admin/ai/create_provider — legg til ny provider
// =============================================================================

#[derive(Deserialize)]
pub struct CreateProviderRequest {
    pub alias_id: Uuid,
    pub provider: String,
    pub model: String,
    pub api_key_env: String,
    pub priority: i16,
}

pub async fn create_provider(
    State(state): State<AppState>,
    _admin: AdminUser,
    Json(req): Json<CreateProviderRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorResponse>)> {
    if req.model.trim().is_empty() {
        return Err(bad_request("Modellnavn kan ikke være tomt"));
    }

    let id = sqlx::query_scalar::<_, Uuid>(
        "INSERT INTO ai_model_providers (alias_id, provider, model, api_key_env, priority)
         VALUES ($1, $2, $3, $4, $5) RETURNING id"
    )
    .bind(req.alias_id)
    .bind(&req.provider)
    .bind(&req.model)
    .bind(&req.api_key_env)
    .bind(req.priority)
    .fetch_one(&state.db)
    .await
    .map_err(|e| {
        if e.to_string().contains("unique") || e.to_string().contains("duplicate") {
            bad_request("Denne modellen finnes allerede for dette aliaset")
        } else if e.to_string().contains("foreign key") {
            bad_request("Alias-ID finnes ikke")
        } else {
            internal_error(&format!("Feil ved opprettelse av provider: {e}"))
        }
    })?;

    tracing::info!(model = %req.model, alias_id = %req.alias_id, user = %_admin.node_id, "Admin: provider opprettet");
    Ok(Json(serde_json::json!({ "id": id, "success": true })))
}

// =============================================================================
// POST /admin/ai/delete_provider — fjern provider
// =============================================================================

#[derive(Deserialize)]
pub struct DeleteProviderRequest {
    pub id: Uuid,
}

pub async fn delete_provider(
    State(state): State<AppState>,
    _admin: AdminUser,
    Json(req): Json<DeleteProviderRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorResponse>)> {
    let rows = sqlx::query("DELETE FROM ai_model_providers WHERE id = $1")
        .bind(req.id)
        .execute(&state.db)
        .await
        .map_err(|e| internal_error(&format!("Feil ved sletting av provider: {e}")))?;

    if rows.rows_affected() == 0 {
        return Err(bad_request("Provider ikke funnet"));
    }

    tracing::info!(provider_id = %req.id, user = %_admin.node_id, "Admin: provider slettet");
    Ok(Json(serde_json::json!({ "success": true })))
}

// =============================================================================
// POST /admin/ai/update_routing — oppdater eller opprett rutingregel
// =============================================================================

#[derive(Deserialize)]
pub struct UpdateRoutingRequest {
    pub job_type: String,
    pub alias: String,
    pub description: Option<String>,
}

pub async fn update_routing(
    State(state): State<AppState>,
    _admin: AdminUser,
    Json(req): Json<UpdateRoutingRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorResponse>)> {
    if req.job_type.trim().is_empty() || req.alias.trim().is_empty() {
        return Err(bad_request("Jobbtype og alias kan ikke være tomme"));
    }

    sqlx::query(
        "INSERT INTO ai_job_routing (job_type, alias, description)
         VALUES ($1, $2, $3)
         ON CONFLICT (job_type) DO UPDATE SET alias = $2, description = $3"
    )
    .bind(&req.job_type)
    .bind(&req.alias)
    .bind(&req.description)
    .execute(&state.db)
    .await
    .map_err(|e| internal_error(&format!("Feil ved oppdatering av ruting: {e}")))?;

    tracing::info!(job_type = %req.job_type, alias = %req.alias, user = %_admin.node_id, "Admin: ruting oppdatert");
    Ok(Json(serde_json::json!({ "success": true })))
}

// =============================================================================
// POST /admin/ai/delete_routing — slett rutingregel
// =============================================================================

#[derive(Deserialize)]
pub struct DeleteRoutingRequest {
    pub job_type: String,
}

pub async fn delete_routing(
    State(state): State<AppState>,
    _admin: AdminUser,
    Json(req): Json<DeleteRoutingRequest>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<ErrorResponse>)> {
    let rows = sqlx::query("DELETE FROM ai_job_routing WHERE job_type = $1")
        .bind(&req.job_type)
        .execute(&state.db)
        .await
        .map_err(|e| internal_error(&format!("Feil ved sletting av ruting: {e}")))?;

    if rows.rows_affected() == 0 {
        return Err(bad_request("Rutingregel ikke funnet"));
    }

    tracing::info!(job_type = %req.job_type, user = %_admin.node_id, "Admin: ruting slettet");
    Ok(Json(serde_json::json!({ "success": true })))
}

// =============================================================================
// GET /admin/ai/usage — detaljert forbruksoversikt med filtre
// =============================================================================

#[derive(Deserialize)]
pub struct UsageQueryParams {
    pub days: Option<i32>,
    pub collection_id: Option<Uuid>,
}

pub async fn ai_usage(
    State(state): State<AppState>,
    _admin: AdminUser,
    axum::extract::Query(params): axum::extract::Query<UsageQueryParams>,
) -> Result<Json<Vec<AiUsageSummary>>, (StatusCode, Json<ErrorResponse>)> {
    let days = params.days.unwrap_or(30).min(365);

    let usage = if let Some(collection_id) = params.collection_id {
        fetch_usage_for_collection(&state.db, collection_id, days).await
    } else {
        fetch_usage_summary(&state.db, days).await
    };

    usage
        .map(Json)
        .map_err(|e| internal_error(&format!("Feil ved henting av forbruk: {e}")))
}

// =============================================================================
// POST /admin/ai/test_prompt — test et alias med en enkel prompt
// =============================================================================

#[derive(Deserialize)]
pub struct TestPromptRequest {
    pub alias: String,
}

#[derive(Serialize)]
pub struct TestPromptResponse {
    pub success: bool,
    pub response_text: String,
    pub model_used: Option<String>,
    pub prompt_tokens: i64,
    pub completion_tokens: i64,
    pub latency_ms: u64,
    pub estimated_cost: f64,
}

/// Kjente modellpriser per million tokens (input, output).
/// Brukes for kostnadsestimat i admin-panelet.
fn model_cost_per_million(model: &str) -> (f64, f64) {
    match model {
        m if m.contains("gemini-2.5-flash-lite") => (0.0, 0.0), // gratis tier
        m if m.contains("gemini-2.5-flash") => (0.15, 0.60),
        m if m.contains("grok-4-1-fast-non-reasoning") => (0.60, 3.00),
        m if m.contains("claude-sonnet-4") => (3.00, 15.00),
        m if m.contains("claude-opus") => (15.00, 75.00),
        m if m.contains("gpt-4o") => (2.50, 10.00),
        m if m.contains("gpt-4o-mini") => (0.15, 0.60),
        _ => (1.00, 3.00), // konservativt estimat
    }
}

pub fn estimate_cost(model: &str, prompt_tokens: i64, completion_tokens: i64) -> f64 {
    let (input_price, output_price) = model_cost_per_million(model);
    (prompt_tokens as f64 * input_price + completion_tokens as f64 * output_price) / 1_000_000.0
}

pub async fn test_prompt(
    State(_state): State<AppState>,
    _admin: AdminUser,
    Json(req): Json<TestPromptRequest>,
) -> Result<Json<TestPromptResponse>, (StatusCode, Json<ErrorResponse>)> {
    if req.alias.trim().is_empty() {
        return Err(bad_request("Alias kan ikke være tomt"));
    }

    let gateway_url = std::env::var("AI_GATEWAY_URL")
        .unwrap_or_else(|_| "http://localhost:4000".to_string());
    let api_key = std::env::var("LITELLM_MASTER_KEY")
        .map_err(|_| internal_error("LITELLM_MASTER_KEY ikke satt"))?;

    let request_body = serde_json::json!({
        "model": req.alias,
        "messages": [
            {"role": "system", "content": "Du er en hjelpsom assistent. Svar kort og konsist."},
            {"role": "user", "content": "Si «Hei fra Synops!» og beskriv deg selv i én setning."}
        ],
        "temperature": 0.3
    });

    let client = reqwest::Client::new();
    let start = std::time::Instant::now();

    let resp = client
        .post(format!("{gateway_url}/v1/chat/completions"))
        .header("Authorization", format!("Bearer {api_key}"))
        .header("Content-Type", "application/json")
        .json(&request_body)
        .timeout(std::time::Duration::from_secs(30))
        .send()
        .await
        .map_err(|e| internal_error(&format!("AI Gateway-kall feilet: {e}")))?;

    let latency_ms = start.elapsed().as_millis() as u64;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        return Err(internal_error(&format!("AI Gateway returnerte {status}: {body}")));
    }

    let body: serde_json::Value = resp.json().await
        .map_err(|e| internal_error(&format!("Kunne ikke parse respons: {e}")))?;

    let response_text = body["choices"][0]["message"]["content"]
        .as_str()
        .unwrap_or("(ingen respons)")
        .to_string();
    let model_used = body["model"].as_str().map(|s| s.to_string());
    let prompt_tokens = body["usage"]["prompt_tokens"].as_i64().unwrap_or(0);
    let completion_tokens = body["usage"]["completion_tokens"].as_i64().unwrap_or(0);

    let cost_model = model_used.as_deref().unwrap_or(&req.alias);
    let estimated_cost = estimate_cost(cost_model, prompt_tokens, completion_tokens);

    tracing::info!(
        alias = %req.alias,
        model = ?model_used,
        latency_ms,
        prompt_tokens,
        completion_tokens,
        user = %_admin.node_id,
        "Admin: test-prompt kjørt"
    );

    Ok(Json(TestPromptResponse {
        success: true,
        response_text,
        model_used,
        prompt_tokens,
        completion_tokens,
        latency_ms,
        estimated_cost,
    }))
}

// =============================================================================
// GET /admin/ai/tier_costs — kostnadsestimat per nivå
// =============================================================================

#[derive(Serialize)]
pub struct TierCostInfo {
    pub alias: String,
    pub description: Option<String>,
    pub providers: Vec<ProviderCostInfo>,
}

#[derive(Serialize)]
pub struct ProviderCostInfo {
    pub model: String,
    pub provider: String,
    pub priority: i16,
    pub input_cost_per_mtok: f64,
    pub output_cost_per_mtok: f64,
    pub estimated_cost_1k_tokens: f64,
}

pub async fn tier_costs(
    State(state): State<AppState>,
    _admin: AdminUser,
) -> Result<Json<Vec<TierCostInfo>>, (StatusCode, Json<ErrorResponse>)> {
    let aliases = sqlx::query_as::<_, AiModelAlias>(
        "SELECT id, alias, description, is_active, created_at FROM ai_model_aliases
         WHERE alias LIKE 'synops/%' ORDER BY alias"
    )
    .fetch_all(&state.db)
    .await
    .map_err(|e| internal_error(&format!("Feil: {e}")))?;

    let providers = sqlx::query_as::<_, AiModelProvider>(
        "SELECT id, alias_id, provider, model, api_key_env, priority, is_active
         FROM ai_model_providers ORDER BY priority"
    )
    .fetch_all(&state.db)
    .await
    .map_err(|e| internal_error(&format!("Feil: {e}")))?;

    let tiers: Vec<TierCostInfo> = aliases.iter().map(|a| {
        let alias_providers: Vec<ProviderCostInfo> = providers.iter()
            .filter(|p| p.alias_id == a.id && p.is_active)
            .map(|p| {
                let (input_cost, output_cost) = model_cost_per_million(&p.model);
                ProviderCostInfo {
                    model: p.model.clone(),
                    provider: p.provider.clone(),
                    priority: p.priority,
                    input_cost_per_mtok: input_cost,
                    output_cost_per_mtok: output_cost,
                    // Estimat: 700 input + 300 output tokens per 1k
                    estimated_cost_1k_tokens: (700.0 * input_cost + 300.0 * output_cost) / 1_000_000.0,
                }
            })
            .collect();
        TierCostInfo {
            alias: a.alias.clone(),
            description: a.description.clone(),
            providers: alias_providers,
        }
    }).collect();

    Ok(Json(tiers))
}

async fn fetch_usage_for_collection(
    db: &PgPool,
    collection_id: Uuid,
    days: i32,
) -> Result<Vec<AiUsageSummary>, sqlx::Error> {
    sqlx::query_as::<_, AiUsageSummary>(
        r#"
        SELECT
            u.collection_node_id,
            n.title AS collection_title,
            u.model_alias,
            u.job_type,
            COALESCE(SUM(u.prompt_tokens), 0)::BIGINT AS total_prompt_tokens,
            COALESCE(SUM(u.completion_tokens), 0)::BIGINT AS total_completion_tokens,
            COALESCE(SUM(u.total_tokens), 0)::BIGINT AS total_tokens,
            COALESCE(SUM(u.estimated_cost)::FLOAT8, 0.0) AS estimated_cost,
            COUNT(*)::BIGINT AS call_count
        FROM ai_usage_log u
        LEFT JOIN nodes n ON n.id = u.collection_node_id
        WHERE u.created_at >= now() - make_interval(days := $1)
          AND u.collection_node_id = $2
        GROUP BY u.collection_node_id, n.title, u.model_alias, u.job_type
        ORDER BY total_tokens DESC
        "#,
    )
    .bind(days)
    .bind(collection_id)
    .fetch_all(db)
    .await
}
