use super::{
    configuration,
    constants::SERIAL_BUFFER_SIZE,
    helpers::{
        hp_is_connected, initialize_hp_config, open_serial_port, process_hp_message,
        save_hp_config, send_response_to_api,
    },
    types::{HPSendingFrame, HeatPumpReceiver, HeatPumpResponse, HeatPumpSender, SerialBuffer},
};
use crate::APP_STATE;
use log::{debug, error, info, warn};
use serialport::ClearBuffer;
use std::{env, io, time::Duration};
use tokio::select;

pub async fn run(mut channel: (HeatPumpSender, HeatPumpReceiver)) {
    let port: String = match env::var("SERIAL_PORT") {
        Ok(custom_port) => custom_port,
        Err(_) => configuration::SERIAL_PORT.to_string(),
    };

    // Initialize serial port
    let mut port = match open_serial_port(
        &port,
        configuration::BAUD_RATE,
        configuration::DATA_BITS,
        configuration::STOP_BITS,
        configuration::PARITY,
    ) {
        Ok(port) => port,
        Err(e) => {
            error!("Failed to open serial port: {}", e);
            std::process::exit(1);
        }
    };

    port.clear(ClearBuffer::All)
        .expect("Failed to clear serial port buffer");

    initialize_hp_config();

    let heat_pump = async move {
        let mut serial_buf: SerialBuffer = vec![0; SERIAL_BUFFER_SIZE];
        // Timer for checking if heat pump connection is dead or alive
        // if heap pump doesn't response in 120 seconds then inform user
        let mut timer = std::time::Instant::now();
        let mut periodical_sending: u8 = 0;
        let mut first_message_succes: bool = true;
        loop {
            select! {
                Some((frame, service_responder)) = channel.1.recv() => {
                    // Obtain message to send to heat pump
                    let buffer_to_send: SerialBuffer = match frame {
                        HPSendingFrame::First => APP_STATE.lock().unwrap().hp_config.create_first_frame(),
                        HPSendingFrame::Second => APP_STATE.lock().unwrap().hp_config.create_second_frame(),
                        HPSendingFrame::Third => APP_STATE.lock().unwrap().hp_config.create_third_frame(),
                    };
                    // If message is sent from other service then send also save the config
                    if service_responder.is_some() {
                        save_hp_config();
                    }
                    // Send message to heat pump
                    match port.write(buffer_to_send.as_slice()) {
                        Ok(written_bytes) => {
                            if written_bytes == buffer_to_send.len() {
                                debug!("Data frame (function code: {:#x}) sent to heat pump", frame);
                            } else {
                                error!("Could not write all bytes to serial port");
                            }
                        },
                        Err(e) => {
                            error!("Error while writing to serial port: {}", e);
                            send_response_to_api(service_responder, HeatPumpResponse::Error);
                            continue;
                        },
                    };

                    // Wait for response from heat pump, if there is no response in 500ms
                    // then send (optiional: timeout response to API) repeat sending message
                    // to establish connection with heat pump
                    match port.read_exact(serial_buf.as_mut_slice()) {
                        Ok(_) =>  {
                            {
                                // Check if the connection with heat pump is established or not
                                // If not then compare received and sent messages to initialize connection
                                // If messages are equal then connection is established
                                let mut lock = APP_STATE.lock().unwrap();
                                if !lock.hp_state.established_connection {
                                    if serial_buf == buffer_to_send {
                                        lock.hp_state.established_connection = true;
                                        timer = std::time::Instant::now();
                                        first_message_succes = true;
                                        info!("Heat pump connection established");
                                    } else {
                                        warn!("Received invalid intialization message. Could not initialize heat pump connection");
                                    }
                                    continue;
                                }
                            }
                            // Reset timer and allow sending repeated message when receive message
                            timer = std::time::Instant::now();
                            if !first_message_succes {
                                port.set_timeout(Duration::from_millis(configuration::TIMEOUT)).unwrap();
                                first_message_succes = true;
                            }

                            if !process_hp_message(serial_buf.clone()) {
                                warn!("Could not process data frame: {:?}. Check if the data frame is valid.", serial_buf);
                                send_response_to_api(service_responder, HeatPumpResponse::Error);
                            } else {
                                send_response_to_api(service_responder, HeatPumpResponse::Success);
                            }
                        },
                        Err(e) => match e.kind() {
                            io::ErrorKind::TimedOut => {
                                send_response_to_api(service_responder, HeatPumpResponse::Timeout);
                                // If didn't receive message from heat pump in config::TIMEOUT (500ms) then send
                                // message again and wait config::MAXIMUM_RESPONSE_TIME (120 seconds) for response.
                                // If didn't receive message from heat pump in config::MAXIMUM_RESPONSE_TIME then there is no connection
                                if first_message_succes {
                                    port.set_timeout(Duration::from_secs(configuration::MAXIMUM_RESPONSE_TIME)).unwrap();
                                    first_message_succes = false;
                                } else {
                                    hp_is_connected(timer.elapsed().as_secs());
                                    port.set_timeout(Duration::from_millis(configuration::TIMEOUT)).unwrap();
                                }
                                // Resend message to trigger sending message to heat pump one more time
                                if channel.0.send((frame, None)).await.is_ok() {
                                    debug!("Serial port read timed out, sending message again");
                                }
                                tokio::time::sleep(Duration::from_micros(1)).await;
                            },
                            _ => {
                                error!("Error while reading from serial port: {}", e);
                                send_response_to_api(service_responder, HeatPumpResponse::Error);
                                hp_is_connected(timer.elapsed().as_secs());
                            },
                        },
                  }
                },
                _ = tokio::time::sleep(Duration::from_secs(10)) => {
                    if !first_message_succes {
                        continue;
                    }
                    // If there are no incoming request to change config, then periodicaly
                    // send messages to heat pump to check if it is alive and obtain data
                    let frame = match periodical_sending % 3 {
                        0 => {
                            periodical_sending = 0;
                            HPSendingFrame::First
                        },
                        1 => HPSendingFrame::Second,
                        2 => HPSendingFrame::Third,
                        _ => unreachable!(),
                    };
                    periodical_sending += 1;
                    match channel.0.send((frame, None)).await {
                        Ok(_) => {},
                        Err(e) => error!("Error while sending periodical message: {}", e),
                    };
                }
            }
        }
    };

    tokio::task::spawn(heat_pump);
}
