//! Doctor's manual oneliner override — two entry points sharing one core.
//!
//! - `PUT /api/cases/{case_id}/oneliner` (X-API-Key) for native clients.
//! - `POST /web/cases/{case_id}/oneliner` (session + CSRF) for the
//!   browser-based web UI.
//!
//! Both routes funnel into [`apply_manual_override`], which validates
//! the text, takes [`OnelinerLocks::lock_for`] for the duration of the
//! disk write, persists [`OnelinerState::Manual`], and emits an
//! `OnelinerUpdated` event so SSE listeners can reflect the change.
//!
//! `Manual` is treated as terminal by the transcribe worker's
//! `update_oneliner`: the manual text is sticky across new recordings
//! until another manual edit replaces it. See plan
//! `plane-die-erweiterung-oneliner-override-pure-goblet.md` for the
//! full concurrency rationale.

use std::path::Path;
use std::sync::Arc;

use axum::Json;
use axum::extract::State;
use doctate_common::oneliners::{OnelinerOverrideRequest, OnelinerState};
use serde::Deserialize;
use tracing::info;

use crate::auth::{AuthenticatedUser, AuthenticatedWebUser};
use crate::case_id::CaseIdPath;
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;
use crate::routes::user_web::locate_case_or_404;

/// Hard cap on accepted text length. A oneliner is a single sentence
/// (typical 30–80 chars); 500 leaves comfortable headroom while
/// preventing accidental dumps from a runaway client.
const MAX_ONELINER_TEXT_LEN: usize = 500;

/// Persist a doctor's manual override for a case. Shared by the
/// X-API-Key API handler and the session-authenticated web handler.
///
/// Validates and trims `raw_text`, takes the per-case lock for the
/// disk-write window (so a concurrent auto-regen cannot overwrite the
/// manual edit), writes [`OnelinerState::Manual`], logs, and emits
/// `OnelinerUpdated`. Returns the persisted state so the caller can
/// echo it back to the client.
///
/// Pre-condition: `case_dir` must already exist. Both call sites check
/// for this — the API handler with `try_exists`, the web handler via
/// `locate_case_or_404` — so the helper itself stays focused on the
/// override semantics.
pub(crate) async fn apply_manual_override(
    user_slug: &str,
    case_dir: &Path,
    raw_text: &str,
    locks: &OnelinerLocks,
    events_tx: &EventSender,
) -> Result<OnelinerState, AppError> {
    let trimmed = raw_text.trim();
    if trimmed.is_empty() {
        return Err(AppError::BadRequest(
            "oneliner text must not be empty".into(),
        ));
    }
    if trimmed.chars().count() > MAX_ONELINER_TEXT_LEN {
        return Err(AppError::BadRequest(format!(
            "oneliner text exceeds {MAX_ONELINER_TEXT_LEN} characters"
        )));
    }

    let state = OnelinerState::Manual {
        text: trimmed.to_owned(),
        set_at: doctate_common::now_rfc3339(),
    };

    // RAII guard — released on function exit. Same lock the worker takes
    // for its re-check-then-write window, so a concurrent auto-regen
    // cannot overwrite this manual edit.
    let _guard = locks.lock_for(case_dir).await;
    paths::write_oneliner_state(case_dir, &state).await?;

    let case_id = case_dir
        .file_name()
        .and_then(|s| s.to_str())
        .unwrap_or("?")
        .to_owned();

    info!(
        user = %user_slug,
        case_id = %case_id,
        len = trimmed.chars().count(),
        "oneliner manual override set"
    );

    events::emit(
        events_tx,
        user_slug.to_owned(),
        case_id,
        CaseEventKind::OnelinerUpdated,
    );

    Ok(state)
}

/// `PUT /api/cases/{case_id}/oneliner` — X-API-Key authenticated.
pub async fn handle_put_oneliner(
    user: AuthenticatedUser,
    State(locks): State<OnelinerLocks>,
    State(events_tx): State<EventSender>,
    CaseIdPath(case_id): CaseIdPath,
    Json(req): Json<OnelinerOverrideRequest>,
) -> Result<Json<OnelinerState>, AppError> {
    let case_id_str = case_id.to_string();
    let case_dir = user.data_dir.join(&case_id_str);
    if !tokio::fs::try_exists(&case_dir).await.unwrap_or(false) {
        return Err(AppError::NotFound(format!("case {case_id_str} not found")));
    }

    let state = apply_manual_override(&user.slug, &case_dir, &req.text, &locks, &events_tx).await?;
    Ok(Json(state))
}

/// Form body for the browser-based web edit endpoint. CSRF token is
/// required; `text` carries the new oneliner content.
#[derive(Deserialize)]
pub struct OnelinerWebForm {
    #[serde(default)]
    pub text: String,
    #[serde(default)]
    pub csrf_token: String,
}

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

/// `POST /web/cases/{case_id}/oneliner` — session + CSRF authenticated.
///
/// Mirrors [`handle_put_oneliner`] but uses the web auth stack so the
/// browser never has to expose an X-API-Key. Closed cases are rejected
/// at `locate_case_or_404` (consistent with analyze/close/reopen).
pub async fn handle_web_put_oneliner(
    user: AuthenticatedWebUser,
    State(config): State<Arc<Config>>,
    State(locks): State<OnelinerLocks>,
    State(events_tx): State<EventSender>,
    CaseIdPath(case_id): CaseIdPath,
    CsrfForm(form): CsrfForm<OnelinerWebForm>,
) -> Result<Json<OnelinerState>, AppError> {
    let user_root = config.data_path.join(&user.slug);
    let case_dir =
        locate_case_or_404(&user_root, &case_id, &user.slug, "oneliner override").await?;

    let state =
        apply_manual_override(&user.slug, &case_dir, &form.text, &locks, &events_tx).await?;
    Ok(Json(state))
}
