#![feature(yggdrasil_os, never_type)]

use std::{
    fmt,
    fs::File,
    io::{self, BufRead, BufReader},
    path::Path,
    process::{Command, ExitCode, Stdio},
    str::FromStr,
};

use cross::net::LocalPacketSocket;
use init::InitMsg;

const INITTAB_PATH: &str = "/etc/inittab";

pub enum InitError {
    IoError(std::io::Error),
    OsError(yggdrasil_rt::Error),
    CustomError(String),
}

impl From<std::io::Error> for InitError {
    fn from(value: std::io::Error) -> Self {
        Self::IoError(value)
    }
}

impl From<yggdrasil_rt::Error> for InitError {
    fn from(value: yggdrasil_rt::Error) -> Self {
        Self::OsError(value)
    }
}

impl fmt::Debug for InitError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::IoError(e) => fmt::Debug::fmt(e, f),
            Self::OsError(e) => fmt::Debug::fmt(e, f),
            Self::CustomError(e) => fmt::Debug::fmt(e, f),
        }
    }
}

#[derive(Debug, PartialEq)]
pub enum RuleAction {
    Wait,
    Once,
    Boot,
}

#[derive(Debug)]
pub struct Rule {
    #[allow(unused)]
    id: String,
    // runlevels ignored here
    action: RuleAction,
    program: String,
    arguments: Vec<String>,
}

impl Rule {
    pub fn run(&self) -> Result<(), InitError> {
        let arguments: Vec<_> = self.arguments.iter().map(String::as_str).collect();
        let mut child = match self.action {
            RuleAction::Wait | RuleAction::Once => Command::new(&self.program)
                .args(&arguments)
                .stdin(Stdio::null())
                .stdout(Stdio::null())
                .stderr(Stdio::null())
                .spawn()?,
            RuleAction::Boot => todo!(),
        };

        if self.action == RuleAction::Wait {
            child.wait()?;
        }

        Ok(())
    }
}

impl FromStr for RuleAction {
    type Err = InitError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "wait" => Ok(Self::Wait),
            "once" => Ok(Self::Once),
            "boot" => Ok(Self::Boot),
            _ => Err(InitError::CustomError(format!(
                "Unrecognized rule action: {:?}",
                s
            ))),
        }
    }
}

fn parse_rule(line: &str) -> Result<Rule, InitError> {
    let terms: Vec<_> = line.split(':').collect();
    if terms.len() != 4 {
        return Err(InitError::CustomError(format!(
            "Expected 4 terms in init rule, got {}",
            terms.len()
        )));
    }

    let id = terms[0].to_owned();
    // Runlevel (term 1) ignored
    let action = RuleAction::from_str(terms[2])?;

    let command_terms: Vec<_> = terms[3].split(' ').collect();
    let program = command_terms[0].to_owned();
    let arguments = command_terms
        .into_iter()
        .skip(1)
        .map(str::to_owned)
        .collect();

    Ok(Rule {
        id,
        action,
        program,
        arguments,
    })
}

fn load_rules<P: AsRef<Path>>(path: P) -> Result<Vec<Rule>, InitError> {
    let file = BufReader::new(File::open(path)?);
    let mut rules = vec![];

    for line in file.lines() {
        let line = line?;
        let line = line.trim();
        let (line, _) = line.split_once('#').unwrap_or((line, ""));

        if line.is_empty() {
            continue;
        }

        rules.push(parse_rule(line)?);
    }

    Ok(rules)
}

fn handle_message(msg: InitMsg) -> io::Result<()> {
    match msg {
        InitMsg::StartService(init::StartService { binary, args }) => {
            Command::new(binary).args(args).spawn()?;

            Ok(())
        }
    }
}

fn main_loop(socket: LocalPacketSocket) -> io::Result<!> {
    let mut buf = [0; 1024];

    loop {
        let len = socket.receive(&mut buf)?;
        if let Ok(msg) = serde_json::from_slice::<InitMsg>(&buf[..len]) {
            if let Err(err) = handle_message(msg) {
                log::warn!("init::handle_message: {err}");
            }
        }
    }
}

fn main() -> ExitCode {
    logsink::setup_logging(false);
    log::info!("Userspace init starting");

    let rules = match load_rules(INITTAB_PATH) {
        Ok(s) => s,
        Err(e) => {
            log::error!("init: failed to load rules: {:?}", e);
            return ExitCode::FAILURE;
        }
    };

    //let channel = MessageChannel::open("service-control", true).unwrap();
    let control_socket = LocalPacketSocket::bind("/init.sock").unwrap();

    log::info!("Rules loaded");

    for rule in rules {
        log::info!("rc: {:?}", rule);
        if let Err(err) = rule.run() {
            log::error!("rc: failed to execute rule {:?}: {:?}", rule, err);
        }
    }

    let Err(error) = main_loop(control_socket);
    log::error!("init: main_loop returned {error}");
    ExitCode::FAILURE
}
