use crate::AppState;
use axum::{
    extract::{Path, Query, State},
    http::StatusCode,
    response::IntoResponse,
    routing::{get, patch},
    Json, Router,
};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use sqlx::types::Uuid;
use contracts::auth_middleware::{AuthUser, require_admin};

pub fn router() -> Router<AppState> {
    Router::new()
        .route("/", get(list_external_roles).post(create_external_role))
        .route("/by-key/{role_key}", get(get_external_role_by_key))
        .route("/by-key/{role_key}", patch(update_external_role_by_key))
        .route("/{id}", get(get_external_role).put(update_external_role).delete(delete_external_role))
}

#[derive(Deserialize)]
struct ListQuery {
    q: Option<String>,
    status: Option<String>,
    vertical: Option<String>,
    category: Option<String>,
    page: Option<i64>,
    per_page: Option<i64>,
}

#[derive(Serialize)]
struct ExternalRoleRow {
    id: Uuid,
    name: String,
    code: String,
    persona_type: Option<String>,
    vertical: Option<String>,
    category: Option<String>,
    onboarding_schema_id: Option<String>,
    modules: Vec<String>,
    permissions: serde_json::Map<String, JsonValue>,
    requires_onboarding_approval: bool,
    requires_lead_approval: bool,
    requires_job_approval: bool,
    feature_limits: JsonValue,
    status: String,
    assigned_users: i64,
    assigned_user_types: Vec<String>,
    created_date: chrono::DateTime<chrono::Utc>,
    updated_at: Option<chrono::DateTime<chrono::Utc>>,
}

#[derive(Serialize)]
struct ListResponse {
    roles: Vec<ExternalRoleRow>,
    total: i64,
    page: i64,
    per_page: i64,
}

#[derive(sqlx::FromRow)]
struct ExternalRoleListRow {
    id: Uuid,
    name: String,
    code: String,
    persona_type: Option<String>,
    is_active: bool,
    created_date: chrono::DateTime<chrono::Utc>,
    updated_at: Option<chrono::DateTime<chrono::Utc>>,
    config_json: Option<serde_json::Value>,
}

async fn list_external_roles(
    auth: AuthUser,
    State(state): State<AppState>,
    Query(q): Query<ListQuery>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    if let Err(_e) = require_admin(&auth) {
        return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
    }
    let page = q.page.unwrap_or(1).max(1);
    let per_page = q.per_page.unwrap_or(20).min(100);
    let offset = (page - 1) * per_page;
    let search = q.q.unwrap_or_default().to_lowercase();
    let status = q.status.unwrap_or_default().to_uppercase();
    let vertical = q.vertical.unwrap_or_default().to_lowercase();
    let category = q.category.unwrap_or_default().to_lowercase();

    let rows = sqlx::query_as::<_, ExternalRoleListRow>(
        r#"
        SELECT
            r.id,
            r.name,
            r.key as code,
            r.persona_type,
            r.is_active,
            r.created_at as created_date,
            rc.updated_at as "updated_at",
            rc.config_json as "config_json"
        FROM roles r
        LEFT JOIN role_runtime_configs rc ON rc.role_id = r.id AND rc.is_active = true
        WHERE r.audience = 'EXTERNAL'
          AND ($1 = '' OR LOWER(r.name) LIKE '%' || $1 || '%' OR LOWER(r.key) LIKE '%' || $1 || '%')
          AND ($2 = '' OR (CASE WHEN $2 = 'ACTIVE' THEN r.is_active ELSE NOT r.is_active END))
        ORDER BY r.created_at DESC
        LIMIT $3 OFFSET $4
        "#,
    )
    .bind(&search)
    .bind(&status)
    .bind(per_page)
    .bind(offset)
    .fetch_all(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    let total: i64 = sqlx::query_scalar::<_, i64>(
        r#"
        SELECT COUNT(*)
        FROM roles r
        WHERE r.audience = 'EXTERNAL'
          AND ($1 = '' OR LOWER(r.name) LIKE '%' || $1 || '%' OR LOWER(r.key) LIKE '%' || $1 || '%')
          AND ($2 = '' OR (CASE WHEN $2 = 'ACTIVE' THEN r.is_active ELSE NOT r.is_active END))
        "#,
    )
    .bind(&search)
    .bind(&status)
    .fetch_one(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    let mut roles: Vec<ExternalRoleRow> = Vec::with_capacity(rows.len());
    for row in rows {
        let (mut vertical_v, mut category_v, mut onboarding_schema_id, mut modules, mut permissions, mut requires_onboarding, mut requires_lead, mut requires_job, mut feature_limits, mut assigned_user_types) = (None, None, None, Vec::new(), serde_json::Map::new(), false, false, false, serde_json::json!({}), Vec::new());
        if let Some(cfg) = row.config_json {
            vertical_v = cfg.get("vertical").and_then(|v| v.as_str()).map(|s| s.to_string());
            category_v = cfg.get("category").and_then(|v| v.as_str()).map(|s| s.to_string());
            onboarding_schema_id = cfg.get("onboarding_schema_id").and_then(|v| v.as_str()).map(|s| s.to_string());
            if let Some(arr) = cfg.get("modules").and_then(|v| v.as_array()) {
                modules = arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect();
            }
            if let Some(obj) = cfg.get("permissions").and_then(|v| v.as_object()) {
                permissions = obj.clone();
            }
            requires_onboarding = cfg.pointer("/requires/onboarding").and_then(|v| v.as_bool()).unwrap_or(false);
            requires_lead = cfg.pointer("/requires/lead").and_then(|v| v.as_bool()).unwrap_or(false);
            requires_job = cfg.pointer("/requires/job").and_then(|v| v.as_bool()).unwrap_or(false);
            feature_limits = cfg.get("feature_limits").cloned().unwrap_or_else(|| serde_json::json!({}));
            if let Some(arr) = cfg.get("assigned_user_types").and_then(|v| v.as_array()) {
                assigned_user_types = arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect();
            }
        }
        if !vertical.is_empty() && vertical_v.as_deref() != Some(vertical.as_str()) {
            continue;
        }
        if !category.is_empty() && category_v.as_deref() != Some(category.as_str()) {
            continue;
        }
        let assigned_users: i64 = sqlx::query_scalar::<_, i64>(
            "SELECT COUNT(*) FROM user_role_assignments WHERE role_id = $1 AND status = 'APPROVED'",
        )
        .bind(row.id)
        .fetch_one(&state.pool)
        .await
        .unwrap_or(0);

        roles.push(ExternalRoleRow {
            id: row.id,
            name: row.name,
            code: row.code,
            persona_type: row.persona_type.or(vertical_v.clone()),
            vertical: vertical_v,
            category: category_v,
            onboarding_schema_id,
            modules,
            permissions,
            requires_onboarding_approval: requires_onboarding,
            requires_lead_approval: requires_lead,
            requires_job_approval: requires_job,
            feature_limits,
            status: if row.is_active { "ACTIVE".to_string() } else { "INACTIVE".to_string() },
            assigned_users,
            assigned_user_types,
            created_date: row.created_date,
            updated_at: row.updated_at,
        });
    }

    Ok(Json(ListResponse { roles, total, page, per_page }))
}

#[derive(Serialize)]
struct ExternalRoleDetail {
    id: Uuid,
    name: String,
    code: String,
    audience: String,
    is_active: bool,
    runtime: JsonValue,
    created_at: chrono::DateTime<chrono::Utc>,
    updated_at: Option<chrono::DateTime<chrono::Utc>>,
}

#[derive(sqlx::FromRow)]
struct ExternalRoleDetailRow {
    id: Uuid,
    name: String,
    code: String,
    audience: String,
    is_active: bool,
    created_at: chrono::DateTime<chrono::Utc>,
    updated_at: Option<chrono::DateTime<chrono::Utc>>,
    config_json: Option<serde_json::Value>,
}

async fn get_external_role(
    auth: AuthUser,
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    if let Err(_e) = require_admin(&auth) {
        return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
    }
    let row = sqlx::query_as::<_, ExternalRoleDetailRow>(
        r#"
        SELECT r.id, r.name, r.key as code, r.audience, r.is_active, r.created_at,
               rc.updated_at as updated_at, rc.config_json as config_json
        FROM roles r
        LEFT JOIN role_runtime_configs rc ON rc.role_id = r.id AND rc.is_active = true
        WHERE r.id = $1 AND r.audience = 'EXTERNAL'
        "#,
    )
    .bind(id)
    .fetch_optional(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
    .ok_or((StatusCode::NOT_FOUND, "External role not found".to_string()))?;

    Ok(Json(ExternalRoleDetail {
        id: row.id,
        name: row.name,
        code: row.code,
        audience: row.audience,
        is_active: row.is_active,
        runtime: row.config_json.unwrap_or_else(|| serde_json::json!({})),
        created_at: row.created_at,
        updated_at: row.updated_at,
    }))
}

async fn get_external_role_by_key(
    auth: AuthUser,
    State(state): State<AppState>,
    Path(role_key): Path<String>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    if let Err(_e) = require_admin(&auth) {
        return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
    }
    let row = sqlx::query_as::<_, ExternalRoleDetailRow>(
        r#"
        SELECT r.id, r.name, r.key as code, r.audience, r.is_active, r.created_at,
               rc.updated_at as updated_at, rc.config_json as config_json
        FROM roles r
        LEFT JOIN role_runtime_configs rc ON rc.role_id = r.id AND rc.is_active = true
        WHERE r.key = $1 AND r.audience = 'EXTERNAL'
        "#,
    )
    .bind(&role_key)
    .fetch_optional(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
    .ok_or((StatusCode::NOT_FOUND, "External role not found".to_string()))?;

    Ok(Json(ExternalRoleDetail {
        id: row.id,
        name: row.name,
        code: row.code,
        audience: row.audience,
        is_active: row.is_active,
        runtime: row.config_json.unwrap_or_else(|| serde_json::json!({})),
        created_at: row.created_at,
        updated_at: row.updated_at,
    }))
}

async fn update_external_role_by_key(
    auth: AuthUser,
    State(state): State<AppState>,
    Path(role_key): Path<String>,
    Json(payload): Json<UpdateExternalRolePayload>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    if let Err(_e) = require_admin(&auth) {
        return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
    }
    let row: (Uuid,) = sqlx::query_as("SELECT id FROM roles WHERE key = $1 AND audience = 'EXTERNAL'")
        .bind(&role_key)
        .fetch_optional(&state.pool)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
        .ok_or((StatusCode::NOT_FOUND, "External role not found".to_string()))?;

    update_external_role_impl(&state, row.0, payload).await
}

#[derive(Deserialize)]
struct CreateExternalRolePayload {
    name: String,
    code: String,
    is_active: Option<bool>,
    persona_type: Option<String>,
    runtime: Option<JsonValue>,
}

#[derive(sqlx::FromRow)]
struct InsertedRole {
    id: Uuid,
    key: String,
    name: String,
    audience: String,
    is_active: bool,
    created_at: chrono::DateTime<chrono::Utc>,
}

#[derive(sqlx::FromRow)]
struct InsertedRc {
    updated_at: chrono::DateTime<chrono::Utc>,
}

async fn create_external_role(
    auth: AuthUser,
    State(state): State<AppState>,
    Json(payload): Json<CreateExternalRolePayload>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    if let Err(_e) = require_admin(&auth) {
        return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
    }
    let is_active = payload.is_active.unwrap_or(true);
    let role = sqlx::query_as::<_, InsertedRole>(
        r#"
        INSERT INTO roles (key, name, audience, is_active, persona_type)
        VALUES ($1, $2, 'EXTERNAL', $3, $4)
        RETURNING id, key, name, audience, is_active, created_at
        "#,
    )
    .bind(payload.code.to_uppercase())
    .bind(&payload.name)
    .bind(is_active)
    .bind(&payload.persona_type)
    .fetch_one(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    let runtime = payload.runtime.unwrap_or_else(|| serde_json::json!({}));
    let rc = sqlx::query_as::<_, InsertedRc>(
        r#"
        INSERT INTO role_runtime_configs (role_id, config_json, version, is_active)
        VALUES ($1, $2, 1, true)
        RETURNING updated_at
        "#,
    )
    .bind(role.id)
    .bind(&runtime)
    .fetch_one(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

    Ok((
        StatusCode::CREATED,
        Json(ExternalRoleDetail {
            id: role.id,
            name: role.name,
            code: role.key,
            audience: role.audience,
            is_active: role.is_active,
            runtime,
            created_at: role.created_at,
            updated_at: Some(rc.updated_at),
        }),
    ))
}

#[derive(Deserialize)]
struct UpdateExternalRolePayload {
    name: Option<String>,
    is_active: Option<bool>,
    runtime: Option<JsonValue>,
}

async fn update_external_role(
    auth: AuthUser,
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
    Json(payload): Json<UpdateExternalRolePayload>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    if let Err(_e) = require_admin(&auth) {
        return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
    }
    update_external_role_impl(&state, id, payload).await
}

async fn update_external_role_impl(
    state: &AppState,
    role_id: Uuid,
    payload: UpdateExternalRolePayload,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    if payload.name.is_some() || payload.is_active.is_some() {
        sqlx::query(
            r#"
            UPDATE roles
            SET name = COALESCE($1, name),
                is_active = COALESCE($2, is_active)
            WHERE id = $3 AND audience = 'EXTERNAL'
            "#,
        )
        .bind(payload.name)
        .bind(payload.is_active)
        .bind(role_id)
        .execute(&state.pool)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
    }
    if let Some(runtime) = payload.runtime {
        sqlx::query(
            r#"
            UPDATE role_runtime_configs
            SET is_active = false
            WHERE role_id = $1 AND is_active = true
            "#,
        )
        .bind(role_id)
        .execute(&state.pool)
        .await
        .ok();
        sqlx::query(
            r#"
            INSERT INTO role_runtime_configs (role_id, config_json, version, is_active)
            VALUES (
                $1,
                $2,
                COALESCE((SELECT MAX(version) FROM role_runtime_configs WHERE role_id = $1), 0) + 1,
                true
            )
            "#,
        )
        .bind(role_id)
        .bind(runtime)
        .execute(&state.pool)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
    }
    // Return the updated role detail
    let row = sqlx::query_as::<_, ExternalRoleDetailRow>(
        r#"
        SELECT r.id, r.name, r.key as code, r.audience, r.is_active, r.created_at,
               rc.updated_at as updated_at, rc.config_json as config_json
        FROM roles r
        LEFT JOIN role_runtime_configs rc ON rc.role_id = r.id AND rc.is_active = true
        WHERE r.id = $1 AND r.audience = 'EXTERNAL'
        "#,
    )
    .bind(role_id)
    .fetch_optional(&state.pool)
    .await
    .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
    .ok_or((StatusCode::NOT_FOUND, "External role not found".to_string()))?;

    Ok(Json(ExternalRoleDetail {
        id: row.id,
        name: row.name,
        code: row.code,
        audience: row.audience,
        is_active: row.is_active,
        runtime: row.config_json.unwrap_or_else(|| serde_json::json!({})),
        created_at: row.created_at,
        updated_at: row.updated_at,
    }))
}

async fn delete_external_role(
    auth: AuthUser,
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
    if let Err(_e) = require_admin(&auth) {
        return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
    }
    let result = sqlx::query("DELETE FROM roles WHERE id = $1 AND audience = 'EXTERNAL'")
        .bind(id)
        .execute(&state.pool)
        .await
        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
    if result.rows_affected() == 0 {
        return Err((StatusCode::NOT_FOUND, "External role not found".to_string()));
    }
    Ok(StatusCode::NO_CONTENT)
}
