#![allow(clippy::disallowed_methods, clippy::disallowed_macros)]
#![allow(deprecated)] // TODO: Update querybuilder usage to DQL
extern crate dittolive_ditto;
extern crate safer_ffi;

mod car;

use std::sync::Arc;

use axum::Router;
use bpaf::Bpaf;
use car::Car;
use dioxus::prelude::*;
use dioxus_liveview::LiveviewRouter;
use dittolive_ditto::{prelude::*, transport::Peer};
use dotenvy::dotenv;
use once_cell::sync::Lazy;
#[allow(deprecated)]
use tracing::{error, info};

const CARSAPP_DATABASE_ID: &str = "d0e9a40e-9beb-4c90-bb38-769269cbb6fb";

static CARS: Lazy<tokio::sync::watch::Sender<Arc<[Car]>>> = Lazy::new(|| {
    let (sender, _) = tokio::sync::watch::channel(Arc::from([]));
    sender
});
static REMOTE_PEERS: Lazy<tokio::sync::watch::Sender<Vec<Peer>>> = Lazy::new(|| {
    let (sender, _) = tokio::sync::watch::channel(Vec::new());
    sender
});

static DITTO: std::sync::OnceLock<Arc<Ditto>> = std::sync::OnceLock::new();

fn ditto() -> &'static Arc<Ditto> {
    DITTO.get().expect("Ditto not initialized")
}

fn app() -> Element {
    let mut cmd_signal = use_signal(|| "".to_string());
    let mut cars_signal = use_signal_sync(|| Arc::<[Car]>::from([]));
    let mut remote_peers_signal = use_signal_sync(Vec::<Peer>::new);

    let help = options()
        .run_inner(&["--help"])
        .unwrap_err()
        .unwrap_stdout();

    let mut cmd_output_signal = use_signal_sync(|| help);

    let mut dql_signal = use_signal(|| "SELECT * FROM system:transport_connections".to_string());
    let mut dql_output_signal = use_signal_sync(String::new);

    let _cars_update_fut = use_future(move || async move {
        let init_val = CARS.borrow().clone();
        cars_signal.set(init_val);

        let mut receiver = CARS.subscribe();

        loop {
            let changed = receiver.changed().await;
            if changed.is_err() {
                {
                    #![allow(deprecated)]
                    error!("error updating cars");
                }
                break;
            }

            let new_val = receiver.borrow().clone();
            cars_signal.set(new_val);
        }
    });

    let _peer_update_fut = use_future(move || async move {
        let init_val = REMOTE_PEERS.borrow().clone();
        remote_peers_signal.set(init_val);

        let mut receiver = REMOTE_PEERS.subscribe();

        loop {
            let changed = receiver.changed().await;
            if changed.is_err() {
                {
                    #![allow(deprecated)]
                    error!("error updating peers");
                }
                break;
            }

            let new_val = receiver.borrow().clone();
            remote_peers_signal.set(new_val);
        }
    });

    rsx! {
        style { {include_str!("mycss.css")} }
        div {
            div {
                input {
                    value: "{cmd_signal}",
                    oninput: move |event| cmd_signal.set(event.value())
                }
                button {
                    onclick: move |_event| {
                        let cmd = cmd_signal.read().clone();
                        cmd_signal.set("".to_string());
                        spawn(async move {
                            let output = handle_cmd(cmd).await;
                            match output {
                                Ok(val) => cmd_output_signal.set(val),
                                Err(e) => {
                                    let output = format!("Error handling cmd: {:?}", e);
                                    cmd_output_signal.set(output);
                                },
                            }
                        });
                    },
                    "Execute"
                }
                pre {
                    "{cmd_output_signal}"
                }
            }
            div {
                h3 { "DQL Query" }
                textarea {
                    value: "{dql_signal}",
                    oninput: move |event| dql_signal.set(event.value()),
                    rows: "4",
                    cols: "60",
                    placeholder: "Enter DQL query, e.g. SELECT * FROM cars WHERE color = 'Red🔴'"
                }
                br {}
                button {
                    onclick: move |_event| {
                        let query = dql_signal.read().clone();
                        info!(?query, "executing DQL query");
                        let ditto = ditto().clone();
                        spawn(async move {
                            let output = execute_dql(&ditto, &query).await;
                            dql_output_signal.set(output);
                        });
                    },
                    "Run DQL"
                }
                pre {
                    "{dql_output_signal}"
                }
            }
            button {
                onclick: move |_event| {
                    info!("Adding a new car");
                    let ditto = ditto().clone();
                    let new_car = car::Car::generate();
                    spawn(async move {
                        ditto
                            .store()
                            .execute(
                                "INSERT INTO cars DOCUMENTS (:car) ON ID CONFLICT DO UPDATE",
                                Some(serde_json::json!({ "car": new_car }).into()),
                            )
                            .await
                            .unwrap();
                    });
                },
                "Add Car"
            }
            table {
                caption { "Ditto Cars App" }
                thead {
                    tr {
                        th { scope: "col", "Brand" }
                        th { scope: "col", "Color" }
                        th { scope: "col", "Km" }
                    }
                }
                tbody {
                    for car in cars_signal.read().iter() {{
                        let brand = car.brand.clone().unwrap_or("No brand".to_string());
                        let color = &car.color;
                        let km = car.kilometers.unwrap_or(0);
                        rsx! {
                            tr {
                                th { scope: "row", "{brand}" }
                                td { "{color}" }
                                td { "{km}" }
                            }
                        }
                    }}
                }
            }

            table {
                caption { "Remote Peers" }
                thead {
                    tr {
                        th { scope: "col", "device_name" }
                        th { scope: "col", "peer_key_string" }
                        th { scope: "col", "os" }
                    }
                }
                tbody {
                    for peer in remote_peers_signal.read().iter() {{
                        let device_name = &peer.device_name;
                        let key = &peer.peer_key_string;
                        let os = peer.os.as_ref().map(|val| val.to_string()).unwrap_or("no OS reported".to_string());
                        rsx! {
                            tr {
                                th { scope: "row", "{device_name}" }
                                td { "{key}" }
                                td { "{os}" }
                            }
                        }
                    }}
                }
            }
        }
    }
}

#[allow(deprecated)] // TODO: Update querybuilder usage to DQL
#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // pretty_env_logger::init();
    dotenv().ok();
    tracing_subscriber::fmt::init();

    // Temporary persistence directory for Ditto; deleted when dropped
    let _temp_root = TempRoot::new();

    let ditto_instance = Ditto::open_sync(
        DittoConfig::new(
            CARSAPP_DATABASE_ID,
            DittoConfigConnect::SmallPeersOnly { private_key: None },
        )
        .with_persistence_directory(_temp_root.root_path()),
    )?;
    ditto_instance.set_license_from_env("DITTO_LICENSE")?;

    DittoLogger::set_minimum_log_level(LogLevel::Debug);

    ditto_instance.update_transport_config(|tc| {
        tc.peer_to_peer.lan.enabled = true;
        tc.peer_to_peer.lan.mdns_enabled = true;
        tc.peer_to_peer.lan.multicast_enabled = false;
        tc.listen.tcp.enabled = true;
        tc.listen.tcp.interface_ip = "0.0.0.0".to_string();
    });

    ditto_instance.disable_sync_with_v3()?;
    ditto_instance.sync().start()?;

    DITTO
        .set(Arc::new(ditto_instance))
        .ok()
        .expect("Ditto already initialized");

    info!("Ditto cars app started!");

    let event_handler = move |documents: Vec<ffi_sdk::BoxedDocument>, _event| {
        let cars = documents
            .iter()
            .map(|doc| doc.get::<car::Car>("").unwrap())
            .collect::<Arc<[_]>>();

        CARS.send_replace(cars);
    };
    // Find and report on all cars
    let store = ditto().store().clone();
    let collection = store.collection("cars").unwrap();
    let pending_op = collection.find_all();
    let _sub = pending_op.subscribe();
    let _lq = pending_op.observe_local(event_handler).unwrap();
    DittoLogger::set_minimum_log_level(LogLevel::Debug);

    // Peers
    let _peer_observer = ditto().presence().register_observer(move |presence_graph| {
        let remote_peers = presence_graph.remote_peers.clone();
        REMOTE_PEERS.send_replace(remote_peers);
    });

    let default_port = 3030;
    let port = std::env::var("PORT")
        .ok()
        .and_then(|val| val.parse::<u16>().ok())
        .unwrap_or(default_port);

    // Start the web server
    let addr: std::net::SocketAddr = ([127, 0, 0, 1], port).into();

    let app = Router::new().with_app("/", app);

    {
        #![allow(deprecated)]
        info!("Listening on http://{addr}");
    }

    let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
    axum::serve(listener, app.into_make_service())
        .await
        .unwrap();

    Ok(())
}

async fn execute_dql(ditto: &Ditto, query: &str) -> String {
    match ditto.store().execute(query, None).await {
        Ok(result) => {
            let count = result.item_count();
            let mut rows: Vec<serde_json::Value> = Vec::with_capacity(count);
            for item in result.iter() {
                match serde_json::from_str::<serde_json::Value>(&item.json_string()) {
                    Ok(val) => rows.push(val),
                    Err(e) => rows.push(serde_json::json!({"_error": e.to_string()})),
                }
            }
            let mutated_ids = result.mutated_document_ids();
            let mut output = serde_json::json!({
                "item_count": count,
                "items": rows,
            });
            if !mutated_ids.is_empty() {
                output["mutated_document_ids"] = serde_json::json!(mutated_ids
                    .iter()
                    .map(|id| id.to_string())
                    .collect::<Vec<_>>());
            }
            serde_json::to_string_pretty(&output).unwrap_or_else(|e| format!("JSON error: {e}"))
        }
        Err(e) => format!("DQL error: {e}"),
    }
}

async fn handle_cmd(cmd: String) -> anyhow::Result<String> {
    info!(?cmd, "handle_cmd");
    let args = shlex::split(&cmd).ok_or(anyhow::anyhow!("Error parsing cmd"))?;

    let opt = options().run_inner(args.as_slice());

    let opt = match opt {
        Ok(val) => val,
        Err(e) => {
            let ret = match e {
                bpaf::ParseFailure::Stdout(doc, _bool) => doc.monochrome(true),
                bpaf::ParseFailure::Completion(val) => val,
                bpaf::ParseFailure::Stderr(doc) => doc.monochrome(true),
            };
            return Ok(ret);
        }
    };

    {
        #![allow(deprecated)]
        info!(?opt, "handling opt");
    }

    match opt {
        Options::AddCar => {
            let new_car = car::Car::generate();
            ditto()
                .store()
                .execute(
                    "INSERT INTO cars DOCUMENTS (:car) ON ID CONFLICT DO UPDATE",
                    Some(serde_json::json!({ "car": new_car }).into()),
                )
                .await
                .unwrap();
        }
        Options::Transport(transport_cmd) => match transport_cmd {
            TransportCmd::Ble { enable } => {
                let mut config = ditto().transport_config();
                config.peer_to_peer.bluetooth_le.enabled = enable;
                ditto().set_transport_config(config);
            }
            TransportCmd::Lan { mdns, multicast } => {
                let mut config = ditto().transport_config();
                if mdns || multicast {
                    config.peer_to_peer.lan.enabled = true;
                    config.peer_to_peer.lan.mdns_enabled = mdns;
                    config.peer_to_peer.lan.multicast_enabled = multicast;
                } else {
                    config.peer_to_peer.lan.enabled = false;
                }
                ditto().set_transport_config(config);
            }
            TransportCmd::TcpListen { bind_address, port } => {
                let mut config = ditto().transport_config();
                config.listen.tcp.interface_ip = bind_address;
                config.listen.tcp.port = port;
                config.listen.tcp.enabled = true;
                ditto().set_transport_config(config);
            }
            TransportCmd::TcpConnect { address } => {
                let mut config = ditto().transport_config();
                config.connect.tcp_servers.insert(address);
                ditto().set_transport_config(config);
            }
            TransportCmd::WsConnect { address } => {
                let mut config = ditto().transport_config();
                config.connect.websocket_urls.insert(address);
                ditto().set_transport_config(config);
            }
            TransportCmd::CurrentConfig => {
                let config = ditto().transport_config();
                let val_json = serde_json::to_string_pretty(&config)?;
                return Ok(val_json);
            }
        },
    }

    Ok("done".to_string())
}

#[derive(Debug, Clone, Bpaf)]
pub enum TransportCmd {
    /// BLE
    #[bpaf(command)]
    Ble { enable: bool },

    /// LAN
    #[bpaf(command)]
    Lan { mdns: bool, multicast: bool },

    /// Start TCP server at port
    #[bpaf(command)]
    TcpListen { bind_address: String, port: u16 },

    /// Connect to TCP server
    #[bpaf(command)]
    TcpConnect { address: String },

    /// Connect to websocket server
    #[bpaf(command)]
    WsConnect { address: String },

    /// Show current transport config
    #[bpaf(command)]
    CurrentConfig,
}

#[derive(Debug, Clone, Bpaf)]
#[bpaf(options)]
pub enum Options {
    /// Transport related commands
    #[bpaf(command)]
    Transport(#[bpaf(external(transport_cmd))] TransportCmd),

    /// Add a random new car
    #[bpaf(command)]
    AddCar,
}
