use std::sync::Arc;

use anyhow::{anyhow, bail, Context, Result};
use chrono::{Duration as ChronoDuration, Utc};
use clap::{Parser, Subcommand, ValueEnum};
use diesel::{dsl::exists, prelude::*, select};
use rand::{rngs::OsRng, RngCore};
use reqwest::{Client, Method, StatusCode};
use sha2::{Digest, Sha256};
use uuid::Uuid;

use papercrate::{
    config::AppConfig,
    db::{self, PgPool},
    documents::search::ensure_quickwit_index,
    jobs::{enqueue_job, JOB_ANALYZE_DOCUMENT},
    models::{
        DocumentAsset, DocumentAssetObject, MagicToken, MagicTokenKind, NewUser, NewUserMembership,
        Tenant, TenantStatus, User,
    },
    s3,
    schema::{
        document_asset_objects, document_assets, documents, magic_tokens, tenants,
        user_memberships, users,
    },
    storage::{ObjectStorage, S3Storage, TenantStorage},
    tenants::TenantService,
    utils::tracing::init_tracing,
};

#[derive(Parser)]
#[command(
    name = "papercrate-admin",
    version,
    about = "Papercrate administration utility"
)]
struct Cli {
    #[command(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    CreateUser {
        username: String,
    },
    ListUsers,
    DeleteUser {
        username: String,
    },
    CreateTenant {
        name: String,
        #[arg(long = "storage-root")]
        storage_root: Option<String>,
        #[arg(long = "quickwit-index")]
        quickwit_index: Option<String>,
    },
    DeleteTenant {
        tenant_id: Uuid,
    },
    AddUserToTenant {
        username: String,
        tenant_id: Uuid,
    },
    RemoveUserFromTenant {
        username: String,
        tenant_id: Uuid,
    },
    ReanalyzeDocuments {
        tenant_id: Uuid,
    },
    ListTenants,
    DeleteAssets {
        tenant_id: Uuid,
    },
    QuickwitCreate {
        tenant_id: Uuid,
    },
    QuickwitDelete {
        tenant_id: Uuid,
    },
    MagicToken {
        username: String,
        #[arg(long = "ttl-minutes", default_value_t = 10)]
        ttl_minutes: i64,
        #[arg(
            long = "max-uses",
            value_name = "MAX_USES",
            help = "Maximum number of uses before the token is rejected (default: unlimited)"
        )]
        max_uses: Option<i32>,
        #[arg(long = "kind", value_enum, default_value_t = MagicTokenKindArg::EmailLogin)]
        kind: MagicTokenKindArg,
    },
}

#[derive(Copy, Clone, Debug, ValueEnum)]
enum MagicTokenKindArg {
    #[value(name = "email_login")]
    EmailLogin,
    #[value(name = "demo_login")]
    DemoLogin,
}

impl From<MagicTokenKindArg> for MagicTokenKind {
    fn from(value: MagicTokenKindArg) -> Self {
        match value {
            MagicTokenKindArg::EmailLogin => MagicTokenKind::EmailLogin,
            MagicTokenKindArg::DemoLogin => MagicTokenKind::DemoLogin,
        }
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    init_tracing("info");
    let cli = Cli::parse();
    let config = AppConfig::load_and_log("admin")?;
    let pool = db::init_pool_with_size(&config.database_url, config.database_max_pool_size)?;

    match cli.command {
        Command::CreateUser { username } => create_user(&pool, &username)?,
        Command::ListUsers => list_users(&pool)?,
        Command::DeleteUser { username } => delete_user(&pool, &username)?,
        Command::CreateTenant {
            name,
            storage_root,
            quickwit_index,
        } => create_tenant(&pool, &name, storage_root, quickwit_index)?,
        Command::DeleteTenant { tenant_id } => delete_tenant(&pool, tenant_id)?,
        Command::AddUserToTenant {
            username,
            tenant_id,
        } => add_user_to_tenant(&pool, &username, tenant_id)?,
        Command::RemoveUserFromTenant {
            username,
            tenant_id,
        } => remove_user_from_tenant(&pool, &username, tenant_id)?,
        Command::ReanalyzeDocuments { tenant_id } => reanalyze_documents(&pool, tenant_id)?,
        Command::ListTenants => list_tenants(&pool)?,
        Command::DeleteAssets { tenant_id } => {
            delete_assets_for_tenant(&config, &pool, tenant_id).await?
        }
        Command::QuickwitCreate { tenant_id } => {
            quickwit_index(&config, &pool, tenant_id, Method::POST).await?
        }
        Command::QuickwitDelete { tenant_id } => {
            quickwit_index(&config, &pool, tenant_id, Method::DELETE).await?
        }
        Command::MagicToken {
            username,
            ttl_minutes,
            max_uses,
            kind,
        } => {
            create_magic_token(&pool, &username, ttl_minutes, max_uses, kind.into())?;
        }
    }

    Ok(())
}

fn create_user(pool: &PgPool, username: &str) -> Result<()> {
    if username.trim().is_empty() {
        bail!("username must not be empty");
    }

    let mut conn = pool.get().context("failed to get database connection")?;
    let exists: bool =
        select(exists(users::table.filter(users::username.eq(username)))).get_result(&mut conn)?;
    if exists {
        bail!("user '{}' already exists", username);
    }

    let new_user = NewUser {
        id: Uuid::new_v4(),
        username: username.to_string(),
    };

    diesel::insert_into(users::table)
        .values(&new_user)
        .execute(&mut conn)?;

    println!("created user '{}' (id: {})", username, new_user.id);
    Ok(())
}

fn list_users(pool: &PgPool) -> Result<()> {
    let mut conn = pool.get().context("failed to get database connection")?;

    let users_list: Vec<User> = users::table.order(users::username.asc()).load(&mut conn)?;
    if users_list.is_empty() {
        println!("No users found.");
        return Ok(());
    }

    for user in users_list {
        let memberships: Vec<String> = user_memberships::table
            .inner_join(tenants::table)
            .filter(user_memberships::user_id.eq(user.id))
            .select(tenants::name)
            .order(tenants::name.asc())
            .load(&mut conn)?;

        if memberships.is_empty() {
            println!("{} ({})", user.username, user.id);
        } else {
            println!(
                "{} ({}) -> {}",
                user.username,
                user.id,
                memberships.join(", ")
            );
        }
    }

    Ok(())
}

fn delete_user(pool: &PgPool, username: &str) -> Result<()> {
    let mut conn = pool.get().context("failed to get database connection")?;

    let user: User = users::table
        .filter(users::username.eq(username))
        .first(&mut conn)
        .optional()?
        .ok_or_else(|| anyhow!("user '{}' not found", username))?;

    diesel::delete(user_memberships::table.filter(user_memberships::user_id.eq(user.id)))
        .execute(&mut conn)?;
    diesel::delete(users::table.filter(users::id.eq(user.id))).execute(&mut conn)?;

    println!("deleted user '{}'", username);
    Ok(())
}

fn create_tenant(
    pool: &PgPool,
    name: &str,
    storage_root_arg: Option<String>,
    quickwit_index_arg: Option<String>,
) -> Result<()> {
    let service = TenantService::new(pool.clone());
    let tenant = service
        .create_tenant(
            name,
            storage_root_arg.as_deref(),
            quickwit_index_arg.as_deref(),
            TenantStatus::Creating,
            &[],
            None,
        )
        .map_err(|err| anyhow!(format!("{err:?}")))?;

    let storage_root = tenant.storage_root.as_deref().unwrap_or("<none>");
    let quickwit_index = tenant.quickwit_index.as_deref().unwrap_or("<none>");

    println!(
        "created tenant '{}' with id {}, storage_root '{}', quickwit_index '{}', status '{}'",
        tenant.name,
        tenant.id,
        storage_root,
        quickwit_index,
        tenant.status.as_str()
    );
    Ok(())
}

fn create_magic_token(
    pool: &PgPool,
    username: &str,
    ttl_minutes: i64,
    max_uses: Option<i32>,
    kind: MagicTokenKind,
) -> Result<()> {
    let mut conn = pool.get().context("failed to get database connection")?;

    let user: User = users::table
        .filter(users::username.eq(username))
        .first(&mut conn)
        .with_context(|| format!("user '{}' not found", username))?;

    let raw_token = generate_random_token();
    let token_hash = hash_token(&raw_token);
    let expires_at = Utc::now() + ChronoDuration::minutes(ttl_minutes);

    let new_token = MagicToken {
        id: Uuid::new_v4(),
        user_id: user.id,
        kind,
        token_hash,
        metadata: serde_json::json!({}),
        expires_at: expires_at.naive_utc(),
        max_uses,
        used_count: 0,
        created_at: Utc::now().naive_utc(),
        created_by: None,
        last_used_at: None,
    };

    diesel::insert_into(magic_tokens::table)
        .values(&new_token)
        .execute(&mut conn)?;

    println!(
        "Magic token created for '{}' (kind: {}, expires_at: {}, max_uses: {})",
        username,
        kind.as_str(),
        expires_at,
        max_uses.map_or("∞".to_string(), |v| v.to_string())
    );
    println!("Token: {}", raw_token);

    Ok(())
}

fn generate_random_token() -> String {
    let mut bytes = [0u8; 32];
    OsRng.fill_bytes(&mut bytes);
    hex::encode(bytes)
}

fn hash_token(token: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(token.as_bytes());
    hex::encode(hasher.finalize())
}

fn delete_tenant(pool: &PgPool, tenant_id: Uuid) -> Result<()> {
    let mut conn = pool.get().context("failed to get database connection")?;

    let tenant: Tenant = tenants::table
        .find(tenant_id)
        .first(&mut conn)
        .optional()?
        .ok_or_else(|| anyhow!("tenant '{}' not found", tenant_id))?;

    let member_exists: bool = select(exists(
        user_memberships::table.filter(user_memberships::tenant_id.eq(tenant.id)),
    ))
    .get_result(&mut conn)?;
    if member_exists {
        bail!("tenant '{}' still has user memberships", tenant.name);
    }

    diesel::delete(tenants::table.filter(tenants::id.eq(tenant.id))).execute(&mut conn)?;
    println!("deleted tenant '{}'", tenant.name);
    Ok(())
}

fn add_user_to_tenant(pool: &PgPool, username: &str, tenant_id: Uuid) -> Result<()> {
    let mut conn = pool.get().context("failed to get database connection")?;

    let user: User = users::table
        .filter(users::username.eq(username))
        .first(&mut conn)
        .optional()?
        .ok_or_else(|| anyhow!("user '{}' not found", username))?;

    let tenant: Tenant = tenants::table
        .find(tenant_id)
        .first(&mut conn)
        .optional()?
        .ok_or_else(|| anyhow!("tenant '{}' not found", tenant_id))?;

    let membership = NewUserMembership {
        id: Uuid::new_v4(),
        user_id: user.id,
        tenant_id: tenant.id,
    };

    diesel::insert_into(user_memberships::table)
        .values(&membership)
        .on_conflict((user_memberships::user_id, user_memberships::tenant_id))
        .do_nothing()
        .execute(&mut conn)?;

    println!("added user '{}' to tenant '{}'", username, tenant.name);
    Ok(())
}

fn remove_user_from_tenant(pool: &PgPool, username: &str, tenant_id: Uuid) -> Result<()> {
    let mut conn = pool.get().context("failed to get database connection")?;

    let user: User = users::table
        .filter(users::username.eq(username))
        .first(&mut conn)
        .optional()?
        .ok_or_else(|| anyhow!("user '{}' not found", username))?;

    let tenant: Tenant = tenants::table
        .find(tenant_id)
        .first(&mut conn)
        .optional()?
        .ok_or_else(|| anyhow!("tenant '{}' not found", tenant_id))?;

    let removed = diesel::delete(
        user_memberships::table
            .filter(user_memberships::user_id.eq(user.id))
            .filter(user_memberships::tenant_id.eq(tenant.id)),
    )
    .execute(&mut conn)?;

    if removed == 0 {
        println!(
            "user '{}' was not a member of tenant '{}'",
            username, tenant.name
        );
    } else {
        println!("removed user '{}' from tenant '{}'", username, tenant.name);
    }
    Ok(())
}

fn reanalyze_documents(pool: &PgPool, tenant_id: Uuid) -> Result<()> {
    let mut conn = pool.get().context("failed to get database connection")?;

    let tenant: Tenant = tenants::table
        .find(tenant_id)
        .first(&mut conn)
        .optional()?
        .ok_or_else(|| anyhow!("tenant '{}' not found", tenant_id))?;

    let targets: Vec<(Uuid, Uuid)> = documents::table
        .filter(documents::tenant_id.eq(tenant.id))
        .filter(documents::deleted_at.is_null())
        .select((documents::id, documents::current_version_id))
        .load(&mut conn)?;

    if targets.is_empty() {
        println!("tenant '{}' has no active documents", tenant.name);
        return Ok(());
    }

    let mut queued = 0usize;
    for (document_id, version_id) in targets {
        enqueue_job(
            &mut conn,
            tenant.id,
            JOB_ANALYZE_DOCUMENT,
            serde_json::json!({
                "document_id": document_id,
                "document_version_id": version_id,
                "force": true,
            }),
            None,
        )
        .map_err(|err| anyhow!("failed to enqueue analyze job: {}", err))?;
        queued += 1;
    }

    println!(
        "queued {} documents for re-analysis in tenant '{}'",
        queued, tenant.name
    );
    Ok(())
}

fn list_tenants(pool: &PgPool) -> Result<()> {
    let mut conn = pool.get().context("failed to get database connection")?;
    let tenants: Vec<Tenant> = tenants::table
        .order(tenants::name.asc())
        .load(&mut conn)
        .context("failed to load tenants")?;

    if tenants.is_empty() {
        println!("No tenants found.");
        return Ok(());
    }

    for tenant in tenants {
        println!("{} {}", tenant.id, tenant.name);
    }

    Ok(())
}

async fn delete_assets_for_tenant(
    config: &AppConfig,
    pool: &PgPool,
    tenant_id: Uuid,
) -> Result<()> {
    let s3_client = s3::build_client(config).await?;
    let storage: Arc<dyn ObjectStorage> =
        Arc::new(S3Storage::new(s3_client, config.s3_bucket.clone()));

    let mut conn = pool.get().context("failed to get database connection")?;
    let tenant: Tenant = tenants::table
        .find(tenant_id)
        .first(&mut conn)
        .optional()?
        .ok_or_else(|| anyhow!("tenant '{}' not found", tenant_id))?;

    let tenant_storage = TenantStorage::new(Arc::clone(&storage), &tenant)
        .with_context(|| format!("missing storage root for tenant {}", tenant.name))?;

    let assets: Vec<DocumentAsset> = document_assets::table
        .filter(document_assets::tenant_id.eq(tenant.id))
        .load(&mut conn)
        .with_context(|| format!("failed to load assets for tenant {}", tenant.name))?;

    if assets.is_empty() {
        println!("Tenant {}: no assets", tenant.name);
        return Ok(());
    }

    println!(
        "Tenant {} ({}): deleting {} assets…",
        tenant.name,
        tenant.id,
        assets.len()
    );

    let asset_ids: Vec<Uuid> = assets.iter().map(|asset| asset.id).collect();

    let objects: Vec<DocumentAssetObject> = document_asset_objects::table
        .filter(document_asset_objects::tenant_id.eq(tenant.id))
        .filter(document_asset_objects::asset_id.eq_any(&asset_ids))
        .load(&mut conn)
        .with_context(|| format!("failed to load asset objects for tenant {}", tenant.name))?;

    for object in &objects {
        if let Err(err) = tenant_storage.delete_object(&object.s3_key).await {
            eprintln!(
                "Failed to delete object {} (tenant {}): {err}",
                object.s3_key, tenant.name
            );
        }
    }

    diesel::delete(
        document_asset_objects::table
            .filter(document_asset_objects::tenant_id.eq(tenant.id))
            .filter(document_asset_objects::asset_id.eq_any(&asset_ids)),
    )
    .execute(&mut conn)
    .with_context(|| format!("failed to remove asset objects for tenant {}", tenant.name))?;

    diesel::delete(document_assets::table.filter(document_assets::tenant_id.eq(tenant.id)))
        .execute(&mut conn)
        .with_context(|| format!("failed to remove asset records for tenant {}", tenant.name))?;

    println!("Tenant {}: asset records deleted.", tenant.name);
    Ok(())
}

async fn quickwit_index(
    config: &AppConfig,
    pool: &PgPool,
    tenant_id: Uuid,
    method: Method,
) -> Result<()> {
    let endpoint = config
        .quickwit_endpoint
        .as_ref()
        .ok_or_else(|| anyhow!("quickwit endpoint not configured"))?;

    let mut conn = pool.get().context("failed to get database connection")?;
    let tenant: Tenant = tenants::table
        .find(tenant_id)
        .first(&mut conn)
        .optional()?
        .ok_or_else(|| anyhow!("tenant '{}' not found", tenant_id))?;

    let client = Client::new();
    let index_id = format!("documents-{}", tenant.id);
    let base_endpoint = endpoint.trim_end_matches('/');

    match method {
        Method::POST => {
            ensure_quickwit_index(&client, base_endpoint, &index_id)
                .await
                .context("failed to ensure quickwit index")?;

            diesel::update(tenants::table.filter(tenants::id.eq(tenant.id)))
                .set(tenants::quickwit_index.eq(Some(index_id.clone())))
                .execute(&mut conn)
                .context("failed to update tenant quickwit_index")?;

            println!(
                "Tenant '{}' quickwit index set to '{}'.",
                tenant.name, index_id
            );
        }
        Method::DELETE => {
            let response = client
                .delete(format!("{}/api/v1/indexes/{}", base_endpoint, index_id))
                .send()
                .await
                .context("failed to send delete index request")?;

            match response.status() {
                status if status.is_success() || status == StatusCode::NOT_FOUND => {
                    diesel::update(tenants::table.filter(tenants::id.eq(tenant.id)))
                        .set(tenants::quickwit_index.eq::<Option<String>>(None))
                        .execute(&mut conn)
                        .context("failed to clear tenant quickwit_index")?;

                    println!("Tenant '{}' quickwit index cleared.", tenant.name);
                }
                status => {
                    let body = response.text().await.unwrap_or_default();
                    bail!(
                        "quickwit delete index failed with status {}: {}",
                        status,
                        body
                    );
                }
            }
        }
        _ => unreachable!(),
    }

    Ok(())
}
