use std::{path::Path, sync::Arc};

use axum::{
    Json,
    extract::{Query, State},
};
use serde::Serialize;
use tokio::fs;

use crate::{
    app::AppState,
    error::AppError,
    media::{PathQuery, path},
};

const MEDIA_EXTENSIONS: &[&str] = &["mkv", "mp4", "m4v", "avi", "mov", "webm"];

#[derive(Serialize)]
pub struct Entry {
    name: String,
    path: String,
    kind: &'static str,
}

#[derive(Serialize)]
pub struct Directory {
    path: String,
    parent: Option<String>,
    entries: Vec<Entry>,
}

pub async fn browse(
    State(state): State<Arc<AppState>>,
    Query(query): Query<PathQuery>,
) -> Result<Json<Directory>, AppError> {
    let config = &state.config;
    let directory = path::resolve(config, &query.path)?;
    if !directory.is_dir() {
        return Err(AppError::bad_request("path is not a directory"));
    }

    let mut reader = fs::read_dir(&directory).await.map_err(AppError::internal)?;
    let mut entries = Vec::new();
    while let Some(item) = reader.next_entry().await.map_err(AppError::internal)? {
        let file_type = item.file_type().await.map_err(AppError::internal)?;
        let item_path = item.path();
        let kind = if file_type.is_dir() {
            "directory"
        } else if file_type.is_file() && is_media(&item_path) {
            "media"
        } else {
            continue;
        };
        entries.push(Entry {
            name: item.file_name().to_string_lossy().into_owned(),
            path: path::relative(config, &item_path)?,
            kind,
        });
    }
    entries.sort_by_key(|entry| (entry.kind != "directory", entry.name.to_lowercase()));

    let current = path::relative(config, &directory)?;
    let parent = directory
        .parent()
        .filter(|parent| parent.starts_with(&config.media_root))
        .and_then(|parent| path::relative(config, parent).ok());
    Ok(Json(Directory {
        path: current,
        parent,
        entries,
    }))
}

fn is_media(path: &Path) -> bool {
    path.extension()
        .and_then(|value| value.to_str())
        .is_some_and(|value| MEDIA_EXTENSIONS.contains(&value.to_ascii_lowercase().as_str()))
}
