// Copyright 2026 The BoringSSL Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! TLS I/O model

use alloc::boxed::Box;
use core::{
    ffi::{CStr, c_char, c_int, c_long, c_void},
    fmt,
    ptr::NonNull,
    task::{Context, Waker},
};

use once_cell::sync::Lazy;

use crate::{
    abort_on_panic,
    errors::{Error, TlsRetryReason},
    ffi::{sanitise_mut_slice, sanitize_slice},
};

/// A wrapper around a `dyn AbstractSocket`, delegating BIO methods to the
/// underlying `AbstractSocket` implementations.
///
/// # Safety
///
/// [`RustBio`] can only be accessed through exclusive ownership.
// TODO(@xfding): switch to `derive(CoercePointee)` when it stabilises for flattening the layout.
pub(crate) struct RustBio {
    // We may need to propagate the waker, but nothing more.
    waker: Option<Waker>,
    pub read_eos: bool,
    pub write_eos: bool,
    socket: Option<Box<dyn AbstractSocket>>,
    reader: Option<Box<dyn AbstractReader>>,
    writer: Option<Box<dyn AbstractWriter>>,
    io_err: Option<Box<dyn core::error::Error + Send + Sync>>,
}

/// Safety: `socket` field is a exclusively owned `Box<dyn AbstractSocket>` pointer,
/// and `AbstractSocket: Send + Sync`.
unsafe impl Send for RustBio {}
unsafe impl Sync for RustBio {}

fn _assert_rust_bio()
where
    RustBio: Send + Unpin,
{
}

/// Purely module-internal implementation details
impl RustBio {
    fn init() -> Self {
        Self {
            waker: None,
            socket: None,
            reader: None,
            writer: None,
            read_eos: false,
            write_eos: false,
            io_err: None,
        }
    }

    pub fn attach_socket(&mut self, socket: Box<dyn AbstractSocket>) {
        self.socket = Some(socket);
    }

    pub fn attach_reader(&mut self, reader: Box<dyn AbstractReader>) {
        self.reader = Some(reader);
    }

    pub fn attach_writer(&mut self, writer: Box<dyn AbstractWriter>) {
        self.writer = Some(writer);
    }

    pub fn get_reader(&mut self) -> Option<&mut dyn AbstractReader> {
        if let Some(reader) = &mut self.reader {
            Some(&mut **reader)
        } else if let Some(socket) = &mut self.socket {
            Some(&mut **socket)
        } else {
            None
        }
    }

    pub fn get_writer(&mut self) -> Option<&mut dyn AbstractWriter> {
        if let Some(writer) = &mut self.writer {
            Some(&mut **writer)
        } else if let Some(socket) = &mut self.socket {
            Some(&mut **socket)
        } else {
            None
        }
    }
}

/// Crate-level `BIO` implementation details
impl RustBio {
    pub fn set_waker(&mut self, waker: &Waker) {
        if let Some(other_waker) = &self.waker
            && waker.will_wake(other_waker)
        {
            return;
        }
        self.waker = Some(waker.clone());
    }

    pub fn new_duplex<T: 'static + AbstractSocket + Sized>(
        socket: T,
    ) -> Result<RustBioHandle, Error> {
        let bio = unsafe {
            // Safety: the BIO_METH will be valid if this is the first call.
            bssl_sys::BIO_new(get_bio_method())
        };
        let bio = NonNull::new(bio).expect("allocation failure");
        unsafe {
            // Safety: `bio` was constructed with our own BIO_METH, so the data must be valid
            // as a `RustBio`
            rust_bio_data_mut(bio.as_ptr()).attach_socket(Box::new(socket));
        }
        Ok(RustBioHandle(bio))
    }

    pub fn new_split<R, W>(reader: R, writer: W) -> Result<RustBioHandle, Error>
    where
        R: 'static + AbstractReader + Sized,
        W: 'static + AbstractWriter + Sized,
    {
        let bio = unsafe {
            // Safety: the BIO_METH will be valid if this is the first call.
            bssl_sys::BIO_new(get_bio_method())
        };
        let bio = NonNull::new(bio).expect("allocation failure");
        let data = unsafe {
            // Safety: `bio` was constructed with our own BIO_METH, so the data must be valid
            // as a `RustBio`
            rust_bio_data_mut(bio.as_ptr())
        };
        data.attach_reader(Box::new(reader));
        data.attach_writer(Box::new(writer));
        Ok(RustBioHandle(bio))
    }

    fn transform_result(
        &mut self,
        res: AbstractSocketResult,
        reason_on_retry: TlsRetryReason,
    ) -> IoStatus {
        match res {
            AbstractSocketResult::Ok(bytes) => IoStatus::Ok(bytes),
            AbstractSocketResult::Retry => IoStatus::Retry(reason_on_retry),
            AbstractSocketResult::EndOfStream => IoStatus::EndOfStream,
            AbstractSocketResult::Err(e) => {
                self.io_err = Some(e);
                IoStatus::Err
            }
        }
    }
}

/// A exclusively owned handle to a BIO constructed by this crate.
pub(crate) struct RustBioHandle(NonNull<bssl_sys::BIO>);

impl RustBioHandle {
    pub fn ptr(&self) -> *mut bssl_sys::BIO {
        self.0.as_ptr()
    }

    pub fn as_mut(&mut self) -> &mut RustBio {
        unsafe {
            // Safety: `self` witnesses the validity of the handle.
            rust_bio_data_mut(self.ptr())
        }
    }

    pub fn as_ref(&self) -> &RustBio {
        unsafe {
            // Safety: `self` witnesses the validity of the handle.
            rust_bio_data(self.ptr())
        }
    }

    pub fn set_waker(&mut self, waker: &Waker) {
        self.as_mut().set_waker(waker);
    }
}

impl Drop for RustBioHandle {
    fn drop(&mut self) {
        unsafe {
            // Safety: the BIO handle should still be valid and was created by this crate.
            bssl_sys::BIO_free(self.0.as_ptr());
        }
    }
}

/// Safety: caller must ensure that `bio` is created with `rust_bio_create` and outlives `'a`
/// for exclusive access.
unsafe fn rust_bio_data_mut<'a>(bio: *mut bssl_sys::BIO) -> &'a mut RustBio {
    let data = unsafe {
        // Safety: `bio` is still valid
        bssl_sys::BIO_get_data(bio)
    };
    unsafe { &mut *(data as *mut RustBio) }
}

/// Safety: caller must ensure that `bio` is created with `rust_bio_create` and outlives `'a`
/// for shared access.
unsafe fn rust_bio_data<'a>(bio: *mut bssl_sys::BIO) -> &'a RustBio {
    let data = unsafe {
        // Safety: `bio` is still valid
        bssl_sys::BIO_get_data(bio)
    };
    unsafe { &*(data as *const RustBio) }
}

/// I/O Status of the possibly pending operation.
#[derive(Debug, Clone, Copy)]
pub enum IoStatus {
    /// Successfully performed I/O of bytes at certain size.
    Ok(usize),
    /// There is no more data to read or write.
    EndOfStream,
    /// I/O operation should be retried with the exactly same buffers when applicable.
    Retry(TlsRetryReason),
    /// There is no backing socket.
    Empty,
    /// I/O operation has failed.
    Err,
}

/// Result of operating an [`AbstractSocket`].
pub enum AbstractSocketResult {
    /// I/O completed by committing some amount of bytes.
    Ok(usize),
    /// I/O is pending completion; the I/O operation should be invoked again with the same parameter.
    Retry,
    /// I/O is impossible because the stream has ended.
    EndOfStream,
    /// I/O operation failed.
    Err(Box<dyn core::error::Error + Send + Sync>),
}

/// Abstract reader.
pub trait AbstractReader: Send + Sync + Unpin {
    /// Read data from the socket.
    fn read(
        &mut self,
        async_ctx: Option<&mut Context<'_>>,
        buffer: &mut [u8],
    ) -> AbstractSocketResult;
}

/// Abstract writer.
pub trait AbstractWriter: Send + Sync + Unpin {
    /// Write data to the socket.
    fn write(&mut self, async_ctx: Option<&mut Context<'_>>, buffer: &[u8])
    -> AbstractSocketResult;
    /// Flush the socket.
    fn flush(&mut self, async_ctx: Option<&mut Context<'_>>) -> AbstractSocketResult;
}

/// Abstract socket wrapper around Rust types that may support async I/O.
pub trait AbstractSocket: AbstractReader + AbstractWriter {}

// NOTE: this is not dead code, we are asserting that `dyn AbstractSocket` is a well-formed type,
// or `AbstractSocket` is dyn-compatible specifically.
fn _assert_dyn_compat()
where
    dyn AbstractSocket:,
{
}

fn get_bio_type() -> c_int {
    static BIO_TYPE: Lazy<c_int> = Lazy::new(|| {
        // Safety: this call does not have side-effect other than ID assignment
        unsafe { bssl_sys::BIO_get_new_index() }
    });
    *BIO_TYPE
}

struct BioMethod(*mut bssl_sys::BIO_METHOD);
/// Safety: once constructed this BIO vtable will stay immutable.
unsafe impl Send for BioMethod {}
/// Safety: once constructed this BIO vtable will stay immutable.
unsafe impl Sync for BioMethod {}

fn get_bio_method() -> *const bssl_sys::BIO_METHOD {
    static BIO_METHOD: Lazy<BioMethod> = Lazy::new(|| {
        let cstr = const {
            if let Ok(cstr) = CStr::from_bytes_with_nul(b"rust_bio\0") {
                cstr
            } else {
                // Compile-time assertion
                unreachable!()
            }
        };
        let vtable = unsafe {
            // Safety: this call does not have side-effect other than allocation.
            bssl_sys::BIO_meth_new(get_bio_type(), cstr.as_ptr())
        };
        unsafe {
            // Safety: all the following calls are simple assignments to the vtable entries.
            bssl_sys::BIO_meth_set_read(vtable, Some(rust_bio_read));
            bssl_sys::BIO_meth_set_write(vtable, Some(rust_bio_write));
            bssl_sys::BIO_meth_set_ctrl(vtable, Some(rust_bio_ctrl));
            bssl_sys::BIO_meth_set_create(vtable, Some(rust_bio_create));
            bssl_sys::BIO_meth_set_destroy(vtable, Some(rust_bio_destroy));
        }
        BioMethod(vtable)
    });
    BIO_METHOD.0
}

unsafe extern "C" fn rust_bio_read(
    bio: *mut bssl_sys::BIO,
    buffer: *mut c_char,
    len: c_int,
) -> c_int {
    let rust_bio = unsafe {
        // Safety: `bio` is still valid and so is the `RustBio` which we have exclusive access to.
        rust_bio_data_mut(bio)
    };
    if rust_bio.read_eos {
        return 0;
    }
    let Ok(len) = usize::try_from(len) else {
        return -1;
    };
    let waker = rust_bio.waker.clone();
    let mut async_ctx = if let Some(waker) = &waker {
        Some(Context::from_waker(waker))
    } else {
        None
    };
    // Zero the buffer now.
    // TODO(@xfding): maybe we want to have a buffer wrapper that tracks initialised region.
    let Some(buf) = (unsafe {
        // Safety: `buffer` is valid for holding `len` bytes by BoringSSL invariants.
        buffer.write_bytes(0, len);
        // Safety: `buffer` and `len` are sanitised and initialised for the right memory region.
        sanitise_mut_slice(buffer as *mut u8, len)
    }) else {
        return -1;
    };
    let work = {
        let Some(reader) = rust_bio.get_reader() else {
            return -1;
        };
        move || reader.read(async_ctx.as_mut(), buf)
    };
    let res = abort_on_panic(work);
    match rust_bio.transform_result(res, TlsRetryReason::WantRead) {
        IoStatus::Ok(bytes) => {
            if let Ok(bytes) = c_int::try_from(bytes) {
                return bytes;
            }
            -1
        }
        IoStatus::EndOfStream => {
            rust_bio.read_eos = true;
            -1
        }
        IoStatus::Retry(_) => {
            unsafe {
                // Safety: `bio` is still valid now.
                bssl_sys::BIO_set_retry_read(bio);
            }
            -1
        }
        IoStatus::Empty | IoStatus::Err => -1,
    }
}

unsafe extern "C" fn rust_bio_write(
    bio: *mut bssl_sys::BIO,
    buffer: *const c_char,
    len: c_int,
) -> c_int {
    let rust_bio = unsafe {
        // Safety: `bio` is still valid and so is the `RustBio` which we have exclusive access to.
        rust_bio_data_mut(bio)
    };
    if rust_bio.write_eos {
        return 0;
    }
    let Ok(len) = usize::try_from(len) else {
        return -1;
    };
    let waker = rust_bio.waker.clone();
    let mut async_ctx = if let Some(waker) = &waker {
        Some(Context::from_waker(waker))
    } else {
        None
    };
    let Some(buf) = (unsafe {
        // Safety: `buffer` and `len` are sanitised and initialised for the right memory region.
        sanitize_slice(buffer as *mut u8, len)
    }) else {
        return -1;
    };
    let work = {
        let Some(writer) = rust_bio.get_writer() else {
            return -1;
        };
        move || writer.write(async_ctx.as_mut(), buf)
    };
    let res = abort_on_panic(work);
    match rust_bio.transform_result(res, TlsRetryReason::WantWrite) {
        IoStatus::Ok(bytes) => {
            if let Ok(bytes) = c_int::try_from(bytes) {
                return bytes;
            }
            -1
        }
        IoStatus::EndOfStream => {
            rust_bio.write_eos = true;
            0
        }
        IoStatus::Retry(_) => {
            unsafe {
                // Safety: `bio` is still valid now.
                bssl_sys::BIO_set_retry_write(bio);
            }
            -1
        }
        IoStatus::Empty | IoStatus::Err => -1,
    }
}

unsafe fn rust_bio_flush(bio: *mut bssl_sys::BIO) -> c_long {
    let rust_bio = unsafe {
        // Safety: `bio` is still valid
        rust_bio_data_mut(bio)
    };
    if rust_bio.write_eos {
        return 0;
    }
    let waker = rust_bio.waker.clone();
    let mut async_ctx = if let Some(waker) = &waker {
        Some(Context::from_waker(waker))
    } else {
        None
    };
    let work = {
        let Some(writer) = rust_bio.get_writer() else {
            return -1;
        };
        move || writer.flush(async_ctx.as_mut())
    };
    let res = abort_on_panic(work);
    match rust_bio.transform_result(res, TlsRetryReason::WantWrite) {
        IoStatus::Ok(_) | IoStatus::Retry(_) => 1,
        IoStatus::EndOfStream => {
            rust_bio.write_eos = true;
            0
        }
        IoStatus::Empty | IoStatus::Err => 0,
    }
}

unsafe extern "C" fn rust_bio_ctrl(
    bio: *mut bssl_sys::BIO,
    ctrl: c_int,
    _: c_long,
    _: *mut c_void,
) -> c_long {
    match ctrl {
        bssl_sys::BIO_CTRL_FLUSH => unsafe {
            // Safety: `bio` is still valid.
            rust_bio_flush(bio)
        },
        _ => 0,
    }
}

unsafe extern "C" fn rust_bio_create(bio: *mut bssl_sys::BIO) -> c_int {
    let data = Box::new(RustBio::init());
    let data = Box::into_raw(data);
    unsafe {
        // Safety: both `bio` and `data` are still valid and exclusively owned.
        bssl_sys::BIO_set_data(bio, data as _);
        // Safety: it is now already safe to mark BIO initialised.
        bssl_sys::BIO_set_init(bio, 1);
    }
    1
}

unsafe extern "C" fn rust_bio_destroy(bio: *mut bssl_sys::BIO) -> c_int {
    let rust_bio = unsafe {
        // Safety: `bio` is still valid
        bssl_sys::BIO_get_data(bio) as *mut RustBio
    };
    let rust_bio = unsafe {
        // Safety: `rust_bio` is created from `rust_bio_create`
        Box::from_raw(rust_bio)
    };
    // Try to catch unwinding on the FFI boundary.
    // NOTE: it is not safe to drop the error value because its destructor can panic again.
    abort_on_panic(move || {
        let _ = rust_bio;
    });
    unsafe {
        // Safety: `bio` is still valid, we just need to signal that it is inactive.
        bssl_sys::BIO_set_init(bio, 0);
    }
    1
}

/// Asynchronous methods were invoked outside `async` context
#[derive(Debug)]
pub struct NoAsyncContext;

impl core::error::Error for NoAsyncContext {}

impl fmt::Display for NoAsyncContext {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("async method is called outside async context")
    }
}
