use alloc::string::ToString;
use embassy_sync::channel::DynamicSender;
use embassy_sync::pubsub::DynSubscriber;
use embassy_time::{Duration, Instant, WithTimeout};
use esp_hal::rng::Rng;
use esp_radio::wifi::{ClientConfig, ScanConfig, WifiController, WifiDevice, WifiEvent};
use log::*;
use alloc::format;

use embassy_net::dns::DnsSocket;
use embassy_net::tcp::client::{TcpClient, TcpClientState};
use embassy_net::Stack;
use nalgebra::Vector2;
use reqwless::client::{HttpClient, TlsConfig};

use crate::ego::engine::gps_to_local_meters_haversine;
use crate::events::{Measurement, SensorSource, SensorState};
use crate::{backoff::Backoff, events::{Prediction}};

#[embassy_executor::task]
pub async fn net_task(mut runner: embassy_net::Runner<'static, WifiDevice<'static>>) {
    info!("Starting network stack");
    runner.run().await
}

#[embassy_executor::task]
pub async fn wifi_connect_task(mut wifi: WifiController<'static>, stack: Stack<'static>, motion: DynamicSender<'static, Measurement>) {
    wifi.set_config(&esp_radio::wifi::ModeConfig::Client(
        ClientConfig::default()
            .with_ssid("The Frequency".to_string())
            .with_auth_method(esp_radio::wifi::AuthMethod::Wpa2Personal)
            .with_password("thepasswordkenneth".to_string())
    )).unwrap();
    wifi.set_mode(esp_radio::wifi::WifiMode::Sta).unwrap();
    wifi.set_power_saving(esp_radio::wifi::PowerSaveMode::Maximum).unwrap();

    loop {
        Backoff::from_secs(3).forever().attempt(async || {
            info!("Connecting to wifi...");
            wifi.start_async().await.unwrap();
            motion.send(Measurement::SensorHardwareStatus(SensorSource::Wifi, crate::events::SensorState::AcquiringFix)).await;
            let networks = wifi.scan_with_config_async(ScanConfig::default().with_show_hidden(true)).await.unwrap();
            for network in networks {
                info!("wifi: {} @ {}db", network.ssid, network.signal_strength);
            }
            match wifi.connect_async().await {
                Ok(_) => Ok(()),
                Err(e) => {
                    error!("Unable to connect to wifi: {e:?}");
                    wifi.stop_async().await.unwrap();
                    Err(())
                }
            }
        }).await.unwrap();

        info!("Waiting for DHCP...");
        motion.send(Measurement::SensorHardwareStatus(SensorSource::Wifi, SensorState::Degraded)).await;
        if stack.wait_config_up().with_timeout(Duration::from_secs(5)).await.is_ok() {
            info!("Online!");
            motion.send(Measurement::SensorHardwareStatus(SensorSource::Wifi, SensorState::Online)).await;
            let ip_cfg = stack.config_v4().unwrap();
            info!("ip={ip_cfg:?}");
            wifi.wait_for_event(WifiEvent::ApStaDisconnected).await;
            info!("Wifi disconnected!");
        } else {
            warn!("DHCP timed out after 5 seconds. Disconnecting wifi");
            wifi.disconnect_async().await.unwrap();
        }
        warn!("Stopping wifi device");
        wifi.stop_async().await.unwrap();
        motion.send(Measurement::SensorHardwareStatus(SensorSource::Wifi, SensorState::Offline)).await;
    }
}

// TODO: Wifi task needs to know when there is data to upload, so it only connects when needed.
#[embassy_executor::task]
pub async fn http_telemetry_task(mut predictions: DynSubscriber<'static, Prediction>, stack: Stack<'static>) {
    // TODO: should wait for wifi disconnect event somehow and use that to restart sending the wifi around

    let seed = Rng::new().random() as i32;
    let mut rx_buf = [0; 4096];
    let mut tx_buf = [0; 4096];
    let dns = DnsSocket::new(stack);
    let tcp_state = TcpClientState::<1, 4096, 4096>::new();
    let tcp = TcpClient::new(stack, &tcp_state);
    let tls = TlsConfig::new(
        seed as u64,
        &mut rx_buf,
        &mut tx_buf,
        reqwless::client::TlsVerify::None,
    );

    let mut client = HttpClient::new_with_tls(&tcp, &dns, tls);

    // TODO: Only should upload a data point after some distance has occurred
    let mut last_location = Vector2::default();
    let mut last_push = Instant::from_ticks(0);

    loop {
        if let Prediction::Location(coords) = predictions.next_message_pure().await {

            if stack.is_config_up() {
                // Only push to HTTP if we have an ip config etc
                if last_push.elapsed().as_secs() >= 5 || gps_to_local_meters_haversine(&last_location, &coords).norm() >= 10.0 {
                    last_location = coords;
                    last_push = Instant::now();
                    if let Err(e) = Backoff::from_secs(3).attempt(async || {
                        push_location(&mut client, coords, Instant::now().as_millis()).await
                    }).await {
                        warn!("Could not submit location! {e:?}");
                    }
                }
            }
        }
    }
}

async fn push_location(client: &mut HttpClient<'_, TcpClient<'_, 1, 4096, 4096>, DnsSocket<'_>>, location: Vector2<f64>, timestamp: u64) -> Result<(), reqwless::Error> {
    let mut buffer = [0u8; 4096];
    let base = "https://nextcloud.malloc.hackerbots.net/nextcloud/index.php/apps/phonetrack/logGet/a062000067304e9dee6590f1b8f9e0db/renderbug";
    let url = format!("{base}?lon={}&lat={}&timestamp={}", location.y, location.x, timestamp);
    info!("Pushing to {url}");
    let mut http_req = client
        .request(
            reqwless::request::Method::GET,
            &url,
        )
        .await?;
    let response = http_req.send(&mut buffer).await?;

    let res = response.body().read_to_end().await?;

    let content = core::str::from_utf8(res).unwrap();
    debug!("HTTP response: {content}");
    Ok(())
}