use std::path::{Path, PathBuf};

use axum::Json;
use axum::extract::{Multipart, State};
use doctate_common::timestamp::recorded_at_to_filename_stem;
use tokio::sync::mpsc::error::TrySendError;
use tracing::{info, warn};

use crate::auth::AuthenticatedUser;
use crate::error::AppError;
use crate::events::{self, CaseEventKind, EventSender};
use crate::models::{AckResponse, AckStatus};
use crate::transcribe::{TranscribeJob, TranscribeSender};

pub async fn handle_upload(
    user: AuthenticatedUser,
    State(transcribe_tx): State<TranscribeSender>,
    State(events_tx): State<EventSender>,
    mut multipart: Multipart,
) -> Result<Json<AckResponse>, AppError> {
    let mut case_id: Option<String> = None;
    let mut recorded_at: Option<String> = None;
    let mut audio: Option<Vec<u8>> = None;

    // Consume multipart fields in whatever order they arrive.
    while let Some(field) = multipart
        .next_field()
        .await
        .map_err(|e| AppError::BadRequest(format!("Invalid multipart data: {e}")))?
    {
        match field.name() {
            Some("case_id") => {
                case_id = Some(
                    field
                        .text()
                        .await
                        .map_err(|e| AppError::BadRequest(format!("Invalid case_id: {e}")))?,
                );
            }
            Some("recorded_at") => {
                recorded_at = Some(
                    field
                        .text()
                        .await
                        .map_err(|e| AppError::BadRequest(format!("Invalid recorded_at: {e}")))?,
                );
            }
            Some("audio") => {
                audio = Some(
                    field
                        .bytes()
                        .await
                        .map_err(|e| AppError::BadRequest(format!("Invalid audio data: {e}")))?
                        .to_vec(),
                );
            }
            _ => {} // Ignore unknown fields
        }
    }

    let case_id = case_id.ok_or_else(|| AppError::BadRequest("Missing field: case_id".into()))?;
    let recorded_at =
        recorded_at.ok_or_else(|| AppError::BadRequest("Missing field: recorded_at".into()))?;
    let audio = audio.ok_or_else(|| AppError::BadRequest("Missing field: audio".into()))?;

    if audio.is_empty() {
        return Err(AppError::BadRequest("Audio data is empty".into()));
    }

    // Validate case_id as UUID (prevents path traversal — UUIDs only contain hex + hyphens).
    uuid::Uuid::parse_str(&case_id)
        .map_err(|_| AppError::BadRequest(format!("Invalid case_id (not a UUID): {case_id}")))?;

    // Validate `recorded_at` shape strictly. A subsecond drift here would
    // produce a filename the day-grouping parser cannot read — see
    // `validate::validate_recorded_at_strict` doc for the historical bug.
    crate::validate::require_recorded_at_strict(&recorded_at)?;

    // Find or create the case directory.
    let case_dir = resolve_case_dir(&user.data_dir, &case_id).await?;

    // Auto-reopen closed cases: a new upload clears the `.closed`
    // marker so the case reappears in /api/oneliners and the web UI.
    // Hard-purged cases (directory already gone) are handled transparently
    // by `resolve_case_dir` above — nothing to clear. No explicit ETag
    // bump is needed: the subsequent m4a write changes `last_recording_at`,
    // which flows into the `/api/oneliners` fingerprint automatically.
    if crate::paths::is_closed(&case_dir).await {
        tokio::fs::remove_file(case_dir.join(crate::paths::CLOSE_MARKER)).await?;
        info!(
            user = %user.slug,
            case_id = %case_id,
            "Reopened closed case via new upload"
        );
        events::emit(
            &events_tx,
            &user.slug,
            &case_id,
            CaseEventKind::CaseReopened,
        );
    }

    // Build a filesystem-safe filename from the timestamp (colons → hyphens).
    let safe_timestamp = recorded_at_to_filename_stem(&recorded_at);
    let file_path = case_dir.join(format!("{safe_timestamp}.m4a"));

    tokio::fs::write(&file_path, &audio).await?;

    info!(
        user = %user.slug,
        case_id = %case_id,
        bytes = audio.len(),
        "Recording received"
    );

    events::emit(
        &events_tx,
        &user.slug,
        &case_id,
        CaseEventKind::RecordingUploaded,
    );

    // Enqueue transcription. Failure here is non-fatal: on restart the recovery
    // scan will re-enqueue any .m4a without a <stem>.json sibling.
    match transcribe_tx.try_send(TranscribeJob {
        audio_path: file_path.clone(),
        user_slug: user.slug.clone(),
    }) {
        Ok(()) => {}
        Err(TrySendError::Full(_)) => {
            warn!(file = %file_path.display(), "Transcription queue full; will be picked up on restart");
        }
        Err(TrySendError::Closed(_)) => {
            warn!(file = %file_path.display(), "Transcription queue closed (no worker)");
        }
    }

    Ok(Json(AckResponse {
        case_id,
        recorded_at,
        status: AckStatus::Received,
    }))
}

/// Determine where to store the audio file for this case.
/// Creates the directory if it's a new case. `user_root` is the per-user
/// root (`<data_path>/<slug>/`); cases live flat directly underneath.
async fn resolve_case_dir(user_root: &Path, case_id: &str) -> Result<PathBuf, AppError> {
    let dir = user_root.join(case_id);
    if !dir.exists() {
        tokio::fs::create_dir_all(&dir).await?;
        info!(case_id = %case_id, "New case created");
    }
    Ok(dir)
}
