//offer ota and config mode

use crate::{
    determine_tank_state, get_version, log::LogMessage, plant_hal::PLANT_COUNT,
    plant_state::PlantState, BOARD_ACCESS,
};
use anyhow::bail;
use chrono::DateTime;
use core::result::Result::Ok;
use embedded_svc::http::Method;
use esp_idf_svc::http::server::{Configuration, EspHttpConnection, EspHttpServer, Request};
use esp_idf_sys::{settimeofday, timeval, vTaskDelay};
use esp_ota::OtaUpdate;
use heapless::String;
use serde::{Deserialize, Serialize};
use std::{
    str::from_utf8,
    sync::{atomic::AtomicBool, Arc},
};
use url::Url;

use crate::config::PlantControllerConfig;
use crate::plant_hal::BoardInteraction;
use crate::plant_state::MoistureSensorState;

#[derive(Serialize, Debug)]
struct SSIDList<'a> {
    ssids: Vec<&'a String<32>>,
}

#[derive(Serialize, Debug)]
struct LoadData<'a> {
    rtc: &'a str,
    native: &'a str,
}

#[derive(Serialize, Debug)]
struct Moistures {
    moisture_a: Vec<std::string::String>,
    moisture_b: Vec<std::string::String>,
}

#[derive(Deserialize, Debug)]
struct SetTime<'a> {
    time: &'a str,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct TestPump {
    pump: usize,
}

#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct WebBackupHeader {
    timestamp: std::string::String,
    size: usize,
}

#[derive(Deserialize)]
pub struct NightLampCommand {
    active: bool,
}

fn write_time(
    request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
    let actual_data = read_up_to_bytes_from_request(request, None)?;
    let time: SetTime = serde_json::from_slice(&actual_data)?;
    let parsed = DateTime::parse_from_rfc3339(time.time).map_err(|err| anyhow::anyhow!(err))?;
    let mut board = BOARD_ACCESS.lock().unwrap();

    let now = timeval {
        tv_sec: parsed.to_utc().timestamp(),
        tv_usec: 0,
    };
    unsafe { settimeofday(&now, core::ptr::null_mut()) };
    board.set_rtc_time(&parsed.to_utc())?;
    anyhow::Ok(None)
}

fn get_time(
    _request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
    let mut board = BOARD_ACCESS.lock().unwrap();
    let native = board
        .time()
        .map(|t| t.to_rfc3339())
        .unwrap_or("error".to_string());
    let rtc = board
        .get_rtc_time()
        .map(|t| t.to_rfc3339())
        .unwrap_or("error".to_string());

    let data = LoadData {
        rtc: rtc.as_str(),
        native: native.as_str(),
    };
    let json = serde_json::to_string(&data)?;

    anyhow::Ok(Some(json))
}

fn get_timezones(
    _request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
    // Get all timezones using chrono-tz
    let timezones: Vec<&'static str> = chrono_tz::TZ_VARIANTS.iter().map(|tz| tz.name()).collect();

    // Convert to JSON
    let json = serde_json::to_string(&timezones)?;
    anyhow::Ok(Some(json))
}

fn get_live_moisture(
    _request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
    let mut board = BOARD_ACCESS.lock().unwrap();
    let config = board.get_config().unwrap();

    let plant_state = Vec::from_iter(
        (0..PLANT_COUNT).map(|i| PlantState::read_hardware_state(i, &mut board, &config.plants[i])),
    );
    let a = Vec::from_iter(
        plant_state
            .iter()
            .map(|s| {
                match &s.sensor_a  {
                    MoistureSensorState::Disabled => "disabled".to_string(),
                    MoistureSensorState::MoistureValue {raw_hz, moisture_percent } => {
                        format!("{moisture_percent:.2}% {raw_hz}hz",)
                    }
                    MoistureSensorState::SensorError(err) => format!("{err:?}"),
                }
            })
    );
    let b = Vec::from_iter(
        plant_state
            .iter()
            .map(|s| {
                match &s.sensor_b  {
                    MoistureSensorState::Disabled => "disabled".to_string(),
                    MoistureSensorState::MoistureValue {raw_hz, moisture_percent } => {
                        format!("{moisture_percent:.2}% {raw_hz}hz",)
                    }
                    MoistureSensorState::SensorError(err) => format!("{err:?}"),
                }
            })
    );

    let data = Moistures {
        moisture_a: a,
        moisture_b: b,
    };
    let json = serde_json::to_string(&data)?;

    anyhow::Ok(Some(json))
}

fn get_config(
    _request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
    let mut board = BOARD_ACCESS.lock().unwrap();
    let json = match board.get_config() {
        Ok(config) => serde_json::to_string(&config)?,
        Err(_) => serde_json::to_string(&PlantControllerConfig::default())?,
    };
    anyhow::Ok(Some(json))
}

fn backup_config(
    request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
    let all = read_up_to_bytes_from_request(request, Some(3072))?;
    let mut board = BOARD_ACCESS.lock().unwrap();
    board.backup_config(&all)?;
    anyhow::Ok(Some("saved".to_owned()))
}

fn get_backup_config(
    _request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
    let mut board = BOARD_ACCESS.lock().unwrap();
    let json = match board.get_backup_config() {
        Ok(config) => from_utf8(&config)?.to_owned(),
        Err(err) => {
            println!("Error get backup config {:?}", err);
            err.to_string()
        }
    };
    anyhow::Ok(Some(json))
}

fn backup_info(
    _request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
    let mut board = BOARD_ACCESS.lock().unwrap();
    let header = board.get_backup_info();
    let json = match header {
        Ok(h) => {
            let timestamp = DateTime::from_timestamp_millis(h.timestamp).unwrap();
            let wbh = WebBackupHeader {
                timestamp: timestamp.to_rfc3339(),
                size: h.size,
            };
            serde_json::to_string(&wbh)?
        }
        Err(_) => {
            //TODO make better
            let wbh = WebBackupHeader {
                timestamp: "no backup".to_owned(),
                size: 0,
            };
            serde_json::to_string(&wbh)?
        }
    };
    anyhow::Ok(Some(json))
}

fn set_config(
    request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
    let all = read_up_to_bytes_from_request(request, Some(3072))?;
    let config: PlantControllerConfig = serde_json::from_slice(&all)?;
    let mut board = BOARD_ACCESS.lock().unwrap();
    board.set_config(&config)?;
    anyhow::Ok(Some("saved".to_owned()))
}

fn get_battery_state(
    _request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
    let mut board = BOARD_ACCESS.lock().unwrap();
    let battery_state = board.get_battery_state();
    let battery_json = serde_json::to_string(&battery_state)?;
    anyhow::Ok(Some(battery_json))
}

fn get_log(
    _request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
    let output = crate::log::get_log();
    anyhow::Ok(Some(serde_json::to_string(&output)?))
}

fn get_log_localization_config() -> Result<std::string::String, anyhow::Error> {
    anyhow::Ok(serde_json::to_string(
        &LogMessage::to_log_localisation_config(),
    )?)
}

fn get_version_web(
    _request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
    anyhow::Ok(Some(serde_json::to_string(&get_version())?))
}

fn pump_test(
    request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
    let actual_data = read_up_to_bytes_from_request(request, None)?;
    let pump_test: TestPump = serde_json::from_slice(&actual_data)?;
    let mut board = BOARD_ACCESS.lock().unwrap();
    board.test_pump(pump_test.pump)?;
    anyhow::Ok(None)
}

fn tank_info(
    _request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
    let mut board = BOARD_ACCESS.lock().unwrap();
    let config = board.get_config()?;
    let tank_info = determine_tank_state(&mut board, &config);
    //should be multsampled
    let water_temp = board.water_temperature_c();
    Ok(Some(serde_json::to_string(
        &tank_info.as_mqtt_info(&config.tank, &water_temp),
    )?))
}

fn night_lamp_test(
    request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
    let actual_data = read_up_to_bytes_from_request(request, None)?;
    let light_command: NightLampCommand = serde_json::from_slice(&actual_data)?;
    let mut board = BOARD_ACCESS.lock().unwrap();
    board.light(light_command.active)?;
    anyhow::Ok(None)
}

fn wifi_scan(
    _request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
    let mut board = BOARD_ACCESS.lock().unwrap();
    let scan_result = board.wifi_scan()?;
    let mut ssids: Vec<&String<32>> = Vec::new();
    scan_result.iter().for_each(|s| ssids.push(&s.ssid));
    let ssid_json = serde_json::to_string(&SSIDList { ssids })?;
    println!("Sending ssid list {}", &ssid_json);
    anyhow::Ok(Some(ssid_json))
}

fn list_files(
    _request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
    let board = BOARD_ACCESS.lock().unwrap();
    let result = board.list_files();
    let file_list_json = serde_json::to_string(&result)?;
    anyhow::Ok(Some(file_list_json))
}

fn ota(
    request: &mut Request<&mut EspHttpConnection>,
) -> Result<Option<std::string::String>, anyhow::Error> {
    let mut board = BOARD_ACCESS.lock().unwrap();
    let mut ota = OtaUpdate::begin()?;
    println!("start ota");

    //having a larger buffer is not really faster, requires more stack and prevents the progress bar from working ;)
    const BUFFER_SIZE: usize = 512;
    let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
    let mut total_read: usize = 0;
    let mut lastiter = 0;
    loop {
        let read = request.read(&mut buffer)?;
        total_read += read;
        let to_write = &buffer[0..read];

        let iter = (total_read / 1024) % 8;
        if iter != lastiter {
            for i in 0..PLANT_COUNT {
                let _ = board.fault(i, iter == i);
            }
            lastiter = iter;
        }

        ota.write(to_write)?;
        if read == 0 {
            break;
        }
    }
    println!("wrote bytes ota {total_read}");
    println!("finish ota");
    let partition = ota.raw_partition();
    println!("finalizing and changing boot partition to {partition:?}");

    let mut finalizer = ota.finalize()?;
    println!("changing boot partition");
    board.set_restart_to_conf(true);
    drop(board);
    finalizer.set_as_boot_partition()?;
    anyhow::Ok(None)
}

fn query_param(uri: &str, param_name: &str) -> Option<std::string::String> {
    println!("{uri} get {param_name}");
    let parsed = Url::parse(&format!("http://127.0.0.1/{uri}")).unwrap();
    let value = parsed.query_pairs().find(|it| it.0 == param_name);
    match value {
        Some(found) => Some(found.1.into_owned()),
        None => None,
    }
}

pub fn httpd(reboot_now: Arc<AtomicBool>) -> Box<EspHttpServer<'static>> {
    let server_config = Configuration {
        stack_size: 32768,
        ..Default::default()
    };
    let mut server: Box<EspHttpServer<'static>> =
        Box::new(EspHttpServer::new(&server_config).unwrap());
    server
        .fn_handler("/version", Method::Get, |request| {
            handle_error_to500(request, get_version_web)
        })
        .unwrap();
    server
        .fn_handler("/log", Method::Get, |request| {
            handle_error_to500(request, get_log)
        })
        .unwrap();
    server
        .fn_handler("/log_localization", Method::Get, |request| {
            cors_response(request, 200, &get_log_localization_config().unwrap())
        })
        .unwrap();
    server
        .fn_handler("/battery", Method::Get, |request| {
            handle_error_to500(request, get_battery_state)
        })
        .unwrap();
    server
        .fn_handler("/time", Method::Get, |request| {
            handle_error_to500(request, get_time)
        })
        .unwrap();
    server
        .fn_handler("/moisture", Method::Get, |request| {
            handle_error_to500(request, get_live_moisture)
        })
        .unwrap();
    server
        .fn_handler("/time", Method::Post, |request| {
            handle_error_to500(request, write_time)
        })
        .unwrap();
    server
        .fn_handler("/tank", Method::Get, |request| {
            handle_error_to500(request, tank_info)
        })
        .unwrap();
    server
        .fn_handler("/pumptest", Method::Post, |request| {
            handle_error_to500(request, pump_test)
        })
        .unwrap();
    server
        .fn_handler("/lamptest", Method::Post, |request| {
            handle_error_to500(request, night_lamp_test)
        })
        .unwrap();
    server
        .fn_handler("/boardtest", Method::Post, move |_| {
            BOARD_ACCESS.lock().unwrap().test()
        })
        .unwrap();
    server
        .fn_handler("/wifiscan", Method::Post, move |request| {
            handle_error_to500(request, wifi_scan)
        })
        .unwrap();
    server
        .fn_handler("/ota", Method::Post, |request| {
            handle_error_to500(request, ota)
        })
        .unwrap();
    server
        .fn_handler("/ota", Method::Options, |request| {
            cors_response(request, 200, "")
        })
        .unwrap();
    server
        .fn_handler("/get_config", Method::Get, move |request| {
            handle_error_to500(request, get_config)
        })
        .unwrap();
    server
        .fn_handler("/get_backup_config", Method::Get, move |request| {
            handle_error_to500(request, get_backup_config)
        })
        .unwrap();

    server
        .fn_handler("/set_config", Method::Post, move |request| {
            handle_error_to500(request, set_config)
        })
        .unwrap();
    server
        .fn_handler("/backup_config", Method::Post, move |request| {
            handle_error_to500(request, backup_config)
        })
        .unwrap();
    server
        .fn_handler("/backup_info", Method::Get, move |request| {
            handle_error_to500(request, backup_info)
        })
        .unwrap();
    server
        .fn_handler("/files", Method::Get, move |request| {
            handle_error_to500(request, list_files)
        })
        .unwrap();
    let reboot_now_for_reboot = reboot_now.clone();
    server
        .fn_handler("/reboot", Method::Post, move |_| {
            BOARD_ACCESS.lock().unwrap().set_restart_to_conf(true);
            reboot_now_for_reboot.store(true, std::sync::atomic::Ordering::Relaxed);
            anyhow::Ok(())
        })
        .unwrap();

    unsafe { vTaskDelay(1) };

    let reboot_now_for_exit = reboot_now.clone();
    server
        .fn_handler("/exit", Method::Post, move |_| {
            reboot_now_for_exit.store(true, std::sync::atomic::Ordering::Relaxed);
            anyhow::Ok(())
        })
        .unwrap();
    server
        .fn_handler("/file", Method::Get, move |request| {
            let filename = query_param(request.uri(), "filename").unwrap();
            let file_handle = BOARD_ACCESS
                .lock()
                .unwrap()
                .get_file_handle(&filename, false);
            match file_handle {
                Ok(mut file_handle) => {
                    let headers = [("Access-Control-Allow-Origin", "*")];
                    let mut response = request.into_response(200, None, &headers)?;
                    const BUFFER_SIZE: usize = 512;
                    let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
                    let mut total_read: usize = 0;
                    loop {
                        unsafe { vTaskDelay(1) };
                        let read = std::io::Read::read(&mut file_handle, &mut buffer)?;
                        total_read += read;
                        let to_write = &buffer[0..read];
                        response.write(to_write)?;
                        if read == 0 {
                            break;
                        }
                    }
                    println!("wrote {total_read} for file {filename}");
                    drop(file_handle);
                    response.flush()?;
                }
                Err(err) => {
                    //todo set headers here for filename to be error
                    let error_text = err.to_string();
                    println!("error handling get file {}", error_text);
                    cors_response(request, 500, &error_text)?;
                }
            }
            anyhow::Ok(())
        })
        .unwrap();
    server
        .fn_handler("/file", Method::Post, move |mut request| {
            let filename = query_param(request.uri(), "filename").unwrap();
            let lock = BOARD_ACCESS.lock().unwrap();
            let file_handle = lock.get_file_handle(&filename, true);
            match file_handle {
                //TODO get free filesystem size, check against during write if not to large
                Ok(mut file_handle) => {
                    const BUFFER_SIZE: usize = 512;
                    let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
                    let mut total_read: usize = 0;
                    let mut lastiter = 0;
                    loop {
                        let iter = (total_read / 1024) % 8;
                        if iter != lastiter {
                            for i in 0..PLANT_COUNT {
                                let _ = lock.fault(i, iter == i);
                            }
                            lastiter = iter;
                        }

                        let read = request.read(&mut buffer)?;
                        total_read += read;
                        let to_write = &buffer[0..read];
                        std::io::Write::write(&mut file_handle, to_write)?;
                        if read == 0 {
                            break;
                        }
                    }
                    cors_response(request, 200, &format!("saved {total_read} bytes"))?;
                }
                Err(err) => {
                    //todo set headers here for filename to be error
                    let error_text = err.to_string();
                    println!("error handling get file {}", error_text);
                    cors_response(request, 500, &error_text)?;
                }
            }
            drop(lock);
            anyhow::Ok(())
        })
        .unwrap();

    server
        .fn_handler("/file", Method::Delete, move |request| {
            let filename = query_param(request.uri(), "filename").unwrap();
            let copy = filename.clone();
            let board = BOARD_ACCESS.lock().unwrap();
            match board.delete_file(&filename) {
                Ok(_) => {
                    let info = format!("Deleted file {copy}");
                    cors_response(request, 200, &info)?;
                }
                Err(err) => {
                    let info = format!("Could not delete file {copy} {err:?}");
                    cors_response(request, 400, &info)?;
                }
            }
            anyhow::Ok(())
        })
        .unwrap();
    server
        .fn_handler("/file", Method::Options, |request| {
            cors_response(request, 200, "")
        })
        .unwrap();
    unsafe { vTaskDelay(1) };
    server
        .fn_handler("/", Method::Get, move |request| {
            let mut response = request.into_ok_response()?;
            response.write(include_bytes!("index.html"))?;
            anyhow::Ok(())
        })
        .unwrap();
    server
        .fn_handler("/favicon.ico", Method::Get, |request| {
            request
                .into_ok_response()?
                .write(include_bytes!("favicon.ico"))?;
            anyhow::Ok(())
        })
        .unwrap();
    server
        .fn_handler("/bundle.js", Method::Get, |request| {
            request
                .into_ok_response()?
                .write(include_bytes!("bundle.js"))?;
            anyhow::Ok(())
        })
        .unwrap();
    server
        .fn_handler("/timezones", Method::Get, move |request| {
            handle_error_to500(request, get_timezones)
        })
        .unwrap();
    server
}

fn cors_response(
    request: Request<&mut EspHttpConnection>,
    status: u16,
    body: &str,
) -> Result<(), anyhow::Error> {
    let headers = [
        ("Access-Control-Allow-Origin", "*"),
        ("Access-Control-Allow-Headers", "*"),
        ("Access-Control-Allow-Methods", "*"),
    ];
    let mut response = request.into_response(status, None, &headers)?;
    response.write(body.as_bytes())?;
    response.flush()?;
    anyhow::Ok(())
}

type AnyhowHandler =
    fn(&mut Request<&mut EspHttpConnection>) -> Result<Option<std::string::String>, anyhow::Error>;
fn handle_error_to500(
    mut request: Request<&mut EspHttpConnection>,
    chain: AnyhowHandler,
) -> Result<(), anyhow::Error> {
    let error = chain(&mut request);
    match error {
        Ok(answer) => match answer {
            Some(json) => {
                cors_response(request, 200, &json)?;
            }
            None => {
                cors_response(request, 200, "")?;
            }
        },
        Err(err) => {
            let error_text = err.to_string();
            println!("error handling process {}", error_text);
            cors_response(request, 500, &error_text)?;
        }
    }
    anyhow::Ok(())
}

fn read_up_to_bytes_from_request(
    request: &mut Request<&mut EspHttpConnection<'_>>,
    limit: Option<usize>,
) -> Result<Vec<u8>, anyhow::Error> {
    let max_read = limit.unwrap_or(1024);
    let mut data_store = Vec::new();
    let mut total_read = 0;
    loop {
        let mut buf = [0_u8; 64];
        let read = request.read(&mut buf)?;
        if read == 0 {
            break;
        }
        let actual_data = &buf[0..read];
        total_read += read;
        if total_read > max_read {
            bail!("Request too large {total_read} > {max_read}");
        }
        data_store.push(actual_data.to_owned());
    }
    let allvec = data_store.concat();
    println!("Raw data {}", from_utf8(&allvec)?);
    Ok(allvec)
}
