//! On-disk file IO for `/api/record`. The wire flow lives in
//! [src/hbbs_http/record_upload.rs](file:///Users/sn0/Desktop/rustdesk/src/hbbs_http/record_upload.rs)
//! on the client side: the controller emits `?type=new` once, then a series
//! of `?type=part&offset=N&length=L` chunks, and finally a `?type=tail`
//! header rewrite at offset 0. We mirror that as a tiny state machine.

use crate::api::error::ApiError;
use crate::api::state::AppState;
use std::path::{Component, Path, PathBuf};
use tokio::fs::{File, OpenOptions};
use tokio::io::{AsyncSeekExt, AsyncWriteExt, SeekFrom};

const TAIL_MAX: usize = 1024;

/// Reject any filename that contains a path separator or `..` component.
/// The client only ever sends a basename per
/// `record_upload.rs:118-122`, so anything else is suspicious.
pub fn sanitized_path(root: &Path, file: &str) -> Result<PathBuf, ApiError> {
    if file.is_empty() {
        return Err(ApiError::BadRequest("file required".into()));
    }
    let p = Path::new(file);
    let mut comps = p.components();
    let only = comps.next();
    let extra = comps.next();
    match (only, extra) {
        (Some(Component::Normal(name)), None) if !name.is_empty() => Ok(root.join(name)),
        _ => Err(ApiError::BadRequest("invalid file name".into())),
    }
}

pub async fn handle_new(
    state: &AppState,
    file: &str,
    peer_id: &str,
) -> Result<(), ApiError> {
    let path = sanitized_path(&state.cfg.recording_dir, file)?;
    if let Some(dir) = path.parent() {
        tokio::fs::create_dir_all(dir)
            .await
            .map_err(|e| ApiError::Internal(format!("mkdir {}: {}", dir.display(), e)))?;
    }
    // Truncate (or create) the file.
    OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .open(&path)
        .await
        .map_err(|e| ApiError::Internal(format!("create {}: {}", path.display(), e)))?;
    state
        .db
        .recording_new(peer_id, file)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    Ok(())
}

pub async fn handle_part(
    state: &AppState,
    file: &str,
    offset: u64,
    length: usize,
    body: &[u8],
) -> Result<(), ApiError> {
    if body.len() != length {
        hbb_common::log::warn!(
            "record part length mismatch: declared={} actual={}",
            length,
            body.len()
        );
    }
    let path = sanitized_path(&state.cfg.recording_dir, file)?;
    let max = state.cfg.recording_max_size_bytes;
    if max > 0 && offset.saturating_add(body.len() as u64) > max {
        return Err(ApiError::Forbidden("recording size cap exceeded".into()));
    }
    let mut f: File = OpenOptions::new()
        .write(true)
        .create(true)
        .open(&path)
        .await
        .map_err(|e| ApiError::Internal(format!("open {}: {}", path.display(), e)))?;
    f.seek(SeekFrom::Start(offset))
        .await
        .map_err(|e| ApiError::Internal(format!("seek: {}", e)))?;
    f.write_all(body)
        .await
        .map_err(|e| ApiError::Internal(format!("write: {}", e)))?;
    f.flush().await.ok();
    let new_size = offset + body.len() as u64;
    state
        .db
        .recording_set_state(file, "recording", Some(new_size as i64), false)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    Ok(())
}

pub async fn handle_tail(
    state: &AppState,
    file: &str,
    offset: u64,
    length: usize,
    body: &[u8],
) -> Result<(), ApiError> {
    if offset != 0 {
        return Err(ApiError::BadRequest("tail must be at offset 0".into()));
    }
    if length > TAIL_MAX || body.len() > TAIL_MAX {
        return Err(ApiError::BadRequest("tail exceeds 1024 bytes".into()));
    }
    let path = sanitized_path(&state.cfg.recording_dir, file)?;
    let mut f = OpenOptions::new()
        .write(true)
        .open(&path)
        .await
        .map_err(|e| ApiError::Internal(format!("open {}: {}", path.display(), e)))?;
    f.seek(SeekFrom::Start(0))
        .await
        .map_err(|e| ApiError::Internal(format!("seek: {}", e)))?;
    f.write_all(body)
        .await
        .map_err(|e| ApiError::Internal(format!("write tail: {}", e)))?;
    f.flush().await.ok();
    state
        .db
        .recording_set_state(file, "finished", None, true)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    Ok(())
}

pub async fn handle_remove(state: &AppState, file: &str) -> Result<(), ApiError> {
    let path = sanitized_path(&state.cfg.recording_dir, file)?;
    if let Err(e) = tokio::fs::remove_file(&path).await {
        if e.kind() != std::io::ErrorKind::NotFound {
            hbb_common::log::warn!("remove {}: {}", path.display(), e);
        }
    }
    state
        .db
        .recording_delete(file)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    Ok(())
}
