pub mod admin;

use axum::{
    extract::{Multipart, Path, Query, State},
    http::StatusCode,
    response::IntoResponse,
    routing::{get, patch, post},
    Json, Router,
};
use redis::AsyncCommands;
use serde::Deserialize;
use uuid::Uuid;
use db::models::company::{CompanyRepository, UpsertCompanyProfilePayload};
use db::models::job::{JobRepository, CreateJobPayload as DbCreateJobPayload, UpdateJobPayload as DbUpdateJobPayload};
use db::models::application::ApplicationRepository;
use db::models::user::UserRepository;
use db::models::verification::VerificationRepository;
use contracts::auth_middleware::AuthUser;
use crate::AppState;

pub fn router() -> Router<AppState> {
    Router::new()
        .route("/profile/me", get(get_profile).patch(update_profile))
        .route("/profile/documents", post(upload_documents))
        .route("/profile/submit", post(submit_for_verification))
        .route("/profile/submit-with-documents", post(submit_with_documents))
        .route("/jobs", get(list_jobs).post(create_job))
        .route("/jobs/{id}", get(get_job).patch(update_job))
        .route("/jobs/{id}/submit", post(submit_job))
        .route("/jobs/{id}/close", post(close_job))
        .route("/jobs/{id}/applications", get(list_applications))
        .route("/applications/{id}/status", patch(update_application_status))
        .route("/applications/{id}/contact", get(view_contact))
}

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

#[derive(Deserialize)]
pub struct CreateJobRequest {
    pub title: String,
    pub description: String,
    pub location: String,
    pub job_type: Option<String>,
    pub salary_min: Option<i32>,
    pub salary_max: Option<i32>,
    pub experience_years: Option<i32>,
    pub skills: Option<Vec<String>>,
    pub category: Option<String>,
}

#[derive(Deserialize)]
pub struct UpdateApplicationStatusPayload {
    pub status: String,
}

async fn get_profile(
    State(state): State<AppState>,
    auth: AuthUser,
) -> impl IntoResponse {
    let cache_key = format!("profile:company:{}", 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 company profile: {}", auth.user_id);
        if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&cached) {
            return (StatusCode::OK, Json(parsed)).into_response();
        }
    }

    match CompanyRepository::get_by_user_id(&state.pool, auth.user_id).await {
        Ok(Some(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()
        }
        Ok(None) => (StatusCode::NOT_FOUND, "Company profile not found").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<UpsertCompanyProfilePayload>,
) -> impl IntoResponse {
    match CompanyRepository::upsert(&state.pool, auth.user_id, payload).await {
        Ok(profile) => {
            // Invalidate profile cache
            let cache_key = format!("profile:company:{}", auth.user_id);
            let mut redis = state.redis.clone();
            let _ = redis.del::<_, ()>(&cache_key).await;
            (StatusCode::OK, Json(profile)).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 company = match CompanyRepository::get_by_user_id(&state.pool, auth.user_id).await {
        Ok(Some(c)) => c,
        Ok(None) => return (StatusCode::NOT_FOUND, "Company profile not found").into_response(),
        Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    };

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

    match CompanyRepository::submit_for_verification(&state.pool, auth.user_id).await {
        Ok(profile) => {
            // Invalidate company profile cache
            let cache_key = format!("profile:company:{}", auth.user_id);
            let mut redis = state.redis.clone();
            let _ = redis.del::<_, ()>(&cache_key).await;
            (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 list_jobs(
    State(state): State<AppState>,
    auth: AuthUser,
    Query(q): Query<PaginationQuery>,
) -> impl IntoResponse {
    let company = match CompanyRepository::get_by_user_id(&state.pool, auth.user_id).await {
        Ok(Some(c)) => c,
        _ => return (StatusCode::NOT_FOUND, "Company not found").into_response(),
    };

    let page = q.page.unwrap_or(1);
    let limit = q.limit.unwrap_or(20);
    let status_filter = q.status.as_deref().unwrap_or("");

    // Build cache key
    let cache_key = format!("jobs:company:{}:{}:{}:{}", company.id, page, limit, status_filter);
    let mut redis = state.redis.clone();

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

    match JobRepository::list_by_company_id(&state.pool, company.id, q.status, page, limit).await {
        Ok(jobs) => {
            let response = serde_json::json!({
                "data": jobs,
                "pagination": { "page": page, "limit": limit }
            });
            // Cache 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) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    }
}

async fn create_job(
    State(state): State<AppState>,
    auth: AuthUser,
    Json(payload): Json<CreateJobRequest>,
) -> impl IntoResponse {
    let company = match CompanyRepository::get_by_user_id(&state.pool, auth.user_id).await {
        Ok(Some(c)) => c,
        Ok(None) => return (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "Company not found" }))).into_response(),
        Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(),
    };

    if company.status != "APPROVED" {
        return (
            StatusCode::FORBIDDEN,
            Json(serde_json::json!({
                "error": "Company profile approval is required before posting jobs",
                "code": "COMPANY_NOT_APPROVED"
            })),
        ).into_response();
    }

    // --- New Quota Logic ---
    let jobs_this_month = match JobRepository::count_by_company_id_this_month(&state.pool, company.id).await {
        Ok(count) => count,
        Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(),
    };

    if jobs_this_month >= 1 {
        // Must use a purchased slot if they've already used their monthly freebie
        if company.purchased_job_slots <= 0 {
            return (
                StatusCode::PAYMENT_REQUIRED,
                Json(serde_json::json!({
                    "error": "Monthly free job quota exhausted. Please purchase job slots.",
                    "code": "QUOTA_EXHAUSTED",
                    "requires_tracecoins": true
                }))
            ).into_response();
        }

        // Deduct ONE purchased slot
        let deduct_result = sqlx::query(
            "UPDATE company_profiles SET purchased_job_slots = purchased_job_slots - 1 WHERE id = $1",
        )
        .bind(company.id)
        .execute(&state.pool)
        .await;

        if let Err(e) = deduct_result {
            tracing::error!("Failed to deduct job slot: {}", e);
            return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "Failed to deduct quota" }))).into_response();
        }
    }
    // -----------------------

    let db_payload = DbCreateJobPayload {
        company_id: company.id,
        title: payload.title,
        category: payload.category,
        description: payload.description,
        location: payload.location,
        job_type: payload.job_type,
        salary_min: payload.salary_min,
        salary_max: payload.salary_max,
        experience_years: payload.experience_years,
        skills: payload.skills,
    };

    match JobRepository::create(&state.pool, db_payload).await {
        Ok(job) => {
            // Invalidate company's job list cache
            let mut redis = state.redis.clone();
            let pattern = format!("jobs:company:{}:*", company.id);
            if let Ok(keys) = redis.keys::<_, Vec<String>>(pattern).await {
                if !keys.is_empty() {
                    let _ = redis.del::<_, ()>(keys).await;
                }
            }
            (StatusCode::CREATED, Json(job)).into_response()
        }
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(),
    }
}

async fn get_job(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
    _auth: AuthUser,
) -> impl IntoResponse {
    match JobRepository::get_by_id(&state.pool, id).await {
        Ok(Some(job)) => (StatusCode::OK, Json(job)).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 update_job(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
    auth: AuthUser,
    Json(payload): Json<DbUpdateJobPayload>,
) -> impl IntoResponse {
    let company = match CompanyRepository::get_by_user_id(&state.pool, auth.user_id).await {
        Ok(Some(c)) => c,
        _ => return (StatusCode::NOT_FOUND, "Company not found").into_response(),
    };

    if company.status != "APPROVED" {
        return (StatusCode::FORBIDDEN, "Company profile approval is required before submitting jobs").into_response();
    }

    let job = match JobRepository::get_by_id(&state.pool, id).await {
        Ok(Some(j)) if j.company_id == company.id => j,
        Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(),
        _ => return (StatusCode::NOT_FOUND, "Job not found").into_response(),
    };

    match JobRepository::update(&state.pool, job.id, payload).await {
        Ok(updated) => {
            // Invalidate company job list cache
            let mut redis = state.redis.clone();
            let pattern = format!("jobs:company:{}:*", company.id);
            if let Ok(keys) = redis.keys::<_, Vec<String>>(pattern).await {
                if !keys.is_empty() {
                    let _ = redis.del::<_, ()>(keys).await;
                }
            }
            (StatusCode::OK, Json(updated)).into_response()
        }
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    }
}

async fn submit_job(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
    auth: AuthUser,
) -> impl IntoResponse {
    let company = match CompanyRepository::get_by_user_id(&state.pool, auth.user_id).await {
        Ok(Some(c)) => c,
        _ => return (StatusCode::NOT_FOUND, "Company not found").into_response(),
    };

    let job = match JobRepository::get_by_id(&state.pool, id).await {
        Ok(Some(j)) if j.company_id == company.id => j,
        Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(),
        _ => return (StatusCode::NOT_FOUND, "Job not found").into_response(),
    };

    if job.status != "DRAFT" {
        return (StatusCode::BAD_REQUEST, "Job already submitted or live").into_response();
    }

    match JobRepository::update_status(&state.pool, job.id, "PENDING_APPROVAL").await {
        Ok(updated) => {
            if let Ok(user) = UserRepository::get_by_id(&state.pool, auth.user_id).await {
                if let Err(e) = state.mail.send_job_submitted_email(&user.email, &format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default()), &updated.title).await {
                    tracing::error!("Failed to send job-submitted email to {}: {:?}", user.email, e);
                }
            }

            // Create verification case so the request appears in Verification Management first.
            let verification_payload = serde_json::json!({
                "entity_type": "JOB",
                "entity_id": updated.id,
                "title": updated.title,
                "category": updated.category,
                "location": updated.location,
                "job_type": updated.job_type,
                "status": updated.status,
                "company_id": updated.company_id,
            });
            let _ = VerificationRepository::create(
                &state.pool,
                auth.user_id,
                "COMPANY",
                "JOB_APPROVAL",
                "MEDIUM",
                verification_payload,
                serde_json::json!([]),
            )
            .await;
            // Invalidate company job list cache
            let mut redis = state.redis.clone();
            let pattern = format!("jobs:company:{}:*", company.id);
            if let Ok(keys) = redis.keys::<_, Vec<String>>(pattern).await {
                if !keys.is_empty() {
                    let _ = redis.del::<_, ()>(keys).await;
                }
            }
            (StatusCode::OK, Json(updated)).into_response()
        }
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    }
}

async fn close_job(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
    auth: AuthUser,
) -> impl IntoResponse {
    let company = match CompanyRepository::get_by_user_id(&state.pool, auth.user_id).await {
        Ok(Some(c)) => c,
        _ => return (StatusCode::NOT_FOUND, "Company not found").into_response(),
    };

    let job = match JobRepository::get_by_id(&state.pool, id).await {
        Ok(Some(j)) if j.company_id == company.id => j,
        Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(),
        _ => return (StatusCode::NOT_FOUND, "Job not found").into_response(),
    };

    match JobRepository::update_status(&state.pool, job.id, "CLOSED").await {
        Ok(updated) => {
            // Invalidate company job list cache
            let mut redis = state.redis.clone();
            let pattern = format!("jobs:company:{}:*", company.id);
            if let Ok(keys) = redis.keys::<_, Vec<String>>(pattern).await {
                if !keys.is_empty() {
                    let _ = redis.del::<_, ()>(keys).await;
                }
            }
            (StatusCode::OK, Json(updated)).into_response()
        }
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    }
}

async fn list_applications(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
    auth: AuthUser,
    Query(q): Query<PaginationQuery>,
) -> impl IntoResponse {
    let company = match CompanyRepository::get_by_user_id(&state.pool, auth.user_id).await {
        Ok(Some(c)) => c,
        _ => return (StatusCode::NOT_FOUND, "Company not found").into_response(),
    };

    let job = match JobRepository::get_by_id(&state.pool, id).await {
        Ok(Some(j)) if j.company_id == company.id => j,
        Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(),
        _ => return (StatusCode::NOT_FOUND, "Job not found").into_response(),
    };

    let page = q.page.unwrap_or(1);
    let limit = q.limit.unwrap_or(20);
    let apps = match ApplicationRepository::list_by_job_id(&state.pool, job.id, q.status, page, limit).await {
        Ok(a) => a,
        Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    };

    // Enrich each application with job seeker profile snapshot (no contact info)
    let mut enriched = Vec::with_capacity(apps.len());
    for app in apps {
        use sqlx::Row;
        let row = sqlx::query(
            r#"
            SELECT
                CONCAT(u.first_name, ' ', u.last_name)   AS applicant_name,
                u.avatar_url,
                js.resume_url,
                js.custom_data
            FROM users u
            LEFT JOIN job_seeker_profiles js ON js.user_id = u.id
            WHERE u.id = $1
            "#,
        )
        .bind(app.applicant_user_id)
        .fetch_optional(&state.pool)
        .await
        .ok()
        .flatten();

        let applicant_name: String = row.as_ref()
            .and_then(|r| r.try_get("applicant_name").ok())
            .unwrap_or_default();
        let avatar_url: Option<String> = row.as_ref()
            .and_then(|r| r.try_get("avatar_url").ok());
        let resume_url: Option<String> = row.as_ref()
            .and_then(|r| r.try_get("resume_url").ok());
        let custom_data: Option<serde_json::Value> = row.as_ref()
            .and_then(|r| r.try_get("custom_data").ok());

        // Extract portfolio fields — never expose email/phone
        let portfolio = custom_data
            .as_ref()
            .and_then(|d| d.get("job_seeker_portfolio"))
            .cloned()
            .unwrap_or(serde_json::Value::Null);

        let headline = portfolio.get("headline").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 education = portfolio.get("education").and_then(|v| v.as_str()).unwrap_or("").to_string();
        let work_experience = portfolio.get("workExperience").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();

        enriched.push(serde_json::json!({
            "id":                app.id,
            "job_id":            app.job_id,
            "applicant_user_id": app.applicant_user_id,
            "status":            app.status,
            "cover_note":        app.cover_note,
            "applied_at":        app.applied_at,
            "updated_at":        app.updated_at,
            // Profile snapshot — contact fields intentionally omitted
            "applicant_name":    applicant_name,
            "avatar_url":        avatar_url,
            "resume_url":        resume_url,
            "headline":          headline,
            "skills":            skills,
            "education":         education,
            "work_experience":   work_experience,
            "summary":           summary,
        }));
    }

    (StatusCode::OK, Json(serde_json::json!({
        "data": enriched,
        "pagination": { "page": page, "limit": limit }
    }))).into_response()
}

async fn update_application_status(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
    auth: AuthUser,
    Json(payload): Json<UpdateApplicationStatusPayload>,
) -> impl IntoResponse {
    let app = match ApplicationRepository::get_by_id(&state.pool, id).await {
        Ok(Some(a)) => a,
        _ => return (StatusCode::NOT_FOUND, "Application not found").into_response(),
    };

    let job = match JobRepository::get_by_id(&state.pool, app.job_id).await {
        Ok(Some(j)) => j,
        _ => return (StatusCode::INTERNAL_SERVER_ERROR, "Job lost").into_response(),
    };

    let company = match CompanyRepository::get_by_user_id(&state.pool, auth.user_id).await {
        Ok(Some(c)) => c,
        _ => return (StatusCode::FORBIDDEN, "Access denied").into_response(),
    };

    if job.company_id != company.id {
        return (StatusCode::FORBIDDEN, "Access denied").into_response();
    }

    match ApplicationRepository::update_status(&state.pool, app.id, &payload.status).await {
        Ok(updated) => {
            // Notify applicant of status change (ignore failures)
            let applicant_info = sqlx::query_as::<_, (String, String, Uuid)>(
                "SELECT CONCAT(u.first_name, ' ', u.last_name) AS name, u.email, u.id FROM users u WHERE u.id = $1",
            )
            .bind(app.applicant_user_id)
            .fetch_optional(&state.pool)
            .await;
            if let Ok(Some((name, email, applicant_uuid))) = applicant_info {
                let _ = state.mail.send_application_status_email(&email, &name, &job.title, &payload.status).await;

                // Send in-app notification to job seeker
                sqlx::query(
                    r#"INSERT INTO notifications (user_id, title, body, type, reference_id)
                       VALUES ($1, $2, $3, $4, $5)"#,
                )
                .bind(applicant_uuid)
                .bind(format!("Application Status: {}", payload.status))
                .bind(format!("Your application for '{}' has been {}.", job.title, payload.status.to_lowercase()))
                .bind("APPLICATION")
                .bind(app.id)
                .execute(&state.pool)
                .await
                .ok();
            }
            (StatusCode::OK, Json(updated)).into_response()
        }
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    }
}

async fn upload_documents(
    State(state): State<AppState>,
    auth: AuthUser,
    mut multipart: Multipart,
) -> impl IntoResponse {
    let company = match CompanyRepository::get_by_user_id(&state.pool, auth.user_id).await {
        Ok(Some(c)) => c,
        Ok(None) => return (StatusCode::NOT_FOUND, "Company profile not found").into_response(),
        Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    };

    let mut uploaded_urls: Vec<String> = Vec::new();

    while let Ok(Some(field)) = multipart.next_field().await {
        let name = field.name().unwrap_or("").to_string();
        if name != "documents" && name != "files" && name != "file" {
            continue;
        }

        let content_type = field.content_type()
            .unwrap_or("application/octet-stream")
            .to_string();

        let ext = if let Some(fname) = field.file_name() {
            fname.rsplit('.').next().unwrap_or("bin").to_lowercase()
        } else {
            match content_type.as_str() {
                "application/pdf" => "pdf".to_string(),
                "image/jpeg" => "jpg".to_string(),
                "image/png" => "png".to_string(),
                _ => "bin".to_string(),
            }
        };

        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() {
            continue;
        }

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

        let data_len = data.len();
        let url = match state.storage
            .upload("company_documents", &ext, data, &content_type)
            .await
        {
            Ok(u) => u,
            Err(e) => {
                tracing::error!("B2 upload failed for company {}: {:?}", company.id, e);
                return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "storage_upload_failed", "detail": format!("{:?}", e) }))).into_response();
            }
        };

        // Persist document record
        if let Err(e) = sqlx::query(
            r#"
            INSERT INTO company_documents (company_id, document_name, document_url, file_size, mime_type)
            VALUES ($1, $2, $3, $4, $5)
            "#,
        )
        .bind(company.id)
        .bind(format!("document_{}", Uuid::new_v4()))
        .bind(&url)
        .bind(data_len as i64)
        .bind(&content_type)
        .execute(&state.pool)
        .await
        {
            tracing::error!("Failed to save document record for company {}: {}", company.id, e);
        }

        uploaded_urls.push(url);
    }

    if uploaded_urls.is_empty() {
        return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "No valid document files provided. Send multipart fields named 'documents'." }))).into_response();
    }

    (StatusCode::OK, Json(serde_json::json!({
        "documents": uploaded_urls,
        "count": uploaded_urls.len()
    }))).into_response()
}

async fn view_contact(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
    auth: AuthUser,
) -> impl IntoResponse {
    let app = match ApplicationRepository::get_by_id(&state.pool, id).await {
        Ok(Some(a)) => a,
        _ => return (StatusCode::NOT_FOUND, "Application not found").into_response(),
    };

    let job = match JobRepository::get_by_id(&state.pool, app.job_id).await {
        Ok(Some(j)) => j,
        _ => return (StatusCode::INTERNAL_SERVER_ERROR, "Job lost").into_response(),
    };

    let company = match CompanyRepository::get_by_user_id(&state.pool, auth.user_id).await {
        Ok(Some(c)) => c,
        _ => return (StatusCode::FORBIDDEN, "Access denied").into_response(),
    };

    if job.company_id != company.id {
        return (StatusCode::FORBIDDEN, "Access denied").into_response();
    }

    let free_views = company.free_contact_views;
    let purchased_views = company.purchased_contact_views;

    if free_views <= 0 && purchased_views <= 0 {
        return (StatusCode::PAYMENT_REQUIRED, Json(serde_json::json!({
            "error": "Contact view quota exhausted",
            "code": "QUOTA_EXHAUSTED",
            "requires_purchase": true,
            "message": "You have used all your free contact views. Please purchase a contact view package to continue."
        }))).into_response();
    }

    let used_free = free_views > 0;

    if used_free {
        sqlx::query(
            "UPDATE company_profiles SET free_contact_views = free_contact_views - 1, updated_at = NOW() WHERE id = $1"
        )
        .bind(company.id)
        .execute(&state.pool)
        .await
        .ok();
    } else {
        sqlx::query(
            "UPDATE company_profiles SET purchased_contact_views = purchased_contact_views - 1, updated_at = NOW() WHERE id = $1"
        )
        .bind(company.id)
        .execute(&state.pool)
        .await
        .ok();
    }

    let contact = sqlx::query_as::<_, (Option<String>, String, Option<String>)>(
        r#"
        SELECT CONCAT(u.first_name, ' ', u.last_name) AS name, u.email, u.phone
        FROM users u
        WHERE u.id = $1
        "#,
    )
    .bind(app.applicant_user_id)
    .fetch_optional(&state.pool)
    .await;

    match contact {
        Ok(Some((name, email, phone))) => {
            let new_free = if used_free { free_views - 1 } else { free_views };
            let new_purchased = if used_free { purchased_views } else { purchased_views - 1 };

            let _ = sqlx::query(
                r#"
                INSERT INTO notifications (user_id, title, body, type, reference_id)
                VALUES ($1, $2, $3, $4, $5)
                "#
            )
            .bind(app.applicant_user_id)
            .bind("Your contact was viewed")
            .bind(format!("{} viewed your application for {}", company.company_name, job.title))
            .bind("APPLICATION")
            .bind(id)
            .execute(&state.pool)
            .await
            .ok();

            (StatusCode::OK, Json(serde_json::json!({
                "application_id": id,
                "name": name,
                "email": email,
                "phone": phone,
                "quota": {
                    "used_free_view": used_free,
                    "free_remaining": new_free,
                    "purchased_remaining": new_purchased,
                    "total_remaining": new_free + new_purchased
                }
            }))).into_response()
        }
        Ok(None) => (StatusCode::NOT_FOUND, "Applicant not found").into_response(),
        Err(e) => {
            tracing::error!("Failed to fetch applicant contact: {}", e);
            (StatusCode::INTERNAL_SERVER_ERROR, "Failed to fetch contact info").into_response()
        }
    }
}

/// POST /api/companies/profile/submit-with-documents
///
/// Accepts multipart/form-data with:
///   * `profile`   — JSON string with company fields
///   * `documents` — one or more file parts
///
/// In one request this handler:
///   1. Saves the profile into `company_profiles` (DRAFT upsert, same SQL as users service).
///   2. Uploads each document to B2 (`storage_backend` is always `"b2"`). On B2 failure
///      the file is recorded in `failure_details` and `documents_failed` is incremented,
///      then processing continues with the next file. No local-disk fallback.
///   3. Creates a `verifications` row (case_type = "PROFILE_VERIFICATION", priority = "MEDIUM").
///   4. Updates `company_profiles.status` to 'PENDING'.
///   5. Returns `{ verification_id, status, documents_uploaded, documents_failed, storage_backend }`.
async fn submit_with_documents(
    State(state): State<AppState>,
    auth: AuthUser,
    mut multipart: Multipart,
) -> impl IntoResponse {
    // ---- 1. Parse multipart: pull `profile` JSON first, then read each `documents` file
    //         in-place (we can't hold a Field across loop iterations because Multipart is mutably
    //         borrowed by next_field). We delay the company_documents DB insert until step 2b
    //         because the company_profiles.id FK isn't available until the profile upsert runs. ----
    let mut profile_json_str: Option<String> = None;
    let mut uploaded_files: Vec<(String, i64, String)> = Vec::new(); // (url, data_len, content_type)
    let mut documents_meta: Vec<serde_json::Value> = Vec::new();
    let mut documents_uploaded: usize = 0;
    let mut documents_failed: usize = 0;
    let storage_backend: &str = "b2";
    let mut failure_details: Vec<serde_json::Value> = Vec::new();

    while let Ok(Some(field)) = multipart.next_field().await {
        let name = field.name().unwrap_or("").to_string();
        if name == "profile" {
            match field.text().await {
                Ok(t) => profile_json_str = Some(t),
                Err(e) => {
                    return (
                        StatusCode::BAD_REQUEST,
                        Json(serde_json::json!({ "error": format!("Failed to read profile field: {}", e) })),
                    )
                        .into_response();
                }
            }
        } else if name == "documents" || name == "files" || name == "file" {
            // ---- 2a. Upload to B2. ----
            let original_filename = field.file_name().unwrap_or("").to_string();
            let content_type = field
                .content_type()
                .unwrap_or("application/octet-stream")
                .to_string();

            let ext = if !original_filename.is_empty() {
                original_filename
                    .rsplit('.')
                    .next()
                    .unwrap_or("bin")
                    .to_lowercase()
            } else {
                match content_type.as_str() {
                    "application/pdf" => "pdf".to_string(),
                    "image/jpeg" => "jpg".to_string(),
                    "image/png" => "png".to_string(),
                    _ => "bin".to_string(),
                }
            };

            let data = match field.bytes().await {
                Ok(b) => b,
                Err(e) => {
                    documents_failed += 1;
                    failure_details.push(serde_json::json!({
                        "filename": original_filename,
                        "error": format!("Failed to read file bytes: {}", e),
                    }));
                    continue;
                }
            };

            if data.is_empty() {
                // silently skip empty files (matches existing upload_documents behaviour)
                continue;
            }

            if data.len() > 10 * 1024 * 1024 {
                documents_failed += 1;
                failure_details.push(serde_json::json!({
                    "filename": original_filename,
                    "error": "File too large. Maximum 10 MB per file.",
                }));
                continue;
            }

            let data_len = data.len() as i64;

            // Upload to B2. On failure, record the file in failure_details and continue.
            let url: String = match state
                .storage
                .upload("company_documents", &ext, data.clone(), &content_type)
                .await
            {
                Ok(u) => u,
                Err(b2_err) => {
                    tracing::warn!(
                        "B2 upload failed for company user {} ({}): {}",
                        auth.user_id,
                        original_filename,
                        b2_err
                    );
                    documents_failed += 1;
                    failure_details.push(serde_json::json!({
                        "filename": original_filename,
                        "error": format!("B2 upload failed: {}", b2_err),
                    }));
                    continue;
                }
            };

            // storage_backend is always "b2" (set at top of function).

            // The frontend encodes the document type as the part before the first '|'
            // in the filename (e.g. "registration_doc|scan001.pdf" → "registration_doc").
            // Splitting on '|' is unambiguous because doc-type keys never contain '|'.
            let document_type = if !original_filename.is_empty() {
                original_filename
                    .splitn(2, '|')
                    .next()
                    .filter(|s| !s.is_empty())
                    .unwrap_or("document")
                    .to_string()
            } else {
                "document".to_string()
            };

            documents_meta.push(serde_json::json!({
                "url": url,
                "document_type": document_type,
                "file_size": data_len,
                "mime_type": content_type,
            }));
            uploaded_files.push((url, data_len, content_type));
            documents_uploaded += 1;
        }
        // ignore other fields silently
    }

    let profile_str = match profile_json_str {
        Some(s) => s,
        None => {
            return (
                StatusCode::BAD_REQUEST,
                Json(serde_json::json!({ "error": "Missing 'profile' multipart field (JSON string)." })),
            )
                .into_response();
        }
    };

    let profile_wrapper: serde_json::Value = match serde_json::from_str(&profile_str) {
        Ok(v) => v,
        Err(e) => {
            return (
                StatusCode::BAD_REQUEST,
                Json(serde_json::json!({ "error": format!("Invalid profile JSON: {}", e) })),
            )
                .into_response();
        }
    };

    // The frontend sends `{ roleKey, profile_data: {...actual fields} }` (see
    // submitCompanyProfileWithDocuments in nxtgauge-frontend-solid/src/lib/api.ts).
    // Unwrap `profile_data` here so every field lookup below and the verification
    // payload we store operate on the actual company fields, not the outer wrapper -
    // reading e.g. `company_name` directly off the wrapper always came back empty,
    // and storing the wrapper itself as the verification payload made admin-side
    // rendering of company name/details break for every submission through this path.
    let profile_obj: serde_json::Value = profile_wrapper
        .get("profile_data")
        .cloned()
        .unwrap_or(profile_wrapper);

    // ---- 2. Save profile to `company_profiles` (DRAFT upsert). ----
    // SQL mapping copied verbatim from apps/users/src/handlers/profile.rs:236-335 (COMPANY branch).
    let name = profile_obj
        .get("company_name")
        .and_then(|v| v.as_str())
        .unwrap_or_default()
        .to_string();
    let email = profile_obj
        .get("company_email")
        .and_then(|v| v.as_str())
        .unwrap_or_default()
        .to_string();
    let phone = profile_obj
        .get("company_phone")
        .and_then(|v| v.as_str())
        .unwrap_or_default()
        .to_string();
    let website = profile_obj
        .get("website")
        .and_then(|v| v.as_str())
        .unwrap_or_default()
        .to_string();
    let city = profile_obj
        .get("location")
        .and_then(|v| v.as_str())
        .unwrap_or_default()
        .to_string();
    let state_val = profile_obj
        .get("state")
        .and_then(|v| v.as_str())
        .unwrap_or_default()
        .to_string();
    let postal = profile_obj
        .get("pin_code")
        .and_then(|v| v.as_str())
        .unwrap_or_default()
        .to_string();
    let address = profile_obj
        .get("address")
        .and_then(|v| v.as_str())
        .unwrap_or_default()
        .to_string();
    let gst = profile_obj
        .get("gst_number")
        .and_then(|v| v.as_str())
        .unwrap_or_default()
        .to_string();

    if let Err(e) = sqlx::query(
        r#"
        INSERT INTO company_profiles (
            user_id, company_name, contact_email, contact_phone, website_url,
            address_line1, city, state, postal_code, gst_number, status, updated_at
        )
        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'DRAFT', NOW())
        ON CONFLICT (user_id) DO UPDATE SET
            company_name = EXCLUDED.company_name,
            contact_email = EXCLUDED.contact_email,
            contact_phone = EXCLUDED.contact_phone,
            website_url = EXCLUDED.website_url,
            address_line1 = EXCLUDED.address_line1,
            city = EXCLUDED.city,
            state = EXCLUDED.state,
            postal_code = EXCLUDED.postal_code,
            gst_number = EXCLUDED.gst_number,
            updated_at = NOW()
        "#,
    )
    .bind(auth.user_id)
    .bind(&name)
    .bind(&email)
    .bind(&phone)
    .bind(&website)
    .bind(&address)
    .bind(&city)
    .bind(&state_val)
    .bind(&postal)
    .bind(&gst)
    .execute(&state.pool)
    .await
    {
        tracing::error!("submit_with_documents: profile upsert failed for user {}: {}", auth.user_id, e);
        return (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(serde_json::json!({ "error": format!("Failed to save profile: {}", e) })),
        )
            .into_response();
    }

    // Look up the company_profiles.id for the company_documents FK.
    let company_id: Uuid = match sqlx::query_scalar::<_, Uuid>(
        "SELECT id FROM company_profiles WHERE user_id = $1",
    )
    .bind(auth.user_id)
    .fetch_one(&state.pool)
    .await
    {
        Ok(id) => id,
        Err(e) => {
            tracing::error!("submit_with_documents: cannot resolve company_id: {}", e);
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({ "error": "Profile saved but company record lookup failed" })),
            )
                .into_response();
        }
    };

    // ---- 2b. Insert company_documents rows (deferred until we have company_id). ----
    // Same SQL as the existing upload_documents handler (apps/companies/src/handlers/mod.rs:612-624).
    // Errors are logged but not propagated — matches existing behaviour.
    for (url, data_len, content_type) in &uploaded_files {
        if let Err(e) = sqlx::query(
            r#"
            INSERT INTO company_documents (company_id, document_name, document_url, file_size, mime_type)
            VALUES ($1, $2, $3, $4, $5)
            "#,
        )
        .bind(company_id)
        .bind(format!("document_{}", Uuid::new_v4()))
        .bind(url)
        .bind(*data_len)
        .bind(content_type)
        .execute(&state.pool)
        .await
        {
            tracing::error!(
                "Failed to save company_documents row for company {}: {}",
                company_id,
                e
            );
        }
    }

    // ---- 4. Create the verification record. ----
    // Guard: reject if an active verification already exists (mirrors users/src/handlers/profile.rs).
    let existing: Result<Option<Uuid>, sqlx::Error> = sqlx::query_scalar(
        r#"
        SELECT id FROM verifications
        WHERE user_id = $1 AND role_key = 'COMPANY'
          AND status IN ('PENDING', 'UNDER_REVIEW', 'DOCUMENTS_REQUESTED', 'REVISION_REQUESTED')
        LIMIT 1
        "#,
    )
    .bind(auth.user_id)
    .fetch_optional(&state.pool)
    .await;

    if existing.unwrap_or(None).is_some() {
        return (
            StatusCode::CONFLICT,
            Json(serde_json::json!({
                "error": "A verification is already in progress. Please wait for it to be reviewed."
            })),
        )
            .into_response();
    }

    // Signature: VerificationRepository::create(pool, user_id, role_key, case_type, priority, payload, documents)
    // (see crates/db/src/models/verification.rs:38). The DB column is `case_type`; the user service
    // populates it with "PROFILE_VERIFICATION" — we do the same.
    let verification = match VerificationRepository::create(
        &state.pool,
        auth.user_id,
        "COMPANY",
        "PROFILE_VERIFICATION",
        "MEDIUM",
        profile_obj.clone(),
        serde_json::Value::Array(documents_meta.clone()),
    )
    .await
    {
        Ok(v) => v,
        Err(e) => {
            tracing::error!(
                "submit_with_documents: failed to create verification for user {}: {}",
                auth.user_id,
                e
            );
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(serde_json::json!({ "error": format!("Failed to create verification record: {}", e) })),
            )
                .into_response();
        }
    };

    // ---- 5. Mark the company_profiles row as PENDING. ----
    // Mirrors `set_profile_status` (COMPANY branch) in apps/users/src/handlers/profile.rs:606-614.
    if let Err(e) = sqlx::query(
        "UPDATE company_profiles SET status = 'PENDING', updated_at = NOW() WHERE user_id = $1",
    )
    .bind(auth.user_id)
    .execute(&state.pool)
    .await
    {
        tracing::error!(
            "submit_with_documents: failed to update company_profiles.status for user {}: {}",
            auth.user_id,
            e
        );
        // Don't fail the whole request — the verification record is the source of truth for the admin queue.
    }

    // Invalidate cached profile so the next read reflects the new status.
    let cache_key = format!("profile:company:{}", auth.user_id);
    let mut redis = state.redis.clone();
    let _ = redis.del::<_, ()>(&cache_key).await;

    let mut response = serde_json::json!({
        "verification_id": verification.id,
        "status": "PENDING",
        "documents_uploaded": documents_uploaded,
        "documents_failed": documents_failed,
        "storage_backend": storage_backend,
    });
    if !failure_details.is_empty() {
        response["document_errors"] = serde_json::Value::Array(failure_details);
    }

    (StatusCode::OK, Json(response)).into_response()
}
