use std::{collections::HashMap, fmt::Display, path::PathBuf, pin::Pin};

use bevy_ecs::resource::Resource;
use futures::channel::mpsc::UnboundedSender;
use futures::FutureExt;
use swls_core::prelude::FsDirEntry;
use swls_core::{
    client::{Client, ClientSync, Resp},
    lsp_types::{request::Request, Diagnostic, MessageType, Url},
    prelude::{File, FsTrait},
};
use tokio::fs::{self, read_to_string, write};
use tracing::debug;

pub mod reqwest {
    pub use reqwest::{
        header::{HeaderMap, HeaderName, HeaderValue},
        Error, StatusCode, Url,
    };
}
use std::future::Future;

pub(crate) type LocalFuture = Pin<Box<dyn Future<Output = ()> + 'static>>;
pub type Job = Box<dyn FnOnce() -> LocalFuture + Send>;

// TODO: use other tmp file location
#[derive(Debug)]
pub struct BinFs(PathBuf);
impl BinFs {
    pub fn new() -> Self {
        let mut tmp_dir = std::env::temp_dir();
        tmp_dir.push("swls");
        let mut c = 0;
        tmp_dir.push(c.to_string());
        while std::fs::exists(&tmp_dir).unwrap_or(false) {
            tmp_dir.pop();
            c += 1;
            tmp_dir.push(c.to_string());
        }
        Self(tmp_dir)
    }
}

#[tower_lsp::async_trait]
impl FsTrait for BinFs {
    fn virtual_url(&self, url: &str) -> Option<swls_core::lsp_types::Url> {
        let mut pb = self.0.clone();
        if let Ok(url) = swls_core::lsp_types::Url::parse(url) {
            pb.push(url.path());
        } else {
            pb.push(url);
        }
        swls_core::lsp_types::Url::from_file_path(pb).ok()
    }

    async fn read_file(&self, url: &swls_core::lsp_types::Url) -> Option<String> {
        let fp = url.to_file_path().ok()?;
        let content = read_to_string(fp).await.ok()?;
        Some(content)
    }

    async fn glob_read(&self, url: &str) -> Option<Vec<File>> {
        let mut files = Vec::new();
        for entry in glob::glob(url).ok()? {
            match entry {
                Ok(path) => {
                    let name = path.display().to_string();
                    let content = read_to_string(path).await.ok()?;
                    files.push(File { name, content })
                }
                Err(e) => tracing::error!("{:?}", e),
            }
        }
        Some(files)
    }

    async fn write_file(&self, url: &swls_core::lsp_types::Url, content: &str) -> Option<()> {
        let fp = url.to_file_path().ok()?;
        if let Some(parent) = fp.parent() {
            fs::create_dir_all(parent).await.ok()?;
        }
        write(fp, content.as_bytes()).await.ok()
    }

    async fn read_dir(&self, path: &Url) -> Option<Vec<FsDirEntry>> {
        let mut entries = Vec::new();
        let path = path.to_file_path().ok()?;
        let mut rd = tokio::fs::read_dir(path).await.ok()?;
        while let Some(entry) = rd.next_entry().await.ok()? {
            let metadata = entry.metadata().await.ok()?;

            let path = if metadata.is_file() {
                Url::from_file_path(entry.path())
            } else {
                Url::from_directory_path(entry.path())
            }
            .ok()?;
            entries.push(FsDirEntry {
                name: entry.file_name().to_string_lossy().into_owned(),
                path,
                is_dir: metadata.is_dir(),
            });
        }
        Some(entries)
    }
    async fn is_file(&self, path: &Url) -> bool {
        if let Some(p) = path.to_file_path().ok() {
            p.is_file()
        } else {
            !path.as_str().ends_with('/')
        }
    }
    async fn is_dir(&self, path: &Url) -> bool {
        if let Some(p) = path.to_file_path().ok() {
            p.is_dir()
        } else {
            !path.as_str().ends_with('/')
        }
    }

    async fn glob(&self, base: &Url, pattern: &str) -> Option<Vec<FsDirEntry>> {
        let base_path = base.to_file_path().ok()?;
        let full_pattern = base_path.join(pattern).to_string_lossy().into_owned();

        let mut entries = Vec::new();
        let paths = glob::glob(&full_pattern).ok()?;
        for path in paths {
            let path = path.ok()?;
            let is_dir = path.is_dir();
            let name = path
                .file_name()
                .map(|n| n.to_string_lossy().into_owned())
                .unwrap_or_default();

            let entry_url = if is_dir {
                Url::from_directory_path(&path).ok()?
            } else {
                Url::from_file_path(&path).ok()?
            };
            entries.push(FsDirEntry {
                name,
                path: entry_url,
                is_dir,
            });
        }

        Some(entries)
    }
}

#[derive(Resource, Clone)]
pub struct TowerClient {
    client: tower_lsp::Client,
    sender: UnboundedSender<Job>,
    handle: tokio::runtime::Handle,
}
impl TowerClient {
    pub fn new(client: tower_lsp::Client, sender: UnboundedSender<Job>) -> Self {
        Self {
            client,
            sender,
            handle: tokio::runtime::Handle::current(),
        }
    }
}

impl ClientSync for TowerClient {
    fn spawn<F: std::future::Future<Output = ()> + Send + 'static>(&self, fut: F) {
        self.handle.spawn(fut);
    }

    fn fetch(
        &self,
        url: &str,
        headers: &HashMap<String, String>,
    ) -> Pin<Box<dyn Send + std::future::Future<Output = Result<Resp, String>>>> {
        use tokio::{fs::File, io::AsyncReadExt};
        use tracing::error;
        debug!("fetching resource {}", url);

        let m_url = reqwest::Url::parse(url);

        let client = ::reqwest::Client::new();
        let builder = client.get(url);
        let builder = headers
            .into_iter()
            .fold(builder, |builder, (k, v)| builder.header(k, v));

        return async {
            let url = m_url.map_err(|_| String::from("invalid url!"))?;
            if url.scheme() == "file" {
                let mut file = File::open(url.path())
                    .await
                    .map_err(|_| format!("File not found {}", url.path()))?;
                let mut body = String::new();
                file.read_to_string(&mut body)
                    .await
                    .map_err(|_| format!("Failed to read file"))?;
                let status = 200;
                let headers = Vec::new();
                return Ok(Resp {
                    headers,
                    body,
                    status,
                });
            }

            let resp = match builder.send().await {
                Ok(x) => x,
                Err(e) => {
                    error!(error = ?e);
                    return Err(e.to_string());
                }
            };

            let status = resp.status().as_u16();
            let headers: Vec<_> = resp
                .headers()
                .iter()
                .flat_map(|(h, v)| {
                    if let Ok(value) = v.to_str() {
                        Some((h.to_string(), value.to_string()))
                    } else {
                        None
                    }
                })
                .collect();
            let body = resp.text().await.unwrap();

            Ok(Resp {
                headers,
                body,
                status,
            })
        }
        .boxed();
    }

    fn spawn_local<F, Fut>(&self, f: F)
    where
        F: FnOnce() -> Fut + Send + 'static,
        Fut: std::future::Future<Output = ()> + 'static,
    {
        let _ = self.sender.unbounded_send(Box::new(move || {
            Box::pin(async move {
                f().await;
            })
        }));
    }
}

#[tower_lsp::async_trait]
impl Client for TowerClient {
    async fn log_message<M: Display + Sync + Send + 'static>(&self, ty: MessageType, msg: M) -> () {
        self.client.log_message(ty, msg).await;
    }

    async fn send_request<R: Request>(&self, params: R::Params) -> Option<R::Result>
    where
        R::Params: Sync + Send + 'static,
        R::Result: Sync + Send + 'static,
    {
        self.client.send_request::<R>(params).await.ok()
    }

    async fn publish_diagnostics(
        &self,
        uri: Url,
        diags: Vec<Diagnostic>,
        version: Option<i32>,
    ) -> () {
        self.client.publish_diagnostics(uri, diags, version).await;
    }
}
