use std::str::FromStr;
use std::sync::Arc;

use axum::extract::State;
use axum::response::Redirect;
use doctate_common::BulkAction;
use doctate_common::timestamp::now_rfc3339;
use serde::Deserialize;
use tracing::{info, warn};

use crate::analyze::backend::any_backend_available;
use crate::analyze::{ANALYSIS_INPUT_FILE, AnalyzeJob, AnalyzeSender, DOCUMENT_FILE};
use crate::auth::AuthenticatedWebUser;
use crate::config::Config;
use crate::csrf::{CsrfForm, HasCsrfToken};
use crate::error::AppError;
use crate::events::{self, CaseEventKind, EventSender};
use crate::oneliner_locks::OnelinerLocks;
use crate::paths::{CloseMarker, write_close_marker};
use crate::routes::case_actions::{
    build_analysis_input, reset_case_artefacts, write_input_create_new,
};
use crate::routes::user_web::locate_case;

#[derive(Debug, Deserialize)]
pub struct BulkForm {
    action: String,
    /// Repeated `case_id=<uuid>` form fields. Empty for an empty selection
    /// (handled as a no-op).
    #[serde(default, rename = "case_id")]
    case_ids: Vec<String>,
    #[serde(default)]
    csrf_token: String,
}

impl HasCsrfToken for BulkForm {
    fn csrf_token(&self) -> &str {
        &self.csrf_token
    }
}

/// POST /web/cases/bulk — apply `action` (analyze | close | reset) to
/// every selected case. Errors per case are logged but do not abort the
/// batch: a single bad/missing case must not block the rest.
pub async fn handle_bulk_action(
    user: AuthenticatedWebUser,
    State(config): State<Arc<Config>>,
    State(analyze_tx): State<AnalyzeSender>,
    State(events_tx): State<EventSender>,
    State(locks): State<OnelinerLocks>,
    CsrfForm(form): CsrfForm<BulkForm>,
) -> Result<Redirect, AppError> {
    // Bulk actions are admin-only across the board: the UI hides the
    // selection checkboxes and action bar from non-admins, and this
    // server-side gate defends against hand-crafted POSTs.
    if !user.is_admin() {
        warn!(slug = %user.slug, action = %form.action, "bulk: not admin");
        return Err(AppError::Forbidden("Nur für Admins".into()));
    }

    let user_root = config.data_path.join(&user.slug);

    // Parse the wire token into the shared enum so the match below is
    // exhaustive at compile time. Keep the exact "Unbekannte Aktion"
    // error shape the handler returned before the enum existed.
    let action = BulkAction::from_str(&form.action).map_err(|err| {
        warn!(slug = %user.slug, action = %err.0, "bulk: unknown action");
        AppError::BadRequest("Unbekannte Aktion".into())
    })?;

    match action {
        BulkAction::Analyze => {
            bulk_analyze(
                &analyze_tx,
                &events_tx,
                &user.slug,
                &user_root,
                &form.case_ids,
            )
            .await
        }
        BulkAction::Close => bulk_close(&events_tx, &user.slug, &user_root, &form.case_ids).await,
        BulkAction::Reset => {
            bulk_reset(&events_tx, &locks, &user.slug, &user_root, &form.case_ids).await
        }
    }

    Ok(Redirect::to("/web/cases"))
}

async fn bulk_analyze(
    analyze_tx: &AnalyzeSender,
    events_tx: &EventSender,
    slug: &str,
    user_root: &std::path::Path,
    case_ids: &[String],
) {
    if !any_backend_available() {
        warn!(slug = %slug, "bulk-analyze: no LLM backend available, skipping all");
        return;
    }
    let mut ok = 0usize;
    let mut skipped = 0usize;
    for case_id in case_ids {
        if uuid::Uuid::parse_str(case_id).is_err() {
            warn!(slug = %slug, case_id = %case_id, "bulk-analyze: invalid case_id");
            skipped += 1;
            continue;
        }
        let Some(case_dir) = locate_case(user_root, case_id).await else {
            warn!(slug = %slug, case_id = %case_id, "bulk-analyze: case not found");
            skipped += 1;
            continue;
        };
        // Same order as the single-case handler: drop old document first so
        // the UI doesn't keep showing "ausgewertet" during re-analysis.
        let document_path = case_dir.join(DOCUMENT_FILE);
        match tokio::fs::remove_file(&document_path).await {
            Ok(_) => {}
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
            Err(e) => {
                warn!(slug = %slug, case_id = %case_id, error = %e, "bulk-analyze: remove old document failed");
                skipped += 1;
                continue;
            }
        }
        let input = match build_analysis_input(&case_dir, "").await {
            Ok(i) => i,
            Err(_) => {
                warn!(slug = %slug, case_id = %case_id, "bulk-analyze: build_analysis_input failed");
                skipped += 1;
                continue;
            }
        };
        let input_path = case_dir.join(ANALYSIS_INPUT_FILE);
        if write_input_create_new(&input_path, &input).await.is_err() {
            warn!(slug = %slug, case_id = %case_id, "bulk-analyze: write input failed");
            skipped += 1;
            continue;
        }
        if analyze_tx
            .send(AnalyzeJob {
                case_dir: case_dir.clone(),
            })
            .is_err()
        {
            warn!(slug = %slug, case_id = %case_id, "bulk-analyze: send failed (worker gone)");
        }
        events::emit(events_tx, slug, case_id, CaseEventKind::AnalysisQueued);
        ok += 1;
    }
    info!(slug = %slug, ok, skipped, "bulk-analyze completed");
}

async fn bulk_close(
    events_tx: &EventSender,
    slug: &str,
    user_root: &std::path::Path,
    case_ids: &[String],
) {
    let closed_at = now_rfc3339();
    let mut ok = 0usize;
    let mut skipped = 0usize;
    for case_id in case_ids {
        if uuid::Uuid::parse_str(case_id).is_err() {
            warn!(slug = %slug, case_id = %case_id, "bulk-close: invalid case_id");
            skipped += 1;
            continue;
        }
        let Some(case_dir) = locate_case(user_root, case_id).await else {
            warn!(slug = %slug, case_id = %case_id, "bulk-close: case not found");
            skipped += 1;
            continue;
        };
        let marker = CloseMarker {
            closed_at: closed_at.clone(),
        };
        if let Err(e) = write_close_marker(&case_dir, &marker).await {
            warn!(slug = %slug, case_id = %case_id, error = %e, "bulk-close: write marker failed");
            skipped += 1;
            continue;
        }
        events::emit(events_tx, slug, case_id, CaseEventKind::CaseClosed);
        ok += 1;
    }
    info!(slug = %slug, closed_at = %closed_at, ok, skipped, "bulk-close completed");
}

async fn bulk_reset(
    events_tx: &EventSender,
    locks: &OnelinerLocks,
    slug: &str,
    user_root: &std::path::Path,
    case_ids: &[String],
) {
    let mut ok = 0usize;
    let mut skipped = 0usize;
    for case_id in case_ids {
        if uuid::Uuid::parse_str(case_id).is_err() {
            warn!(slug = %slug, case_id = %case_id, "bulk-reset: invalid case_id");
            skipped += 1;
            continue;
        }
        let Some(case_dir) = locate_case(user_root, case_id).await else {
            warn!(slug = %slug, case_id = %case_id, "bulk-reset: case not found");
            skipped += 1;
            continue;
        };
        if let Err(e) = reset_case_artefacts(&case_dir, locks).await {
            warn!(slug = %slug, case_id = %case_id, error = %e, "bulk-reset: failed");
            skipped += 1;
            continue;
        }
        events::emit(events_tx, slug, case_id, CaseEventKind::CaseReset);
        ok += 1;
    }
    info!(slug = %slug, ok, skipped, "bulk-reset completed");
}
