use crate::AppState;
use axum::{
    extract::{Multipart, Path, Query, State},
    http::StatusCode,
    response::IntoResponse,
    routing::{delete, get, post},
    Json, Router,
};
use bytes::BufMut;
use redis::AsyncCommands;
use serde::Deserialize;
use uuid::Uuid;
use db::models::job_seeker::{JobSeekerRepository, UpsertJobSeekerProfilePayload, CreateJobSeekerDocumentPayload};
use db::models::job::JobRepository;
use db::models::application::{ApplicationRepository, CreateApplicationPayload};
use contracts::auth_middleware::AuthUser;

pub fn router() -> Router<AppState> {
    Router::new()
        .route("/profile/me", get(get_profile).patch(update_profile))
        .route("/profile/resume", post(generate_resume))
        .route("/profile/documents", post(upload_document))
        .route("/profile/documents", get(list_documents))
        .route("/profile/documents/{id}", delete(delete_document))
        .route("/profile/submit", post(submit_for_verification))
        .route("/jobs", get(browse_jobs))
        .route("/jobs/{id}", get(get_job))
        .route("/jobs/{id}/apply", post(apply_to_job))
        .route("/applications", get(list_my_applications))
        .route("/applications/{id}", get(get_my_application))
        .route("/applications/{id}/withdraw", post(withdraw_application))
}

#[derive(Deserialize)]
pub struct JobBrowseQuery {
    pub page: Option<i64>,
    pub limit: Option<i64>,
    pub location: Option<String>,
    pub job_type: Option<String>,
    pub search: Option<String>,
    pub skills: Option<String>,
    pub sort_by: Option<String>,
    pub order: Option<String>,
}

#[derive(Deserialize)]
#[allow(dead_code)]
pub struct ApplyRequest {
    pub cover_note: Option<String>,
    pub resume_url: Option<String>,
}

#[derive(Deserialize)]
pub struct PaginationQuery {
    pub page: Option<i64>,
    pub limit: Option<i64>,
}

// ── Handlers ──────────────────────────────────────────────────────────────────

async fn get_or_create_job_seeker_profile(
    pool: &sqlx::PgPool,
    user_id: Uuid,
) -> Result<db::models::job_seeker::JobSeekerProfile, sqlx::Error> {
    if let Some(profile) = JobSeekerRepository::get_by_user_id(pool, user_id).await? {
        return Ok(profile);
    }

    JobSeekerRepository::upsert(
        pool,
        user_id,
        UpsertJobSeekerProfilePayload {
            full_name: None,
            location: None,
            summary: None,
            experience_years: None,
            skills: None,
            resume_url: None,
            bio: None,
            custom_data: None,
        },
    )
    .await
}

async fn get_profile(
    State(state): State<AppState>,
    auth: AuthUser,
) -> impl IntoResponse {
    let cache_key = format!("profile:job_seeker:{}", auth.user_id);
    let mut redis = state.redis.clone();

    // Try cache first
    if let Ok(cached) = redis.get::<_, String>(&cache_key).await {
        tracing::debug!("Cache hit for job seeker profile: {}", auth.user_id);
        if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&cached) {
            return (StatusCode::OK, Json(parsed)).into_response();
        }
    }

    match get_or_create_job_seeker_profile(&state.pool, auth.user_id).await {
        Ok(profile) => {
            // Cache for 5 minutes
            let _: Result<(), _> = redis.set_ex(&cache_key, &serde_json::to_string(&profile).unwrap_or_default(), 300).await;
            (StatusCode::OK, Json(profile)).into_response()
        }
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    }
}

async fn update_profile(
    State(state): State<AppState>,
    auth: AuthUser,
    Json(payload): Json<UpsertJobSeekerProfilePayload>,
) -> impl IntoResponse {
    match JobSeekerRepository::upsert(&state.pool, auth.user_id, payload).await {
        Ok(profile) => {
            // Invalidate profile cache
            let cache_key = format!("profile:job_seeker:{}", auth.user_id);
            let mut redis = state.redis.clone();
            let _ = redis.del::<_, ()>(&cache_key).await;

            // Auto-regenerate resume in background whenever portfolio data changes
            let pool = state.pool.clone();
            let storage = state.storage.clone();
            tokio::spawn(async move {
                if let Err(e) = regenerate_resume_for_user(&pool, &storage, auth.user_id).await {
                    tracing::warn!("Background resume generation failed for {}: {}", auth.user_id, e);
                }
            });

            (StatusCode::OK, Json(profile)).into_response()
        }
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    }
}

/// Shared resume generation logic — called from background task after profile save.
async fn regenerate_resume_for_user(
    pool: &sqlx::PgPool,
    storage: &storage::StorageClient,
    user_id: uuid::Uuid,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
    let seeker = JobSeekerRepository::get_by_user_id(pool, user_id)
        .await?
        .ok_or("Job seeker profile not found")?;

    let name_row = sqlx::query_as::<_, (Option<String>, Option<String>)>(
        "SELECT first_name, last_name FROM users WHERE id = $1",
    )
    .bind(user_id)
    .fetch_optional(pool)
    .await
    .ok()
    .flatten();

    let (first, last) = name_row.unwrap_or((None, None));
    let full_name = match (first.as_deref(), last.as_deref()) {
        (Some(f), Some(l)) if !f.is_empty() || !l.is_empty() => format!("{} {}", f, l).trim().to_string(),
        (Some(f), _) if !f.is_empty() => f.to_string(),
        (_, Some(l)) if !l.is_empty() => l.to_string(),
        _ => seeker.full_name.clone().filter(|n| !n.is_empty()).unwrap_or_else(|| "Job Seeker".to_string()),
    };

    let portfolio = seeker.custom_data
        .as_ref()
        .and_then(|d| d.get("job_seeker_portfolio"))
        .cloned()
        .unwrap_or_default();

    let headline    = portfolio.get("headline").and_then(|v| v.as_str()).unwrap_or("").to_string();
    let summary     = portfolio.get("summary").and_then(|v| v.as_str()).unwrap_or("").to_string();
    let education   = portfolio.get("education").and_then(|v| v.as_str()).unwrap_or("").to_string();
    let work_exp    = portfolio.get("workExperience").and_then(|v| v.as_str()).unwrap_or("").to_string();
    let skills      = portfolio.get("skills").and_then(|v| v.as_str()).unwrap_or("").to_string();

    let pdf_bytes = tokio::task::spawn_blocking(move || {
        build_resume_pdf(&full_name, &headline, &summary, &education, &work_exp, &skills)
    })
    .await??;

    let resume_url = storage
        .upload("resumes", "pdf", bytes::Bytes::from(pdf_bytes), "application/pdf")
        .await?;

    sqlx::query("UPDATE job_seeker_profiles SET resume_url = $1 WHERE id = $2")
        .bind(&resume_url)
        .bind(seeker.id)
        .execute(pool)
        .await?;

    Ok(resume_url)
}

/// POST /api/jobseeker/profile/resume — kept for backward compatibility / admin use.
/// Normal path: resume is auto-regenerated after every profile save.
async fn generate_resume(
    State(state): State<AppState>,
    auth: AuthUser,
) -> impl IntoResponse {
    match regenerate_resume_for_user(&state.pool, &state.storage, auth.user_id).await {
        Ok(url) => (StatusCode::OK, Json(serde_json::json!({ "resume_url": url }))).into_response(),
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(),
    }
}

// ── PDF builder ───────────────────────────────────────────────────────────────

fn wrap_text(text: &str, max_chars: usize) -> Vec<String> {
    let mut lines = Vec::new();
    for para in text.split('\n') {
        let para = para.trim();
        if para.is_empty() { lines.push(String::new()); continue; }
        let words: Vec<&str> = para.split_whitespace().collect();
        let mut current = String::new();
        for word in words {
            if current.is_empty() {
                current.push_str(word);
            } else if current.len() + 1 + word.len() <= max_chars {
                current.push(' ');
                current.push_str(word);
            } else {
                lines.push(current);
                current = word.to_string();
            }
        }
        if !current.is_empty() { lines.push(current); }
    }
    lines
}

fn build_resume_pdf(
    name: &str,
    headline: &str,
    summary: &str,
    education: &str,
    work_experience: &str,
    skills: &str,
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
    use printpdf::*;

    // Push a single text span at a specific (x, y) position in mm.
    fn push_text(ops: &mut Vec<Op>, text: &str, size: f32, x: f32, y: f32, font: BuiltinFont) {
        ops.push(Op::StartTextSection);
        ops.push(Op::SetTextCursor { pos: Point::new(Mm(x), Mm(y)) });
        ops.push(Op::SetFont { font: PdfFontHandle::Builtin(font), size: Pt(size) });
        ops.push(Op::ShowText { items: vec![TextItem::Text(text.to_string())] });
        ops.push(Op::EndTextSection);
    }

    let mut ops: Vec<Op> = Vec::new();
    let left = 20.0_f32;
    let mut y = 276.0_f32;

    // Name (large, bold)
    push_text(&mut ops, name, 20.0, left, y, BuiltinFont::HelveticaBold);
    y -= 9.0;

    // Headline
    if !headline.is_empty() {
        push_text(&mut ops, headline, 11.0, left, y, BuiltinFont::Helvetica);
        y -= 5.0;
    }

    // Separator
    push_text(&mut ops, "___________________________________________________", 9.0, left, y, BuiltinFont::Helvetica);
    y -= 8.0;

    // Sections: bold header then wrapped body lines
    for (label, content) in [
        ("PROFESSIONAL SUMMARY", summary),
        ("SKILLS", skills),
        ("EDUCATION", education),
        ("WORK EXPERIENCE", work_experience),
    ] {
        if content.trim().is_empty() || y < 20.0 { continue; }
        push_text(&mut ops, label, 9.5, left, y, BuiltinFont::HelveticaBold);
        y -= 5.5;
        for line in wrap_text(content, 88) {
            if y < 20.0 { break; }
            if line.is_empty() {
                y -= 2.5;
            } else {
                push_text(&mut ops, &line, 9.5, left, y, BuiltinFont::Helvetica);
                y -= 5.0;
            }
        }
        y -= 4.0;
    }

    // Privacy footer
    push_text(
        &mut ops,
        "* Contact details are hidden. Use Nxtgauge to unlock and connect with this candidate.",
        7.5, left, 12.0, BuiltinFont::Helvetica,
    );

    let page = PdfPage::new(Mm(210.0), Mm(297.0), ops);
    let bytes = PdfDocument::new(&format!("{} - Resume", name))
        .with_pages(vec![page])
        .save(&PdfSaveOptions::default(), &mut Vec::new());

    Ok(bytes)
}

async fn browse_jobs(
    State(state): State<AppState>,
    Query(q): Query<JobBrowseQuery>,
) -> impl IntoResponse {
    let page = q.page.unwrap_or(1).max(1);
    let limit = q.limit.unwrap_or(20).min(100).max(1);
    let offset = (page - 1) * limit;

    // Parse sort_by and order, with defaults
    let sort_by = q.sort_by.as_deref().unwrap_or("created_at");
    let order = q.order.as_deref().unwrap_or("desc");
    let order_dir = if order.eq_ignore_ascii_case("asc") { "ASC" } else { "DESC" };

    // Build cache key based on all query params
    let cache_key = format!(
        "jobs:list:{}:{}:{}:{}:{}:{}:{}:{}",
        page,
        limit,
        sort_by,
        order_dir,
        q.search.as_deref().unwrap_or(""),
        q.location.as_deref().unwrap_or(""),
        q.job_type.as_deref().unwrap_or(""),
        q.skills.as_deref().unwrap_or(""),
    );

    // Try cache first
    let mut redis = state.redis.clone();
    if let Ok(cached) = redis.get::<_, String>(&cache_key).await {
        tracing::debug!("Cache hit for jobs list: {}", cache_key);
        if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&cached) {
            return (StatusCode::OK, Json(parsed)).into_response();
        }
    }

    // Validate sort_by column to prevent SQL injection
    let sort_column = match sort_by {
        "created_at" => "j.created_at",
        "salary" => "j.salary_max",
        "title" => "j.title",
        _ => "j.created_at",
    };

    #[derive(serde::Serialize, sqlx::FromRow)]
    struct JobWithCompany {
        id: uuid::Uuid,
        company_id: uuid::Uuid,
        title: String,
        category: Option<String>,
        description: String,
        location: String,
        job_type: String,
        salary_min: Option<i32>,
        salary_max: Option<i32>,
        experience_years: Option<i32>,
        skills: Option<Vec<String>>,
        status: String,
        rejection_reason: Option<String>,
        expires_at: Option<chrono::DateTime<chrono::Utc>>,
        approved_at: Option<chrono::DateTime<chrono::Utc>>,
        approved_by: Option<uuid::Uuid>,
        created_at: chrono::DateTime<chrono::Utc>,
        updated_at: chrono::DateTime<chrono::Utc>,
        company_name: String,
    }

    #[derive(serde::Serialize, sqlx::FromRow)]
    struct TotalCount {
        count: i64,
    }

    // Build the dynamic WHERE clause
    let search_pattern = q.search.as_ref().map(|s| format!("%{}%", s));

    // Skills filter: comma-separated -> convert to array overlap check
    // Assuming jobs.skills is text[] in PostgreSQL
    let skills_param: Option<Vec<String>> = q.skills.as_ref().map(|s| {
        s.split(',').map(|sk| sk.trim().to_lowercase()).collect()
    });

    // Get total count first
    let count_query = format!(
        r#"
        SELECT COUNT(*) as count
        FROM jobs j
        LEFT JOIN company_profiles c ON c.id = j.company_id
        WHERE j.status = 'LIVE'
        AND ($1::VARCHAR IS NULL OR j.location ILIKE '%' || $1 || '%')
        AND ($2::VARCHAR IS NULL OR j.job_type = $2)
        AND ($3::VARCHAR IS NULL OR j.title ILIKE '%' || $3 || '%' OR j.location ILIKE '%' || $3 || '%' OR c.company_name ILIKE '%' || $3 || '%')
        AND ($5::text[] IS NULL OR j.skills && $5::text[])
        "#,
    );

    let total_result = sqlx::query_as::<_, TotalCount>(&count_query)
        .bind(&q.location)
        .bind(&q.job_type)
        .bind(&search_pattern)
        .bind(&q.skills)  // placeholder for skills array (unused when None)
        .bind(&skills_param)
        .fetch_one(&state.pool)
        .await;

    let total = match total_result {
        Ok(t) => t.count,
        Err(e) => {
            tracing::error!("Count query failed: {}", e);
            return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response();
        }
    };

    let total_pages = (total as f64 / limit as f64).ceil() as i64;

    // Main query with pagination
    let jobs_query = format!(
        r#"
        SELECT j.id, j.company_id, j.title, j.category, j.description, j.location,
               j.job_type, j.salary_min, j.salary_max, j.experience_years, j.skills,
               j.status, j.rejection_reason, j.expires_at, j.approved_at, j.approved_by,
               j.created_at, j.updated_at,
               COALESCE(c.company_name, 'Company') AS company_name
        FROM jobs j
        LEFT JOIN company_profiles c ON c.id = j.company_id
        WHERE j.status = 'LIVE'
        AND ($1::VARCHAR IS NULL OR j.location ILIKE '%' || $1 || '%')
        AND ($2::VARCHAR IS NULL OR j.job_type = $2)
        AND ($3::VARCHAR IS NULL OR j.title ILIKE '%' || $3 || '%' OR j.location ILIKE '%' || $3 || '%' OR c.company_name ILIKE '%' || $3 || '%')
        AND ($5::text[] IS NULL OR j.skills && $5::text[])
        ORDER BY {} {}
        LIMIT $6 OFFSET $7
        "#,
        sort_column, order_dir
    );

    let jobs = sqlx::query_as::<_, JobWithCompany>(&jobs_query)
        .bind(&q.location)
        .bind(&q.job_type)
        .bind(&search_pattern)
        .bind(&q.skills)  // placeholder
        .bind(&skills_param)
        .bind(limit)
        .bind(offset)
        .fetch_all(&state.pool)
        .await;

    match jobs {
        Ok(j) => {
            let response = serde_json::json!({
                "data": j,
                "pagination": {
                    "page": page,
                    "limit": limit,
                    "total": total,
                    "total_pages": total_pages
                }
            });
            // Cache result for 5 minutes
            let _: Result<(), _> = redis.set_ex(&cache_key, &serde_json::to_string(&response).unwrap_or_default(), 300).await;
            (StatusCode::OK, Json(response)).into_response()
        }
        Err(e) => {
            tracing::error!("Browse jobs query failed: {}", e);
            (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
        }
    }
}

async fn get_job(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> impl IntoResponse {
    #[derive(serde::Serialize, sqlx::FromRow)]
    struct JobWithCompany {
        id: uuid::Uuid,
        company_id: uuid::Uuid,
        title: String,
        category: Option<String>,
        description: String,
        location: String,
        job_type: String,
        salary_min: Option<i32>,
        salary_max: Option<i32>,
        experience_years: Option<i32>,
        skills: Option<Vec<String>>,
        status: String,
        rejection_reason: Option<String>,
        expires_at: Option<chrono::DateTime<chrono::Utc>>,
        approved_at: Option<chrono::DateTime<chrono::Utc>>,
        approved_by: Option<uuid::Uuid>,
        created_at: chrono::DateTime<chrono::Utc>,
        updated_at: chrono::DateTime<chrono::Utc>,
        company_name: String,
    }

    let job = sqlx::query_as::<_, JobWithCompany>(
        r#"
        SELECT j.id, j.company_id, j.title, j.category, j.description, j.location,
               j.job_type, j.salary_min, j.salary_max, j.experience_years, j.skills,
               j.status, j.rejection_reason, j.expires_at, j.approved_at, j.approved_by,
               j.created_at, j.updated_at,
               COALESCE(c.company_name, 'Company') AS company_name
        FROM jobs j
        LEFT JOIN company_profiles c ON c.id = j.company_id
        WHERE j.id = $1
        "#,
    )
    .bind(id)
    .fetch_optional(&state.pool)
    .await;

    match job {
        Ok(Some(j)) if j.status == "LIVE" => (StatusCode::OK, Json(j)).into_response(),
        Ok(Some(_)) => (StatusCode::FORBIDDEN, "Job is not live").into_response(),
        Ok(None) => (StatusCode::NOT_FOUND, "Job not found").into_response(),
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    }
}

async fn apply_to_job(
    State(state): State<AppState>,
    auth: AuthUser,
    Path(id): Path<Uuid>,
    Json(payload): Json<ApplyRequest>,
) -> impl IntoResponse {
    let seeker = match JobSeekerRepository::get_by_user_id(&state.pool, auth.user_id).await {
        Ok(Some(s)) => s,
        _ => return (StatusCode::NOT_FOUND, "Job seeker profile not found").into_response(),
    };

    let job = match JobRepository::get_by_id(&state.pool, id).await {
        Ok(Some(j)) if j.status == "LIVE" => j,
        Ok(Some(_)) => return (StatusCode::BAD_REQUEST, "Job is not live").into_response(),
        _ => return (StatusCode::NOT_FOUND, "Job not found").into_response(),
    };

    if seeker.active_application_count >= 50 {
        return (StatusCode::TOO_MANY_REQUESTS, "Max 50 active applications").into_response();
    }

    let db_payload = CreateApplicationPayload {
        job_id: job.id,
        applicant_user_id: auth.user_id,
        cover_note: payload.cover_note,
    };

    match ApplicationRepository::create(&state.pool, db_payload).await {
        Ok(app) => {
            let _ = JobSeekerRepository::update_active_application_count(&state.pool, seeker.id, 1).await;
            
            // Send email notification to company
            // Get company user details via raw query
            let company_user = sqlx::query_as::<_, (String, Option<String>, uuid::Uuid)>(
                "SELECT u.email, CONCAT(u.first_name, ' ', u.last_name) AS name, u.id FROM users u INNER JOIN company_profiles c ON c.user_id = u.id WHERE c.id = $1"
            )
            .bind(job.company_id)
            .fetch_optional(&state.pool)
            .await;

            if let Ok(Some((email, name, company_user_id))) = company_user {
                let seeker_name = seeker.full_name.clone().filter(|n| !n.is_empty()).unwrap_or_else(|| "A candidate".to_string());
                let _ = state.mail.send_new_application_email(
                    &email,
                    name.as_deref().unwrap_or("Company"),
                    &job.title,
                    &seeker_name
                ).await;

                // Send in-app notification to company
                sqlx::query(
                    r#"INSERT INTO notifications (user_id, title, body, type, reference_id)
                       VALUES ($1, $2, $3, $4, $5)"#,
                )
                .bind(company_user_id)
                .bind("New Application Received")
                .bind(format!("{} applied for your job '{}'. View their application now.", seeker_name, job.title))
                .bind("APPLICATION")
                .bind(app.id)
                .execute(&state.pool)
                .await
                .ok();
            }
            
            (StatusCode::CREATED, Json(app)).into_response()
        }
        Err(e) => {
            if e.to_string().contains("unique") {
                (StatusCode::CONFLICT, "Already applied to this job").into_response()
            } else {
                (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
            }
        }
    }
}

async fn list_my_applications(
    State(state): State<AppState>,
    auth: AuthUser,
    Query(q): Query<PaginationQuery>,
) -> impl IntoResponse {
    let _seeker = match JobSeekerRepository::get_by_user_id(&state.pool, auth.user_id).await {
        Ok(Some(s)) => s,
        _ => return (StatusCode::NOT_FOUND, "Job seeker profile not found").into_response(),
    };

    let page = q.page.unwrap_or(1);
    let limit = q.limit.unwrap_or(20);

    match ApplicationRepository::list_by_user_id(&state.pool, auth.user_id, page, limit).await {
        Ok(apps) => (StatusCode::OK, Json(serde_json::json!({
            "data": apps,
            "pagination": { "page": page, "limit": limit }
        }))).into_response(),
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    }
}

async fn get_my_application(
    State(state): State<AppState>,
    auth: AuthUser,
    Path(id): Path<Uuid>,
) -> impl IntoResponse {
    let _seeker = match JobSeekerRepository::get_by_user_id(&state.pool, auth.user_id).await {
        Ok(Some(s)) => s,
        _ => return (StatusCode::NOT_FOUND, "Job seeker profile not found").into_response(),
    };

    match ApplicationRepository::get_by_id(&state.pool, id).await {
        Ok(Some(app)) if app.applicant_user_id == auth.user_id => (StatusCode::OK, Json(app)).into_response(),
        Ok(Some(_)) => (StatusCode::FORBIDDEN, "Access denied").into_response(),
        Ok(None) => (StatusCode::NOT_FOUND, "Application not found").into_response(),
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    }
}

async fn withdraw_application(
    State(state): State<AppState>,
    auth: AuthUser,
    Path(id): Path<Uuid>,
) -> impl IntoResponse {
    let seeker = match JobSeekerRepository::get_by_user_id(&state.pool, auth.user_id).await {
        Ok(Some(s)) => s,
        _ => return (StatusCode::NOT_FOUND, "Job seeker profile not found").into_response(),
    };

    let app = match ApplicationRepository::get_by_id(&state.pool, id).await {
        Ok(Some(a)) if a.applicant_user_id == auth.user_id => a,
        Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(),
        _ => return (StatusCode::NOT_FOUND, "Application not found").into_response(),
    };

    match ApplicationRepository::update_status(&state.pool, app.id, "WITHDRAWN").await {
        Ok(updated) => {
            let _ = JobSeekerRepository::update_active_application_count(&state.pool, seeker.id, -1).await;
            (StatusCode::OK, Json(updated)).into_response()
        }
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    }
}

async fn submit_for_verification(
    State(state): State<AppState>,
    auth: AuthUser,
) -> impl IntoResponse {
    let seeker = match JobSeekerRepository::get_by_user_id(&state.pool, auth.user_id).await {
        Ok(Some(s)) => s,
        Ok(None) => return (StatusCode::NOT_FOUND, "Job seeker profile not found").into_response(),
        Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    };

    if matches!(
        seeker.status.as_str(),
        "PENDING_REVIEW"
            | "PENDING"
            | "UNDER_REVIEW"
            | "DOCUMENTS_REQUESTED"
            | "REVISION_REQUESTED"
            | "APPROVED"
    ) {
        return (StatusCode::BAD_REQUEST, format!("Profile is already {}", seeker.status)).into_response();
    }

    match JobSeekerRepository::submit_for_verification(&state.pool, auth.user_id).await {
        Ok(profile) => (StatusCode::OK, Json(serde_json::json!({
            "status": profile.status,
            "message": "Profile submitted for verification"
        }))).into_response(),
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    }
}

async fn upload_document(
    State(state): State<AppState>,
    auth: AuthUser,
    mut multipart: Multipart,
) -> impl IntoResponse {
    // A job seeker who hasn't saved any basic profile info yet won't have a
    // job_seeker_profiles row - lazily create a blank one instead of 404ing,
    // since uploading a verification document doesn't depend on that data.
    let seeker = match get_or_create_job_seeker_profile(&state.pool, auth.user_id).await {
        Ok(s) => s,
        Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(),
    };

    let mut file_bytes = bytes::BytesMut::new();
    let mut content_type = "application/octet-stream".to_string();
    let mut ext = "bin".to_string();
    let mut found = false;

    // Extract document_type from multipart fields (non-file fields)
    let mut document_type = "other".to_string();
    let mut file_name = "document".to_string();
    let mut file_size: i64 = 0;

    while let Ok(Some(field)) = multipart.next_field().await {
        let name = field.name().unwrap_or("").to_string();

        if name == "document_type" {
            if let Ok(text) = field.text().await {
                document_type = text;
            }
        } else if name == "file_name" {
            if let Ok(text) = field.text().await {
                file_name = text;
            }
        } else if name == "file" || name == "document" || (!found && !name.is_empty() && field.file_name().is_some()) {
            if let Some(ct) = field.content_type() {
                content_type = ct.to_string();
                ext = match ct {
                    "application/pdf" => "pdf",
                    "image/jpeg" => "jpg",
                    "image/png" => "png",
                    "image/webp" => "webp",
                    "application/msword" => "doc",
                    "application/vnd.openxmlformats-officedocument.wordprocessingml.document" => "docx",
                    _ => "bin",
                }.to_string();
            }

            if let Some(fname) = field.file_name() {
                file_name = fname.to_string();
                if ext == "bin" {
                    if let Some(e) = fname.rsplit('.').next() {
                        ext = e.to_lowercase();
                    }
                }
            }

            let data = match field.bytes().await {
                Ok(b) => b,
                Err(e) => return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": format!("Failed to read file: {}", e) }))).into_response(),
            };

            if data.is_empty() {
                return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Empty file" }))).into_response();
            }

            if data.len() > 10 * 1024 * 1024 {
                return (StatusCode::PAYLOAD_TOO_LARGE, Json(serde_json::json!({ "error": "File too large. Maximum 10 MB." }))).into_response();
            }

            file_size = data.len() as i64;
            file_bytes.put(data);
            found = true;
        }
    }

    if !found || file_bytes.is_empty() {
        return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "No document file provided. Send a multipart field named 'file' or 'document'." }))).into_response();
    }

    // Upload to Backblaze B2 under "documents" prefix
    let file_url = match state.storage
        .upload("documents", &ext, file_bytes.freeze(), &content_type)
        .await
    {
        Ok(url) => url,
        Err(e) => {
            tracing::error!("B2 upload failed: {:?}", e);
            return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "storage_upload_failed", "detail": format!("{:?}", e) }))).into_response();
        }
    };

    let payload = CreateJobSeekerDocumentPayload {
        document_type,
        file_name: file_name.clone(),
        file_size,
        mime_type: content_type,
    };

    match JobSeekerRepository::create_document(&state.pool, seeker.id, payload, file_url.clone()).await {
        Ok(doc) => (StatusCode::CREATED, Json(serde_json::json!({
            "id": doc.id,
            "document_type": doc.document_type,
            "file_name": doc.file_name,
            "file_url": doc.file_url,
            "file_size": doc.file_size,
            "mime_type": doc.mime_type,
            "created_at": doc.created_at,
        }))).into_response(),
        Err(e) => {
            tracing::error!("Failed to save document record: {}", e);
            // Best-effort cleanup
            state.storage.delete_by_url(&file_url).await;
            (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "Failed to save document record" }))).into_response()
        }
    }
}

async fn list_documents(
    State(state): State<AppState>,
    auth: AuthUser,
) -> impl IntoResponse {
    // No profile row yet means no documents have ever been uploaded either -
    // that's a normal empty state, not an error.
    let seeker = match JobSeekerRepository::get_by_user_id(&state.pool, auth.user_id).await {
        Ok(Some(s)) => s,
        Ok(None) => return (StatusCode::OK, Json(serde_json::json!({ "data": [] }))).into_response(),
        Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(),
    };

    match JobSeekerRepository::list_documents(&state.pool, seeker.id).await {
        Ok(docs) => (StatusCode::OK, Json(serde_json::json!({ "data": docs }))).into_response(),
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    }
}

async fn delete_document(
    State(state): State<AppState>,
    auth: AuthUser,
    Path(id): Path<Uuid>,
) -> impl IntoResponse {
    let seeker = match JobSeekerRepository::get_by_user_id(&state.pool, auth.user_id).await {
        Ok(Some(s)) => s,
        Ok(None) => return (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "Job seeker profile not found" }))).into_response(),
        Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(),
    };

    // Fetch doc to get file_url for cleanup
    match JobSeekerRepository::list_documents(&state.pool, seeker.id).await {
        Ok(docs) => {
            let doc = docs.iter().find(|d| d.id == id);
            if doc.is_none() {
                return (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "Document not found" }))).into_response();
            }
            let file_url = doc.unwrap().file_url.clone();

            match JobSeekerRepository::delete_document(&state.pool, seeker.id, id).await {
                Ok(_) => {
                    state.storage.delete_by_url(&file_url).await;
                    (StatusCode::OK, Json(serde_json::json!({ "message": "Document deleted" }))).into_response()
                }
                Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
            }
        }
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    }
}
