//! Task attachment handlers for file uploads and downloads

use crate::error::{ApiError, ApiResult};
use crate::models::AttachmentTuple;
use crate::state::AppState;
use axum::{
    extract::{Path, State},
    http::StatusCode,
    Json,
};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use syntaxis_core::persistence::Database;
use uuid::Uuid;

/// Upload attachment request
#[derive(Debug, Deserialize)]
pub struct UploadAttachmentRequest {
    /// Filename
    pub filename: String,
    /// File size in bytes
    pub file_size: i64,
    /// MIME type
    pub mime_type: String,
    /// Storage path (server-side file location)
    pub storage_path: String,
    /// Uploaded by user ID
    pub uploaded_by: String,
}

/// Attachment DTO
#[derive(Debug, Serialize)]
pub struct AttachmentDto {
    /// Attachment ID
    pub id: String,
    /// Task ID
    pub task_id: String,
    /// Filename
    pub filename: String,
    /// File size in bytes
    pub file_size: i64,
    /// MIME type
    pub mime_type: String,
    /// Storage path
    pub storage_path: String,
    /// Uploaded by user ID
    pub uploaded_by: String,
    /// Uploaded timestamp
    pub uploaded_at: String,
}

/// Get attachments for a task
///
/// Retrieves all attachments associated with a specific task.
pub async fn get_attachments(
    State(state): State<AppState>,
    Path((project_id, task_id)): Path<(String, String)>,
) -> ApiResult<Json<Vec<AttachmentDto>>> {
    // Verify project exists
    let _project = state
        .db
        .get_project(&project_id)
        .await?
        .ok_or_else(|| ApiError::NotFound(format!("Project {} not found", project_id)))?;

    // Verify task exists
    let _task = sqlx::query("SELECT id FROM checklist_items WHERE id = ? AND project_id = ?")
        .bind(&task_id)
        .bind(&project_id)
        .fetch_optional(state.db.pool())
        .await
        .map_err(|e| ApiError::Database(e.to_string()))?
        .ok_or_else(|| ApiError::NotFound(format!("Task {} not found", task_id)))?;

    // Fetch attachments
    let attachments: Vec<AttachmentTuple> = sqlx::query_as(
        r#"
        SELECT id, task_id, filename, file_size, mime_type, storage_path, uploaded_by, uploaded_at
        FROM task_attachments
        WHERE task_id = ?
        ORDER BY uploaded_at DESC
        "#,
    )
    .bind(&task_id)
    .fetch_all(state.db.pool())
    .await
    .map_err(|e| ApiError::Database(e.to_string()))?;

    let dtos = attachments
        .into_iter()
        .map(
            |(
                id,
                task_id,
                filename,
                file_size,
                mime_type,
                storage_path,
                uploaded_by,
                uploaded_at,
            )| AttachmentDto {
                id,
                task_id,
                filename,
                file_size,
                mime_type,
                storage_path,
                uploaded_by,
                uploaded_at,
            },
        )
        .collect();

    Ok(Json(dtos))
}

/// Upload an attachment to a task
///
/// Registers a file attachment for a task. The actual file upload is handled separately.
pub async fn upload_attachment(
    State(state): State<AppState>,
    Path((project_id, task_id)): Path<(String, String)>,
    Json(req): Json<UploadAttachmentRequest>,
) -> ApiResult<(StatusCode, Json<AttachmentDto>)> {
    // Verify project exists
    let _project = state
        .db
        .get_project(&project_id)
        .await?
        .ok_or_else(|| ApiError::NotFound(format!("Project {} not found", project_id)))?;

    // Verify task exists
    let _task = sqlx::query("SELECT id FROM checklist_items WHERE id = ? AND project_id = ?")
        .bind(&task_id)
        .bind(&project_id)
        .fetch_optional(state.db.pool())
        .await
        .map_err(|e| ApiError::Database(e.to_string()))?
        .ok_or_else(|| ApiError::NotFound(format!("Task {} not found", task_id)))?;

    // Verify user exists
    let _user = sqlx::query("SELECT id FROM users WHERE id = ?")
        .bind(&req.uploaded_by)
        .fetch_optional(state.db.pool())
        .await
        .map_err(|e| ApiError::Database(e.to_string()))?
        .ok_or_else(|| ApiError::NotFound(format!("User {} not found", req.uploaded_by)))?;

    // Create attachment record
    let attachment_id = Uuid::new_v4().to_string();
    let now = Utc::now().to_rfc3339();

    sqlx::query(
        r#"
        INSERT INTO task_attachments (id, task_id, filename, file_size, mime_type, storage_path, uploaded_by, uploaded_at)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        "#
    )
    .bind(&attachment_id)
    .bind(&task_id)
    .bind(&req.filename)
    .bind(req.file_size)
    .bind(&req.mime_type)
    .bind(&req.storage_path)
    .bind(&req.uploaded_by)
    .bind(&now)
    .execute(state.db.pool())
    .await
    .map_err(|e| ApiError::Database(e.to_string()))?;

    // Broadcast attachment uploaded event
    state
        .events
        .broadcast(crate::websocket::ProjectEvent::AttachmentUploaded {
            project_id,
            task_id: task_id.clone(),
            attachment_id: attachment_id.clone(),
            filename: req.filename.clone(),
            timestamp: now.clone(),
        });

    Ok((
        StatusCode::CREATED,
        Json(AttachmentDto {
            id: attachment_id,
            task_id,
            filename: req.filename,
            file_size: req.file_size,
            mime_type: req.mime_type,
            storage_path: req.storage_path,
            uploaded_by: req.uploaded_by,
            uploaded_at: now,
        }),
    ))
}

/// Delete an attachment
///
/// Removes an attachment record. The actual file deletion is handled separately.
pub async fn delete_attachment(
    State(state): State<AppState>,
    Path((project_id, task_id, attachment_id)): Path<(String, String, String)>,
) -> ApiResult<StatusCode> {
    // Verify project exists
    let _project = state
        .db
        .get_project(&project_id)
        .await?
        .ok_or_else(|| ApiError::NotFound(format!("Project {} not found", project_id)))?;

    // Verify task exists
    let _task = sqlx::query("SELECT id FROM checklist_items WHERE id = ? AND project_id = ?")
        .bind(&task_id)
        .bind(&project_id)
        .fetch_optional(state.db.pool())
        .await
        .map_err(|e| ApiError::Database(e.to_string()))?
        .ok_or_else(|| ApiError::NotFound(format!("Task {} not found", task_id)))?;

    // Delete attachment
    let result = sqlx::query("DELETE FROM task_attachments WHERE id = ? AND task_id = ?")
        .bind(&attachment_id)
        .bind(&task_id)
        .execute(state.db.pool())
        .await
        .map_err(|e| ApiError::Database(e.to_string()))?;

    if result.rows_affected() == 0 {
        return Err(ApiError::NotFound(format!(
            "Attachment {} not found",
            attachment_id
        )));
    }

    Ok(StatusCode::NO_CONTENT)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_upload_attachment_request() {
        let json = r#"{
            "filename": "document.pdf",
            "file_size": 1024000,
            "mime_type": "application/pdf",
            "storage_path": "/uploads/document.pdf",
            "uploaded_by": "user-123"
        }"#;
        let req: UploadAttachmentRequest = serde_json::from_str(json).unwrap();

        assert_eq!(req.filename, "document.pdf");
        assert_eq!(req.file_size, 1024000);
        assert_eq!(req.mime_type, "application/pdf");
    }

    #[test]
    fn test_attachment_dto_serialization() {
        let dto = AttachmentDto {
            id: "att-1".to_string(),
            task_id: "task-1".to_string(),
            filename: "test.pdf".to_string(),
            file_size: 1024,
            mime_type: "application/pdf".to_string(),
            storage_path: "/uploads/test.pdf".to_string(),
            uploaded_by: "user-1".to_string(),
            uploaded_at: "2025-01-01T00:00:00Z".to_string(),
        };

        let json = serde_json::to_string(&dto).unwrap();
        assert!(json.contains("test.pdf"));
        assert!(json.contains("1024"));
    }
}
