use std::path::PathBuf;
use std::sync::mpsc::{Receiver, TryRecvError};

use bone_ui::widgets::FilePickerMode;

#[derive(Debug)]
pub enum NativeOutcome {
    Path(PathBuf),
    Cancelled,
    Error(String),
}

#[derive(Debug)]
pub enum SpawnError {
    Unsupported,
}

pub struct PendingHandle {
    rx: Receiver<NativeOutcome>,
    pub mode: FilePickerMode,
}

impl PendingHandle {
    #[must_use]
    pub fn poll(&self) -> PollState {
        match self.rx.try_recv() {
            Ok(outcome) => PollState::Ready(outcome),
            Err(TryRecvError::Empty) => PollState::Pending,
            Err(TryRecvError::Disconnected) => PollState::Ready(NativeOutcome::Error(
                "picker worker disconnected".to_owned(),
            )),
        }
    }
}

#[derive(Debug)]
pub enum PollState {
    Pending,
    Ready(NativeOutcome),
}

#[derive(Copy, Clone)]
pub struct Request<'a> {
    pub mode: FilePickerMode,
    pub title: &'a str,
    pub accept_label: &'a str,
    pub seed_filename: Option<&'a str>,
    pub current_folder: Option<&'a std::path::Path>,
}

#[cfg(target_os = "linux")]
pub fn spawn(req: Request<'_>) -> Result<PendingHandle, SpawnError> {
    linux_impl::spawn(req)
}

#[cfg(not(target_os = "linux"))]
pub fn spawn(_req: Request<'_>) -> Result<PendingHandle, SpawnError> {
    Err(SpawnError::Unsupported)
}

#[cfg(target_os = "linux")]
mod linux_impl {
    use std::path::PathBuf;
    use std::sync::mpsc;

    use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest};
    use bone_ui::widgets::FilePickerMode;

    use super::{NativeOutcome, PendingHandle, Request, SpawnError};

    pub(super) fn spawn(req: Request<'_>) -> Result<PendingHandle, SpawnError> {
        let mode = req.mode;
        let title = req.title.to_owned();
        let accept = req.accept_label.to_owned();
        let seed = req.seed_filename.map(str::to_owned);
        let folder = req.current_folder.map(PathBuf::from);
        let (tx, rx) = mpsc::channel();
        let join = std::thread::Builder::new()
            .name("bone-native-picker".to_owned())
            .spawn(move || {
                let outcome = pollster::block_on(run(mode, &title, &accept, seed, folder));
                let _ = tx.send(outcome);
            });
        match join {
            Ok(_handle) => Ok(PendingHandle { rx, mode }),
            Err(_) => Err(SpawnError::Unsupported),
        }
    }

    async fn run(
        mode: FilePickerMode,
        title: &str,
        accept: &str,
        seed: Option<String>,
        folder: Option<PathBuf>,
    ) -> NativeOutcome {
        match mode {
            FilePickerMode::Open => run_open(title, accept, folder).await,
            FilePickerMode::Save => run_save(title, accept, seed, folder).await,
        }
    }

    async fn run_open(title: &str, accept: &str, folder: Option<PathBuf>) -> NativeOutcome {
        let mut req = OpenFileRequest::default()
            .title(title)
            .accept_label(accept)
            .modal(true)
            .multiple(false)
            .directory(true);
        if let Some(f) = folder {
            req = match req.current_folder::<PathBuf>(Some(f)) {
                Ok(r) => r,
                Err(e) => return NativeOutcome::Error(e.to_string()),
            };
        }
        let response = match req.send().await {
            Ok(r) => r,
            Err(e) => return classify_send_error(&e),
        };
        let selected = match response.response() {
            Ok(s) => s,
            Err(e) => return classify_response_error(&e),
        };
        match selected
            .uris()
            .first()
            .and_then(|u| uri_to_path(u.as_str()))
        {
            Some(path) => NativeOutcome::Path(path),
            None => NativeOutcome::Cancelled,
        }
    }

    async fn run_save(
        title: &str,
        accept: &str,
        seed: Option<String>,
        folder: Option<PathBuf>,
    ) -> NativeOutcome {
        let mut req = SaveFileRequest::default()
            .title(title)
            .accept_label(accept)
            .modal(true);
        if let Some(s) = seed.as_deref() {
            req = req.current_name(s);
        }
        if let Some(f) = folder {
            req = match req.current_folder::<PathBuf>(Some(f)) {
                Ok(r) => r,
                Err(e) => return NativeOutcome::Error(e.to_string()),
            };
        }
        let response = match req.send().await {
            Ok(r) => r,
            Err(e) => return classify_send_error(&e),
        };
        let selected = match response.response() {
            Ok(s) => s,
            Err(e) => return classify_response_error(&e),
        };
        match selected
            .uris()
            .first()
            .and_then(|u| uri_to_path(u.as_str()))
        {
            Some(path) => NativeOutcome::Path(path),
            None => NativeOutcome::Cancelled,
        }
    }

    fn classify_send_error(e: &ashpd::Error) -> NativeOutcome {
        NativeOutcome::Error(format!("portal send: {e}"))
    }

    fn classify_response_error(e: &ashpd::Error) -> NativeOutcome {
        match e {
            ashpd::Error::Response(ashpd::desktop::ResponseError::Cancelled) => {
                NativeOutcome::Cancelled
            }
            other => NativeOutcome::Error(format!("portal response: {other}")),
        }
    }

    fn uri_to_path(uri: &str) -> Option<PathBuf> {
        let encoded = uri.strip_prefix("file://")?;
        let decoded = percent_encoding::percent_decode_str(encoded).decode_utf8_lossy();
        Some(PathBuf::from(decoded.as_ref()))
    }

    #[cfg(test)]
    mod tests {
        use super::uri_to_path;
        use std::path::PathBuf;

        #[test]
        fn plain_path() {
            assert_eq!(
                uri_to_path("file:///home/nel/docs"),
                Some(PathBuf::from("/home/nel/docs")),
            );
        }

        #[test]
        fn percent_decoded() {
            assert_eq!(
                uri_to_path("file:///home/nel/my%20docs"),
                Some(PathBuf::from("/home/nel/my docs")),
            );
        }

        #[test]
        fn rejects_non_file_scheme() {
            assert_eq!(uri_to_path("http://example.com/docs"), None);
        }
    }
}
