use core::{
    mem::size_of,
    net::{IpAddr, Ipv4Addr},
};

use yggdrasil_abi::{
    error::Error,
    net::{
        protocols::{IcmpV4Frame, InetChecksum, IpProtocol},
        types::NetValueImpl,
        SubnetAddr, SubnetV4Addr,
    },
};

use crate::{
    l3::{self, Route},
    L3Packet,
};

async fn send_v4_reply(
    destination_ip: Ipv4Addr,
    icmp_frame: &IcmpV4Frame,
    icmp_data: &[u8],
) -> Result<(), Error> {
    let mut reply_frame = IcmpV4Frame {
        ty: 0,
        code: 0,
        checksum: u16::to_network_order(0),
        rest: icmp_frame.rest,
    };

    if icmp_data.len() % 2 != 0 {
        return Err(Error::InvalidArgument);
    }

    let l4_bytes = bytemuck::bytes_of(&reply_frame);
    let mut checksum = InetChecksum::new();
    checksum.add_bytes(l4_bytes, true);
    checksum.add_bytes(icmp_data, true);

    reply_frame.checksum = checksum.finish().to_network_order();

    l3::send_l4_ip(&l3::L4UnresolvedPacket {
        destination_ip: IpAddr::V4(destination_ip),
        l4_frame: bytemuck::bytes_of(&reply_frame),
        l4_options: &[],
        l4_data: icmp_data,
        protocol: IpProtocol::ICMP,
        ttl: 255,
    })
    .await
}

async fn handle_v4(source_address: Ipv4Addr, l3_packet: L3Packet) -> Result<(), Error> {
    if l3_packet.data_length < size_of::<IcmpV4Frame>() {
        log::debug!("Truncated ICMPv4 packet");
        return Err(Error::MissingData);
    }

    if l3_packet.data_length - size_of::<IcmpV4Frame>() > 576 {
        log::debug!("ICMPv4 packet too large");
        return Err(Error::MissingData);
    }

    let l3_data = l3_packet.l3_data();
    let icmp_frame: &IcmpV4Frame = bytemuck::from_bytes(&l3_data[..size_of::<IcmpV4Frame>()]);
    let icmp_data = &l3_data[size_of::<IcmpV4Frame>()..l3_packet.data_length];

    match (icmp_frame.ty, icmp_frame.code) {
        (8, 0) => send_v4_reply(source_address, icmp_frame, icmp_data).await,
        // ICMP redirect to host
        (5, 1) => {
            // TODO don't just accept whatever gateway is supplied, it's a MitM asking to happen
            let gateway = Ipv4Addr::from(u32::from_network_order(icmp_frame.rest));
            log::warn!("ICMP redirect to host: {gateway}");
            Route::insert(Route {
                priority: 5,
                interface: l3_packet.interface_id,
                gateway: Some(IpAddr::V4(gateway)),
                subnet: SubnetAddr::V4(SubnetV4Addr::from_address_mask(gateway, 32)),
            })
            .ok();
            Ok(())
        }
        (0, 0) => Ok(()),
        _ => {
            log::debug!(
                "Ignoring unknown ICMPv4 type:code: {}:{}",
                icmp_frame.ty,
                icmp_frame.code
            );
            Ok(())
        }
    }
}

pub async fn handle(l3_packet: L3Packet) -> Result<(), Error> {
    match l3_packet.source_address {
        IpAddr::V4(v4) => handle_v4(v4, l3_packet).await,
        IpAddr::V6(v6) => {
            log::warn!("TODO: ICMPv6 from {v6}");
            Err(Error::NotImplemented)
        }
    }
}
