//! IPC helpers for control socket

pub use falcorn_proto::control::*;

use crate::errors::{FalcornError, Result};
use std::io::{Read, Write};
use std::os::fd::AsRawFd;
use std::os::unix::net::UnixStream;

pub fn parse_control_acl_mode(mode: &str) -> Result<ControlAclMode> {
    match mode.to_lowercase().as_str() {
        "off" => Ok(ControlAclMode::Off),
        "same_user" => Ok(ControlAclMode::SameUser),
        other => Err(FalcornError::runtime(format!(
            "Invalid control_acl_mode '{}': expected 'off' or 'same_user'",
            other
        ))),
    }
}

pub fn write_typed_frame<T: serde::Serialize>(
    stream: &mut UnixStream,
    frame_type: ControlFrameType,
    payload: &T,
) -> Result<()> {
    let body = bincode::serialize(payload)
        .map_err(|e| FalcornError::runtime(format!("Control serialize error: {}", e)))?;
    if body.len() > CONTROL_MAX_PAYLOAD_LEN {
        return Err(FalcornError::runtime("Control payload too large"));
    }

    let mut header = [0u8; CONTROL_FRAME_HEADER_LEN];
    header[0..4].copy_from_slice(&CONTROL_FRAME_MAGIC);
    header[4] = CONTROL_PROTOCOL_VERSION;
    header[5] = frame_type as u8;
    header[6..8].copy_from_slice(&0u16.to_le_bytes());
    header[8..12].copy_from_slice(&(body.len() as u32).to_le_bytes());

    stream.write_all(&header).map_err(FalcornError::IoError)?;
    stream.write_all(&body).map_err(FalcornError::IoError)?;
    Ok(())
}

pub fn read_frame(stream: &mut UnixStream) -> Result<(ControlFrameType, Vec<u8>)> {
    let mut header = [0u8; CONTROL_FRAME_HEADER_LEN];
    stream
        .read_exact(&mut header)
        .map_err(FalcornError::IoError)?;

    if header[0..4] != CONTROL_FRAME_MAGIC {
        return Err(FalcornError::runtime("Invalid control frame magic"));
    }
    if header[4] != CONTROL_PROTOCOL_VERSION {
        return Err(FalcornError::runtime(
            "Unsupported control protocol version",
        ));
    }

    let frame_type = ControlFrameType::from_u8(header[5])
        .ok_or_else(|| FalcornError::runtime("Unknown control frame type"))?;
    let len = u32::from_le_bytes([header[8], header[9], header[10], header[11]]) as usize;

    if len > CONTROL_MAX_PAYLOAD_LEN {
        return Err(FalcornError::runtime("Control frame payload too large"));
    }

    let mut payload = vec![0u8; len];
    stream
        .read_exact(&mut payload)
        .map_err(FalcornError::IoError)?;

    Ok((frame_type, payload))
}

pub fn set_control_socket_permissions(path: &str, mode: u32) -> Result<()> {
    use std::os::unix::fs::PermissionsExt;

    let perms = std::fs::Permissions::from_mode(mode & 0o777);
    std::fs::set_permissions(path, perms).map_err(|e| {
        FalcornError::runtime(format!(
            "Failed to set control socket permissions ({:o}) on {}: {}",
            mode, path, e
        ))
    })
}

pub fn enforce_peer_acl(stream: &UnixStream, acl_mode: ControlAclMode) -> Result<()> {
    match acl_mode {
        ControlAclMode::Off => Ok(()),
        ControlAclMode::SameUser => {
            let peer_uid = peer_euid(stream)?;
            let local_uid = unsafe { libc::geteuid() as u32 };
            if peer_uid == local_uid {
                Ok(())
            } else {
                Err(FalcornError::runtime(format!(
                    "ACL denied: peer uid {} does not match server uid {}",
                    peer_uid, local_uid
                )))
            }
        }
    }
}

#[cfg(any(target_os = "linux", target_os = "android"))]
fn peer_euid(stream: &UnixStream) -> Result<u32> {
    let fd = stream.as_raw_fd();
    let mut cred = libc::ucred {
        pid: 0,
        uid: 0,
        gid: 0,
    };
    let mut len = std::mem::size_of::<libc::ucred>() as libc::socklen_t;

    let rc = unsafe {
        libc::getsockopt(
            fd,
            libc::SOL_SOCKET,
            libc::SO_PEERCRED,
            (&mut cred as *mut libc::ucred).cast(),
            &mut len,
        )
    };

    if rc != 0 {
        return Err(FalcornError::runtime(format!(
            "Failed to query SO_PEERCRED: {}",
            std::io::Error::last_os_error()
        )));
    }
    Ok(cred.uid as u32)
}

#[cfg(any(
    target_os = "macos",
    target_os = "ios",
    target_os = "freebsd",
    target_os = "openbsd",
    target_os = "netbsd",
    target_os = "dragonfly"
))]
fn peer_euid(stream: &UnixStream) -> Result<u32> {
    let fd = stream.as_raw_fd();
    let mut euid: libc::uid_t = 0;
    let mut egid: libc::gid_t = 0;

    let rc = unsafe { libc::getpeereid(fd, &mut euid, &mut egid) };
    if rc != 0 {
        return Err(FalcornError::runtime(format!(
            "Failed to query getpeereid: {}",
            std::io::Error::last_os_error()
        )));
    }

    Ok(euid as u32)
}

#[cfg(not(any(
    target_os = "linux",
    target_os = "android",
    target_os = "macos",
    target_os = "ios",
    target_os = "freebsd",
    target_os = "openbsd",
    target_os = "netbsd",
    target_os = "dragonfly"
)))]
fn peer_euid(_stream: &UnixStream) -> Result<u32> {
    Err(FalcornError::runtime(
        "Peer credential lookup is not supported on this OS",
    ))
}
