use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sqlx::{FromRow, PgPool};
use uuid::Uuid;

// ── Structs ───────────────────────────────────────────────────────────────────

#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct OnboardingState {
    pub id: Uuid,
    pub user_id: Uuid,
    pub role_id: Uuid,
    pub status: String, // NOT_STARTED | IN_PROGRESS | COMPLETED
    pub progress_json: Value,
    pub completed_at: Option<DateTime<Utc>>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Deserialize)]
pub struct SaveProgressPayload {
    pub role_id: Uuid,
    pub progress_json: Value,
}

#[derive(Debug, Deserialize)]
pub struct SubmitOnboardingPayload {
    pub role_id: Uuid,
    pub progress_json: Value,
}

// ── Repository ────────────────────────────────────────────────────────────────

pub struct OnboardingStateRepository;

impl OnboardingStateRepository {
    /// Fetch onboarding state for a user + role. Returns None if no record exists yet.
    pub async fn get(
        pool: &PgPool,
        user_id: Uuid,
        role_id: Uuid,
    ) -> Result<Option<OnboardingState>, sqlx::Error> {
        sqlx::query_as::<_, OnboardingState>(
            r#"
            SELECT id, user_id, role_id, status, progress_json,
                   completed_at, created_at, updated_at
            FROM onboarding_states
            WHERE user_id = $1 AND role_id = $2
            "#,
        )
        .bind(user_id)
        .bind(role_id)
        .fetch_optional(pool)
        .await
    }

    /// Upsert progress — inserts a new record or updates progress on conflict.
    pub async fn save_progress(
        pool: &PgPool,
        user_id: Uuid,
        role_id: Uuid,
        progress: &Value,
    ) -> Result<OnboardingState, sqlx::Error> {
        sqlx::query_as::<_, OnboardingState>(
            r#"
            INSERT INTO onboarding_states (user_id, role_id, status, progress_json)
            VALUES ($1, $2, 'IN_PROGRESS', $3)
            ON CONFLICT (user_id, role_id) DO UPDATE
                SET status = CASE
                        WHEN onboarding_states.status = 'COMPLETED' THEN 'COMPLETED'
                        ELSE 'IN_PROGRESS'
                    END,
                    progress_json = EXCLUDED.progress_json,
                    updated_at = NOW()
            RETURNING id, user_id, role_id, status, progress_json,
                      completed_at, created_at, updated_at
            "#,
        )
        .bind(user_id)
        .bind(role_id)
        .bind(progress)
        .fetch_one(pool)
        .await
    }

    /// Mark onboarding as COMPLETED and freeze the final answers.
    pub async fn complete(
        pool: &PgPool,
        user_id: Uuid,
        role_id: Uuid,
        final_answers: &Value,
    ) -> Result<OnboardingState, sqlx::Error> {
        sqlx::query_as::<_, OnboardingState>(
            r#"
            INSERT INTO onboarding_states (user_id, role_id, status, progress_json, completed_at)
            VALUES ($1, $2, 'COMPLETED', $3, NOW())
            ON CONFLICT (user_id, role_id) DO UPDATE
                SET status = 'COMPLETED',
                    progress_json = EXCLUDED.progress_json,
                    completed_at = NOW(),
                    updated_at = NOW()
            RETURNING id, user_id, role_id, status, progress_json,
                      completed_at, created_at, updated_at
            "#,
        )
        .bind(user_id)
        .bind(role_id)
        .bind(final_answers)
        .fetch_one(pool)
        .await
    }

    /// Check whether onboarding is complete for a user+role. Returns false if no record.
    pub async fn is_complete(
        pool: &PgPool,
        user_id: Uuid,
        role_id: Uuid,
    ) -> Result<bool, sqlx::Error> {
        let status = sqlx::query_scalar::<_, String>(
            r#"
            SELECT status FROM onboarding_states
            WHERE user_id = $1 AND role_id = $2
            "#,
        )
        .bind(user_id)
        .bind(role_id)
        .fetch_optional(pool)
        .await?;
        Ok(status.map(|s| s == "COMPLETED").unwrap_or(false))
    }
}
