use std::sync::Arc;

use async_trait::async_trait;
use chrono::Utc;
use diesel::prelude::*;
use reqwest::Client;
use serde::Deserialize;
use tracing::warn;
use uuid::Uuid;

use crate::documents::search::ensure_quickwit_index;
use crate::jobs::JOB_PROVISION_TENANT;
use crate::models::{NewUserMembership, TenantStatus};
use crate::schema::{tenants, user_memberships};
use crate::state::AppState;
use crate::tenants::TenantRepository;
use crate::workers::{JobExecution, JobHandler};

pub struct ProvisionTenantJob;

impl ProvisionTenantJob {
    pub fn new() -> Self {
        Self
    }
}

#[async_trait]
impl JobHandler for ProvisionTenantJob {
    fn job_type(&self) -> &'static str {
        JOB_PROVISION_TENANT
    }

    async fn handle(
        &self,
        state: Arc<AppState>,
        job: crate::models::Job,
        _storage: crate::storage::TenantStorage,
    ) -> JobExecution {
        let mut conn = match state.db_unscoped() {
            Ok(conn) => conn,
            Err(err) => {
                warn!(job_id = %job.id, error = ?err, "failed to get connection for tenant provisioning");
                return JobExecution::Retry {
                    delay: std::time::Duration::from_secs(30),
                    error: "database connection unavailable".into(),
                };
            }
        };

        let tenant = match TenantRepository::get_by_id(&mut conn, job.tenant_id) {
            Ok(tenant) => tenant,
            Err(err) => {
                warn!(job_id = %job.id, error = ?err, "tenant not found for provisioning");
                return JobExecution::Failed {
                    error: "tenant not found".into(),
                };
            }
        };

        drop(conn);

        let mut conn = match state.db_for_tenant(tenant.id) {
            Ok(conn) => conn,
            Err(err) => {
                warn!(job_id = %job.id, error = ?err, "failed to scope connection for tenant provisioning");
                return JobExecution::Retry {
                    delay: std::time::Duration::from_secs(30),
                    error: "tenant connection unavailable".into(),
                };
            }
        };

        if tenant.status == TenantStatus::Active {
            warn!(
                job_id = %job.id,
                tenant_id = %tenant.id,
                "tenant already active; skipping provisioning"
            );
            return JobExecution::Success;
        }

        if tenant.status != TenantStatus::Creating {
            warn!(
                job_id = %job.id,
                tenant_id = %tenant.id,
                status = %tenant.status.as_str(),
                "tenant not in creating state; skipping provisioning"
            );
            return JobExecution::Failed {
                error: format!(
                    "tenant status '{}' not eligible for provisioning",
                    tenant.status.as_str()
                ),
            };
        }
        let endpoint = match &state.config.quickwit_endpoint {
            Some(endpoint) => endpoint.trim_end_matches('/').to_owned(),
            None => {
                warn!(
                    job_id = %job.id,
                    tenant_id = %tenant.id,
                    "quickwit endpoint not configured; retrying"
                );
                return JobExecution::Retry {
                    delay: std::time::Duration::from_secs(30),
                    error: "quickwit endpoint not configured".into(),
                };
            }
        };

        let index_id = tenant
            .quickwit_index
            .as_deref()
            .map(str::to_owned)
            .unwrap_or_else(|| format!("documents-{}", tenant.id));

        let client = Client::new();
        if let Err(err) = ensure_quickwit_index(&client, &endpoint, &index_id).await {
            warn!(
                job_id = %job.id,
                tenant_id = %tenant.id,
                error = %err,
                "failed to ensure quickwit index"
            );
            return JobExecution::Retry {
                delay: std::time::Duration::from_secs(30),
                error: err.to_string(),
            };
        }

        if let Some(members) = ProvisionPayload::from_job(&job) {
            for member in members {
                let new_membership = NewUserMembership {
                    id: Uuid::new_v4(),
                    user_id: member,
                    tenant_id: tenant.id,
                };

                if let Err(err) = diesel::insert_into(user_memberships::table)
                    .values(&new_membership)
                    .on_conflict((user_memberships::user_id, user_memberships::tenant_id))
                    .do_nothing()
                    .execute(&mut conn)
                {
                    warn!(
                        job_id = %job.id,
                        tenant_id = %tenant.id,
                        user_id = %member,
                        error = %err,
                        "failed to assign initial membership"
                    );
                }
            }
        }

        if let Err(err) = diesel::update(tenants::table.find(tenant.id))
            .set((
                tenants::status.eq(TenantStatus::Active),
                tenants::quickwit_index.eq(Some(index_id)),
                tenants::updated_at.eq(Utc::now().naive_utc()),
            ))
            .execute(&mut conn)
        {
            warn!(job_id = %job.id, error = %err, "failed to activate tenant");
            return JobExecution::Retry {
                delay: std::time::Duration::from_secs(30),
                error: format!("failed to update tenant status: {err}"),
            };
        }

        JobExecution::Success
    }
}

#[derive(Deserialize, Default)]
struct ProvisionPayload {
    #[serde(default)]
    members: Vec<Uuid>,
}

impl ProvisionPayload {
    fn from_job(job: &crate::models::Job) -> Option<Vec<Uuid>> {
        serde_json::from_value(job.payload.clone())
            .map(|payload: ProvisionPayload| payload.members)
            .ok()
    }
}
