use std::{sync::mpsc::Sender, time::Duration};

use anyhow::{bail, Context};
use aprs_parser::{AprsData, AprsPacket};
use base64::{engine::general_purpose, Engine};
use kiss_tnc::Tnc;
use sha3::{Digest, Sha3_256};

#[derive(Debug)]
enum Command {
    ReqImageId(u8),
    BurstBalloon,
    CutPayload,
}

impl Command {
    pub fn decode(msg: &[u8]) -> Option<Self> {
        match msg.first()? {
            b'R' => {
                let id = std::str::from_utf8(msg.get(1..)?).ok()?.parse().ok()?;

                Some(Self::ReqImageId(id))
            }
            b'B' if msg.len() == 1 => Some(Self::BurstBalloon),
            b'C' if msg.len() == 1 => Some(Self::CutPayload),
            _ => None,
        }
    }
}

// handles commands from APRS
pub struct CommandHandler {
    callsign: String,

    // the minimum id the command must have. Used to prevent replay attacks
    min_id: u64,
    // used for the cryptographic signature. Allows us to ignore commands from unknown sources
    secret: String,

    burst_command: String,
    cutdown_command: String,
    img_request: Sender<u8>,
}

impl CommandHandler {
    pub fn new(
        callsign: String,
        secret: String,
        burst_command: String,
        cutdown_command: String,
        img_request: Sender<u8>,
    ) -> Self {
        Self {
            callsign,
            min_id: 0,
            secret,

            burst_command,
            cutdown_command,
            img_request,
        }
    }

    pub fn process_forever(&self) {
        tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .unwrap()
            .block_on(async {
                loop {
                    if let Err(e) = self.connect_and_process().await {
                        eprintln!("{}", e);
                    }
                    tokio::time::sleep(Duration::from_secs(1)).await;
                }
            });
    }

    async fn connect_and_process(&self) -> anyhow::Result<()> {
        // TODO don't hardcode this
        let mut tnc = Tnc::connect_tcp("localhost:8001").await?;

        let (_, data) = tnc.read_frame().await?;

        if let Ok(packet) = AprsPacket::decode_ax25(&data) {
            self.process_packet(&packet);
        }

        Ok(())
    }

    fn process_packet(&self, packet: &AprsPacket) {
        if let AprsData::Message(msg) = &packet.data {
            if msg.addressee == self.callsign.as_bytes() {
                if let Err(e) = self.process_msg(&msg.text) {
                    eprintln!("Ignoring APRS packet: {}", e);
                }
            }
        }
    }

    fn process_msg(&self, msg: &[u8]) -> anyhow::Result<()> {
        let msg = String::from_utf8(msg.to_vec())?;

        let mut iter = msg.split('|');

        let cmd = iter.next().context("Missing command")?;
        let id: u64 = iter
            .next()
            .context("Missing id")?
            .parse()
            .context("Invalid id")?;
        let hash_actual = general_purpose::STANDARD
            .decode(iter.next().context("Missing hash")?)
            .context("Invalid hash")?;

        if hash_actual != self.hash(cmd, id) {
            bail!("Invalid signature");
        }

        if id <= self.min_id {
            bail!("Given ID too small");
        }

        let cmd = Command::decode(cmd.as_bytes()).context("Invalid command")?;

        match cmd {
            Command::ReqImageId(id) => self.img_request.send(id)?,
            Command::BurstBalloon => {
                let (_, stderr) = subprocess::Exec::shell(&self.burst_command)
                    .communicate()?
                    .read()?;

                if let Some(stderr) = stderr {
                    if !stderr.is_empty() {
                        eprintln!("{}", String::from_utf8_lossy(&stderr));
                    }
                }
            }
            Command::CutPayload => {
                let (_, stderr) = subprocess::Exec::shell(&self.cutdown_command)
                    .communicate()?
                    .read()?;

                if let Some(stderr) = stderr {
                    if !stderr.is_empty() {
                        eprintln!("{}", String::from_utf8_lossy(&stderr));
                    }
                }
            }
        }

        Ok(())
    }

    fn hash(&self, cmd: &str, id: u64) -> [u8; 16] {
        let mut hasher = Sha3_256::new();
        hasher.update(cmd);
        hasher.update(b"|");
        hasher.update(id.to_be_bytes());
        hasher.update(&self.secret);

        // safe to unwrap because we know 16=16,
        // even if the compiler doesn't!
        hasher.finalize()[0..16].try_into().unwrap()
    }
}