use gloo::net::http;
use thiserror::Error;
use time::{format_description::well_known::Rfc3339, OffsetDateTime};

use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::{js_sys, JsFuture};
use web_sys::{
    js_sys::Object, FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetFileOptions,
    FileSystemWritableFileStream, WorkerGlobalScope,
};

const GITHUB_API: &str = "https://api.github.com";

pub async fn github_fetch_last_updated(
    owner: &str,
    repo: &str,
    path: &str,
) -> Result<OffsetDateTime, GithubError> {
    let commit: Vec<serde_json::Value> = http::Request::get(&format!(
        "{GITHUB_API}/repos/{owner}/{repo}/commits?path={path}&page=1&per_page=1"
    ))
    .send()
    .await?
    .json()
    .await?;

    commit[0]
        .get("commit")
        .and_then(|commit| commit.get("committer"))
        .and_then(|committer| committer.get("date"))
        .map(|date| {
            OffsetDateTime::parse(
                date.as_str().ok_or(GithubError::MissingCommitDateError)?,
                &Rfc3339,
            )
            .map_err(|err| err.into())
        })
        .unwrap_or(Err(GithubError::MissingCommitDateError))
}

pub async fn github_fetch_file(
    owner: &str,
    repo: &str,
    path: &str,
) -> Result<Vec<u8>, GithubError> {
    http::Request::get(&format!(
        "{GITHUB_API}/repos/{owner}/{repo}/contents/{path}"
    ))
    .header("Accept", "application/vnd.github.raw+json")
    .send()
    .await?
    .binary()
    .await
    .map_err(|err| err.into())
}

#[derive(Debug, Error)]
pub enum GithubError {
    #[error("no file returned from the given GitHub API")]
    NoSuchFile,
    #[error("failed to fetch from GitHub API")]
    RequestError(#[from] gloo::net::Error),
    #[error("file fetched from the GitHub API does not have a commit date")]
    MissingCommitDateError,
    #[error("date/time format reported for GitHub commit is invalid")]
    InvalidDateTimeError(#[from] time::error::Parse),
}

/// Read a file from the in-browser OPFS filesystem, creating the file
/// if it does not exist.
pub async fn read_opfs_file(path: &str) -> Result<String, OpfsError> {
    let dir: FileSystemDirectoryHandle = JsFuture::from(
        js_sys::global()
            .dyn_into::<WorkerGlobalScope>()
            .map_err(OpfsError::NotSupported)?
            .navigator()
            .storage()
            .get_directory(),
    )
    .await
    .map_err(OpfsError::HandleAccess)?
    .into();

    let opt = FileSystemGetFileOptions::new();
    opt.set_create(true);
    let file_handle: FileSystemFileHandle =
        JsFuture::from(dir.get_file_handle_with_options(path, &opt))
            .await
            .map_err(OpfsError::HandleAccess)?
            .into();

    let file: web_sys::File = JsFuture::from(file_handle.get_file())
        .await
        .map_err(OpfsError::HandleAccess)?
        .into();

    Ok(gloo::file::futures::read_as_text(&gloo::file::Blob::from(file)).await?)
}

/// Write contents to a file in the in-browser OPFS, creating the file
/// if it does not exist.
pub async fn write_opfs_file(path: &str, contents: &str) -> Result<(), OpfsError> {
    let dir: FileSystemDirectoryHandle = JsFuture::from(
        js_sys::global()
            .dyn_into::<WorkerGlobalScope>()
            .map_err(OpfsError::NotSupported)?
            .navigator()
            .storage()
            .get_directory(),
    )
    .await
    .map_err(OpfsError::HandleAccess)?
    .into();

    let opt = FileSystemGetFileOptions::new();
    opt.set_create(true);
    let file_handle: FileSystemFileHandle =
        JsFuture::from(dir.get_file_handle_with_options(path, &opt))
            .await
            .map_err(OpfsError::HandleAccess)?
            .into();

    let write_handle: FileSystemWritableFileStream = JsFuture::from(file_handle.create_writable())
        .await
        .map_err(OpfsError::HandleAccess)?
        .into();

    JsFuture::from(
        write_handle
            .write_with_str(contents)
            .map_err(OpfsError::WriteFile)?,
    )
    .await
    .map_err(OpfsError::WriteFile)?;

    JsFuture::from(write_handle.close())
        .await
        .map_err(OpfsError::WriteFile)?;

    Ok(())
}

#[derive(Debug, Error)]
pub enum OpfsError {
    #[error("OPFS is not supported by this browser: {0:?}")]
    NotSupported(Object),
    #[error("could not get OPFS directory path: {0:?}")]
    HandleAccess(JsValue),
    #[error("could not read OPFS file")]
    ReadFile(#[from] gloo::file::FileReadError),
    #[error("could not write OPFS file: {0:?}")]
    WriteFile(JsValue),
}

#[cfg(all(
    any(target_arch = "wasm32", target_arch = "wasm64"),
    target_os = "unknown",
    test
))]
mod worker_tests {
    use wasm_bindgen_test::*;
    wasm_bindgen_test_configure!(run_in_dedicated_worker);

    use super::{read_opfs_file, write_opfs_file};

    #[wasm_bindgen_test]
    async fn test_read_write_opfs() {
        let empty = read_opfs_file("test.txt")
            .await
            .expect("file read must succeed");
        assert_eq!(empty, "");

        write_opfs_file("test.txt", "contents")
            .await
            .expect("file write must succeed");

        let nonempty = read_opfs_file("test.txt")
            .await
            .expect("file read must succeed");
        assert_eq!(nonempty, "contents");
    }
}