use async_trait::async_trait;
use hyper::{
    header::{CONTENT_TYPE, HeaderValue},
    Body, Request, Response, StatusCode,
};
use tokio::{fs::File, io::AsyncReadExt};
use std::{
    convert::Infallible,
    path::{Path, PathBuf},
};

use crate::structs::struct_plugin::{Plugin, PluginContext};

pub struct PluginStatic {
    /// If true, requests for "/" or any directory path try to serve "index.<ext>" (in order).
    pub serve_root_as_index: bool,
    /// Candidate file extensions to try when the request has no extension.
    /// Example: vec!["pdf","html","svg"]
    pub candidate_exts: Vec<String>,
}

impl PluginStatic {
    /// Default constructor with no candidate extensions.
    pub fn new(serve_root_as_index: bool, candidate_exts: Vec<String>) -> Self {
        Self {
            serve_root_as_index,
            candidate_exts,
        }
    }

    /// Constructor allowing you to specify a preferred order of extensions to probe when
    /// the route doesn't include one (e.g., "/testpage" -> try "testpage.pdf", then "testpage.html").
    pub fn with_types<T: Into<String>>(
        serve_root_as_index: bool,
        types: impl IntoIterator<Item = T>,
    ) -> Self {
        Self {
            serve_root_as_index,
            candidate_exts: types.into_iter().map(Into::into).collect(),
        }
    }
}

/// Very small, no-deps content-type mapper. Extend as needed.
fn guess_content_type(path: &str) -> &'static str {
    match Path::new(path)
        .extension()
        .and_then(|e| e.to_str())
        .map(|s| s.to_ascii_lowercase())
    {
        Some(ref ext) if ext == "html" || ext == "htm" => "text/html; charset=utf-8",
        Some(ref ext) if ext == "css" => "text/css; charset=utf-8",
        Some(ref ext) if ext == "js" || ext == "mjs" => "text/javascript; charset=utf-8",
        Some(ref ext) if ext == "json" => "application/json; charset=utf-8",
        Some(ref ext) if ext == "txt" => "text/plain; charset=utf-8",
        Some(ref ext) if ext == "xml" => "application/xml; charset=utf-8",
        Some(ref ext) if ext == "svg" => "image/svg+xml",
        Some(ref ext) if ext == "png" => "image/png",
        Some(ref ext) if ext == "jpg" || ext == "jpeg" => "image/jpeg",
        Some(ref ext) if ext == "gif" => "image/gif",
        Some(ref ext) if ext == "webp" => "image/webp",
        Some(ref ext) if ext == "ico" => "image/x-icon",
        Some(ref ext) if ext == "pdf" => "application/pdf",
        Some(ref ext) if ext == "wasm" => "application/wasm",
        Some(ref ext) if ext == "mp3" => "audio/mpeg",
        Some(ref ext) if ext == "wav" => "audio/wav",
        Some(ref ext) if ext == "ogg" => "audio/ogg",
        Some(ref ext) if ext == "mp4" => "video/mp4",
        Some(ref ext) if ext == "otf" => "font/otf",
        Some(ref ext) if ext == "ttf" => "font/ttf",
        Some(ref ext) if ext == "woff" => "font/woff",
        Some(ref ext) if ext == "woff2" => "font/woff2",
        Some(ref ext) if ext == "csv" => "text/csv; charset=utf-8",
        _ => "application/octet-stream",
    }
}

#[async_trait]
impl Plugin for PluginStatic {
    async fn plugin_init(&mut self) {
        println!(
            "{} initialized (serve_root_as_index: {}, candidate_exts: {:?})",
            self.plugin_name(),
            self.serve_root_as_index,
            self.candidate_exts
        );
    }

    fn plugin_name(&self) -> &str {
        "PluginStatic"
    }

    fn plugin_can_handle(&self, _req: &Request<Body>) -> bool {
        true
    }

    async fn plugin_handle(
        &self,
        req: Request<Body>,
        ctx: &PluginContext,
    ) -> Result<Response<Body>, Infallible> {
        let path = req.uri().path();

        // --- Demo broadcast hook --------------------------------------------
        // If the request hits /emit/<id>, broadcast that id to all subscribers.
        // This is just to prove-through the broadcasting from a plugin.
        if let Some(id) = path.strip_prefix("/emit/") {
            ctx.broadcast.send_update_id(id.to_string());
            let mut ok = Response::new(Body::from(format!("broadcasted: {}", id)));
            *ok.status_mut() = StatusCode::OK;
            return Ok(ok);
        }
        // ---------------------------------------------------------------------

        // Basic path hardening: strip leading '/', reject traversal.
        let safe_path = path.trim_start_matches('/');
        if safe_path.contains("../") || safe_path.contains("..\\") {
            let mut resp = Response::new(Body::from("400 Bad Request"));
            *resp.status_mut() = StatusCode::BAD_REQUEST;
            return Ok(resp);
        }

        // Build initial file path under ./static
        let file_path = PathBuf::from("./static").join(safe_path);

        // If the path is empty or ends with '/', we might want an index file.
        let ends_with_slash = path.ends_with('/');
        let has_extension = file_path
            .extension()
            .and_then(|e| e.to_str())
            .map(|s| !s.is_empty())
            .unwrap_or(false);

        // Try to open the best candidate file, returning (path, bytes) if found.
        async fn try_open(path: &Path) -> Option<(String, Vec<u8>)> {
            if path.is_file() {
                if let Ok(mut f) = File::open(path).await {
                    let mut contents = Vec::new();
                    if f.read_to_end(&mut contents).await.is_ok() {
                        return Some((path.to_string_lossy().into_owned(), contents));
                    }
                }
            }
            None
        }

        // 1) If serve_root_as_index applies, try index.<ext> in order for directory paths.
        if self.serve_root_as_index && (safe_path.is_empty() || ends_with_slash) {
            // e.g., "./static/" + "index"
            let base = if safe_path.is_empty() {
                PathBuf::from("./static").join("index")
            } else {
                file_path.join("index")
            };

            // Try configured candidate extensions
            for ext in &self.candidate_exts {
                let candidate = base.with_extension(ext);
                if let Some((p, bytes)) = try_open(&candidate).await {
                    return Ok(ok_with_type(bytes, guess_content_type(&p)));
                }
            }

            // Fallback to plain "index.html"
            let candidate = base.with_extension("html");
            if let Some((p, bytes)) = try_open(&candidate).await {
                return Ok(ok_with_type(bytes, guess_content_type(&p)));
            }
        }

        // 2) If request has an explicit extension, try serving it directly.
        if has_extension {
            if let Some((p, bytes)) = try_open(&file_path).await {
                return Ok(ok_with_type(bytes, guess_content_type(&p)));
            }
        } else {
            // 3) No extension provided: try base.<ext> in configured order
            for ext in &self.candidate_exts {
                let candidate = file_path.with_extension(ext);
                if let Some((p, bytes)) = try_open(&candidate).await {
                    return Ok(ok_with_type(bytes, guess_content_type(&p)));
                }
            }

            // 4) Fallbacks when no candidate ext matched
            // 4a) If there's an exact file without extension, serve it.
            if let Some((p, bytes)) = try_open(&file_path).await {
                return Ok(ok_with_type(bytes, guess_content_type(&p)));
            }

            // 4b) Try common defaults like .html last
            let html = file_path.with_extension("html");
            if let Some((p, bytes)) = try_open(&html).await {
                return Ok(ok_with_type(bytes, guess_content_type(&p)));
            }
        }

        // 404
        let mut not_found = Response::new(Body::from("404 Not Found"));
        *not_found.status_mut() = StatusCode::NOT_FOUND;
        Ok(not_found)
    }
}

fn ok_with_type(body: Vec<u8>, content_type: &'static str) -> Response<Body> {
    let mut resp = Response::new(Body::from(body));
    resp.headers_mut()
        .insert(CONTENT_TYPE, HeaderValue::from_static(content_type));
    resp
}
