// SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
// SPDX-License-Identifier: BSD-2-Clause
//! Provide a wrapper around a "naked" exitcode or an error report.

use eyre::{Report, eyre};
use sysexits::ExitCode;

use crate::defs::Signal;

/// The wrapped error types that we support.
#[derive(Debug)]
#[non_exhaustive]
pub enum WrappedReport {
    /// A plain exit status, no messages.
    Exact(u8),

    /// An `eyre` error report.
    Eyre(Report),

    /// Terminated by a signal, no messages.
    Signal(Signal),
}

impl From<u8> for WrappedReport {
    #[inline]
    fn from(exitcode: u8) -> Self {
        Self::Exact(exitcode)
    }
}

impl From<ExitCode> for WrappedReport {
    #[inline]
    fn from(exitcode: ExitCode) -> Self {
        Self::Exact(exitcode.into())
    }
}

impl From<Report> for WrappedReport {
    #[inline]
    fn from(report: Report) -> Self {
        Self::Eyre(report)
    }
}

impl From<Signal> for WrappedReport {
    #[inline]
    fn from(sig: Signal) -> Self {
        Self::Signal(sig)
    }
}

/// The top-level exit status of the program: the exit code along with
/// an optional error message.
#[derive(Debug)]
pub struct ExitCodeReport {
    /// The exit code to return.
    exitcode: u8,

    /// Possibly an error to report.
    report: WrappedReport,
}

impl ExitCodeReport {
    /// The exit code to return.
    #[inline]
    #[must_use]
    pub const fn exitcode(&self) -> u8 {
        self.exitcode
    }

    /// Possibly an error to report.
    #[inline]
    #[must_use]
    pub const fn report(&self) -> &WrappedReport {
        &self.report
    }

    /// Process a signal value.
    #[inline]
    #[must_use]
    pub fn from_raw_signal(value: i32) -> Self {
        Signal::from_raw(value).map_or_else(
            |_| Self {
                exitcode: ExitCode::OsErr.into(),
                report: eyre!("The child process was terminated by unknown signal {value}").into(),
            },
            |sig| {
                value.checked_add(128_i32).map_or_else(
                    || Self {
                        exitcode: ExitCode::OsErr.into(),
                        report: eyre!("Could not add 128 to {value}").into(),
                    },
                    |added| {
                        u8::try_from(added).map_or_else(
                            |_| Self {
                                exitcode: ExitCode::OsErr.into(),
                                report: eyre!("Exit code {added} for signal {value} out of range")
                                    .into(),
                            },
                            |exitcode| Self {
                                exitcode,
                                report: sig.into(),
                            },
                        )
                    },
                )
            },
        )
    }
}

impl From<u8> for ExitCodeReport {
    #[inline]
    fn from(exitcode: u8) -> Self {
        Self {
            exitcode,
            report: WrappedReport::Exact(exitcode),
        }
    }
}

impl From<i32> for ExitCodeReport {
    #[inline]
    fn from(exitcode: i32) -> Self {
        u8::try_from(exitcode).map_or_else(
            |_| Self {
                exitcode: ExitCode::OsErr.into(),
                report: WrappedReport::Eyre(eyre!("Command exit code out of bounds: {exitcode}")),
            },
            |clamped| Self {
                exitcode: clamped,
                report: WrappedReport::Exact(clamped),
            },
        )
    }
}

impl From<ExitCode> for ExitCodeReport {
    #[inline]
    fn from(exitcode: ExitCode) -> Self {
        Self {
            exitcode: exitcode.into(),
            report: WrappedReport::Exact(exitcode.into()),
        }
    }
}

impl<T: Into<WrappedReport>> From<(ExitCode, T)> for ExitCodeReport {
    #[inline]
    fn from((exitcode, report): (ExitCode, T)) -> Self {
        Self {
            exitcode: exitcode.into(),
            report: report.into(),
        }
    }
}

impl From<Report> for ExitCodeReport {
    #[inline]
    fn from(report: Report) -> Self {
        Self {
            exitcode: ExitCode::Software.into(),
            report: report.into(),
        }
    }
}
