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::auto_trigger::{self, CaseAnalysisState};
use crate::analyze::backend::any_backend_available;
use crate::analyze::{AnalyzeJob, AnalyzeSender, InFlight};
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};
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 /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(in_flight): State<InFlight>,
    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,
                &in_flight,
                &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,
                &in_flight,
                &user.slug,
                &user_root,
                &form.case_ids,
            )
            .await
        }
    }

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

async fn bulk_analyze(
    analyze_tx: &AnalyzeSender,
    in_flight: &InFlight,
    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;
        };
        if !in_flight.try_claim(&case_dir) {
            // A job for this case is already queued or running.
            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");
                in_flight.release(&case_dir);
                skipped += 1;
                continue;
            }
        };
        if analyze_tx
            .send(AnalyzeJob {
                case_dir: case_dir.clone(),
                input,
            })
            .is_err()
        {
            warn!(slug = %slug, case_id = %case_id, "bulk-analyze: send failed (worker gone)");
            in_flight.release(&case_dir);
            skipped += 1;
            continue;
        }
        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,
    in_flight: &InFlight,
    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;
        };
        in_flight.release(&case_dir);
        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");
}

/// CSRF-validated form payload for the user-facing "Aktualisieren"-bar.
/// No selection list — the handler operates on every case in
/// `Required` or `Idle` state for the authenticated user.
#[derive(Debug, Deserialize)]
pub struct AnalyzeRequiredForm {
    #[serde(default)]
    csrf_token: String,
}

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

/// POST /cases/analyze-required — manual override for the idle-trigger
/// wait. Walks every UUID-named, non-closed case under the user root
/// and force-enqueues those in `CaseAnalysisState::Required` or `Idle`.
/// `Failed`, `Queued`, and `Current` cases are skipped. Unlike the
/// admin `/cases/bulk` analyze action, this is not gated to admins —
/// it is the user-visible "Aktualisieren"-bar above the case list.
pub async fn handle_analyze_required(
    user: AuthenticatedWebUser,
    State(config): State<Arc<Config>>,
    State(analyze_tx): State<AnalyzeSender>,
    State(in_flight): State<InFlight>,
    State(events_tx): State<EventSender>,
    State(boot_time): State<crate::BootTime>,
    CsrfForm(_form): CsrfForm<AnalyzeRequiredForm>,
) -> Result<Redirect, AppError> {
    let user_root = config.data_path.join(&user.slug);

    if !any_backend_available() {
        warn!(slug = %user.slug, "analyze-required: no LLM backend available, skipping");
        return Ok(Redirect::to("/cases"));
    }

    let now = std::time::SystemTime::now();
    let idle_threshold = std::time::Duration::from_secs(config.analysis_idle_threshold_secs);

    let mut ok = 0usize;
    let mut skipped = 0usize;

    let Ok(mut entries) = tokio::fs::read_dir(&user_root).await else {
        info!(slug = %user.slug, ok, skipped, "analyze-required: no user dir");
        return Ok(Redirect::to("/cases"));
    };

    while let Ok(Some(entry)) = entries.next_entry().await {
        let Ok(name) = entry.file_name().into_string() else {
            continue;
        };
        if uuid::Uuid::parse_str(&name).is_err() {
            continue;
        }
        let case_dir = entry.path();
        if !case_dir.is_dir() {
            continue;
        }
        if crate::paths::is_closed(&case_dir).await {
            continue;
        }

        // Force-enqueue both Required and Idle: the explicit user click
        // overrides the wait-for-quiet semantics. Failed/Queued/Current
        // are intentionally left alone (Failed → manual per-backend retry
        // path on the case page; Queued → already in flight; Current →
        // nothing to do).
        match auto_trigger::evaluate_state(&case_dir, idle_threshold, now, boot_time.0, &in_flight)
            .await
        {
            CaseAnalysisState::Required { .. } | CaseAnalysisState::Idle { .. } => {}
            _ => {
                skipped += 1;
                continue;
            }
        }

        if !in_flight.try_claim(&case_dir) {
            // A concurrent trigger raced us — they will send the job.
            skipped += 1;
            continue;
        }

        let input = match build_analysis_input(&case_dir, "").await {
            Ok(i) => i,
            Err(_) => {
                warn!(slug = %user.slug, case = %case_dir.display(), "analyze-required: build_analysis_input failed");
                in_flight.release(&case_dir);
                skipped += 1;
                continue;
            }
        };
        if analyze_tx
            .send(AnalyzeJob {
                case_dir: case_dir.clone(),
                input,
            })
            .is_err()
        {
            warn!(slug = %user.slug, case = %case_dir.display(), "analyze-required: send failed (worker gone)");
            in_flight.release(&case_dir);
            skipped += 1;
            continue;
        }
        events::emit(&events_tx, &user.slug, &name, CaseEventKind::AnalysisQueued);
        ok += 1;
    }

    info!(slug = %user.slug, ok, skipped, "analyze-required completed");
    Ok(Redirect::to("/cases"))
}
