use actix_multipart::Multipart;
use actix_web::{HttpResponse, Result, web};
use actix_web::http::header::{CONTENT_DISPOSITION, CONTENT_TYPE};
use chrono::Utc;
use futures_util::StreamExt;
use models::rooms::room_attachment;
use sea_orm::{ActiveModelTrait, EntityTrait, Set};
use service::AppService;
use session::Session;
use uuid::Uuid;

#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
pub struct UploadResponse {
    pub id: String,
    pub url: String,
    pub file_name: String,
    pub file_size: i64,
    pub content_type: String,
}

fn sanitize_key(key: &str) -> String {
    key.replace(['/', '\\', ':'], "_")
}

fn extract_filename(disposition: &actix_web::http::header::HeaderValue) -> Option<String> {
    let s = disposition.to_str().ok()?;
    // Simple parsing: extract filename from Content-Disposition header
    // e.g., "form-data; filename="test.png""
    for part in s.split(';') {
        let part = part.trim();
        if part.starts_with("filename=") {
            let value = &part[9..].trim_matches('"');
            if !value.is_empty() {
                return Some(value.to_string());
            }
        }
    }
    None
}

pub async fn upload(
    service: web::Data<AppService>,
    session: Session,
    path: web::Path<Uuid>,
    mut payload: Multipart,
) -> Result<HttpResponse, crate::error::ApiError> {
    let user_id = session
        .user()
        .ok_or_else(|| crate::error::ApiError(service::error::AppError::Unauthorized))?;

    let storage = service
        .storage
        .as_ref()
        .ok_or_else(|| {
            crate::error::ApiError(service::error::AppError::BadRequest(
                "Storage not configured".to_string(),
            ))
        })?;

    let room_id = path.into_inner();
    service
        .room
        .require_room_member(room_id, user_id)
        .await
        .map_err(crate::error::ApiError::from)?;

    let max_size = service.config.storage_max_file_size();

    let mut file_data: Vec<u8> = Vec::new();
    let mut file_name = String::new();
    let mut content_type = "application/octet-stream".to_string();

    while let Some(item) = payload.next().await {
        let mut field = item.map_err(|e| {
            crate::error::ApiError(service::error::AppError::BadRequest(e.to_string()))
        })?;

        if let Some(disposition) = field.headers().get(&CONTENT_DISPOSITION) {
            if let Some(name) = extract_filename(disposition) {
                file_name = name;
            }
        }
        if let Some(ct) = field.headers().get(&CONTENT_TYPE) {
            if let Ok(ct_str) = ct.to_str() {
                if !ct_str.is_empty() && ct_str != "application/octet-stream" {
                    content_type = ct_str.to_string();
                }
            }
        }

        while let Some(chunk) = field.next().await {
            let data = chunk.map_err(|e| {
                crate::error::ApiError(service::error::AppError::BadRequest(e.to_string()))
            })?;
            if file_data.len() + data.len() > max_size {
                return Err(crate::error::ApiError(
                    service::error::AppError::BadRequest(format!(
                        "File exceeds maximum size of {} bytes",
                        max_size
                    )),
                ));
            }
            file_data.extend_from_slice(&data);
        }
    }

    if file_data.is_empty() {
        return Err(crate::error::ApiError(
            service::error::AppError::BadRequest("No file provided".to_string()),
        ));
    }

    if file_name.is_empty() {
        file_name = format!("upload_{}", Uuid::now_v7());
    }

    // Detect content type from file extension if still octet-stream
    if content_type == "application/octet-stream" {
        content_type = mime_guess2::from_path(&file_name)
            .first_or_octet_stream()
            .to_string();
    }

    let unique_name = format!("{}_{}", Uuid::now_v7(), sanitize_key(&file_name));
    let key = format!("rooms/{}/{}", room_id, unique_name);
    let file_size = file_data.len() as i64;

    let _url = storage
        .upload(&key, file_data)
        .await
        .map_err(|e| {
            crate::error::ApiError(service::error::AppError::InternalServerError(
                e.to_string(),
            ))
        })?;

    // Write to room_attachment table (message will be linked when message is created)
    let attachment_id = Uuid::now_v7();
    let attachment: room_attachment::ActiveModel = room_attachment::ActiveModel {
        id: Set(attachment_id),
        room: Set(room_id),
        message: Set(Uuid::nil()),
        uploader: Set(user_id),
        file_name: Set(file_name.clone()),
        file_size: Set(file_size),
        content_type: Set(content_type.clone()),
        s3_key: Set(key),
        created_at: Set(Utc::now()),
    };
    attachment
        .insert(&service.db)
        .await
        .map_err(|e| {
            crate::error::ApiError(service::error::AppError::InternalServerError(
                e.to_string(),
            ))
        })?;

    // Return the structured attachment URL instead of the /files/... path
    // (the /files/... path has no handler on the API server)
    let attachment_url = format!("/api/rooms/{}/attachments/{}", room_id, attachment_id);

    Ok(crate::ApiResponse::ok(UploadResponse {
        id: attachment_id.to_string(),
        url: attachment_url,
        file_name,
        file_size,
        content_type,
    })
    .to_response())
}

#[utoipa::path(
    get,
    path = "/api/rooms/{room_id}/attachments/{attachment_id}",
    params(
        ("room_id" = Uuid, Path, description = "Room ID"),
        ("attachment_id" = Uuid, Path, description = "Attachment ID"),
    ),
    responses(
        (status = 200, description = "Download file"),
        (status = 401, description = "Unauthorized"),
        (status = 403, description = "Not a room member"),
        (status = 404, description = "Not found"),
    ),
    tag = "Room"
)]
pub async fn get_attachment(
    service: web::Data<AppService>,
    session: Session,
    path: web::Path<(Uuid, Uuid)>,
) -> Result<HttpResponse, crate::error::ApiError> {
    let user_id = session
        .user()
        .ok_or_else(|| crate::error::ApiError(service::error::AppError::Unauthorized))?;

    let (room_id, attachment_id) = path.into_inner();

    service
        .room
        .require_room_member(room_id, user_id)
        .await
        .map_err(crate::error::ApiError::from)?;

    let attachment = room_attachment::Entity::find_by_id(attachment_id)
        .one(&service.db)
        .await
        .map_err(|e| crate::error::ApiError(service::error::AppError::InternalServerError(e.to_string())))?
        .ok_or_else(|| crate::error::ApiError(service::error::AppError::NotFound("attachment not found".into())))?;

    // Ensure the attachment belongs to the requested room
    if attachment.room != room_id {
        return Err(crate::error::ApiError(service::error::AppError::NotFound("attachment not found".into())));
    }

    let storage = service
        .storage
        .as_ref()
        .ok_or_else(|| crate::error::ApiError(service::error::AppError::InternalServerError("Storage not configured".to_string())))?;

    let (data, content_type) = storage
        .read(&attachment.s3_key)
        .await
        .map_err(|e| crate::error::ApiError(service::error::AppError::InternalServerError(e.to_string())))?;

    Ok(HttpResponse::Ok()
        .content_type(content_type.clone())
        .insert_header((
            CONTENT_TYPE,
            content_type,
        ))
        .insert_header((
            CONTENT_DISPOSITION,
            format!("inline; filename=\"{}\"", attachment.file_name),
        ))
        .body(data))
}
