//! Implementation of the fg builtin.

use crate::builtins::error::Error;
use crate::fds::make_fd_blocking;
use crate::parser::ParserEnvSetMode;
use crate::reader::{reader_save_screen_state, reader_write_title};
use crate::tokenizer::tok_command;
use crate::{env::EnvMode, tty_handoff::TtyHandoff};
use crate::{err_fmt, err_str};
use fish_util::perror;
use libc::STDIN_FILENO;
use nix::sys::termios::{self, tcsetattr};
use std::os::fd::BorrowedFd;

use super::prelude::*;

/// Builtin for putting a job in the foreground.
pub fn fg(parser: &mut Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> BuiltinResult {
    let opts = HelpOnlyCmdOpts::parse(argv, parser, streams)?;

    let Some(&cmd) = argv.first() else {
        return Err(STATUS_INVALID_ARGS);
    };

    if opts.print_help {
        builtin_print_help(parser, streams, cmd);
        return Ok(SUCCESS);
    }

    let job;
    let job_pos;
    let optind = opts.optind;
    if optind == argv.len() {
        // Select last constructed job (i.e. first job in the job queue) that can be brought
        // to the foreground.
        let jobs = parser.jobs();
        match jobs.iter().enumerate().find(|(_pos, job)| {
            job.is_constructed()
                && !job.is_completed()
                && ((job.is_stopped() || !job.is_foreground()) && job.wants_job_control())
        }) {
            None => {
                err_str!(Error::NO_SUITABLE_JOBS).cmd(cmd).finish(streams);
                return Err(STATUS_INVALID_ARGS);
            }
            Some((pos, j)) => {
                job_pos = Some(pos);
                job = Some(j.clone());
            }
        }
    } else if optind + 1 < argv.len() {
        // Specifying more than one job to put to the foreground is a syntax error, we still
        // try to locate the job $argv[1], since we need to determine which error message to
        // emit (ambiguous job specification vs malformed job ID).
        let mut found_job = false;
        if let Ok(pid) = parse_pid(streams, cmd, argv[optind]) {
            found_job = parser.job_get_from_pid(pid).is_some();
        }

        let err = if found_job {
            err_str!("Ambiguous job")
        } else {
            err_fmt!("'%s' is not a job", argv[optind])
        };
        err.cmd(cmd).full_trailer(parser).finish(streams);

        job_pos = None;
        job = None;
    } else {
        match parse_pid(streams, cmd, argv[optind]) {
            Ok(pid) => {
                let j = parser.job_get_with_index_from_pid(pid);
                if j.as_ref()
                    .is_none_or(|(_pos, j)| !j.is_constructed() || j.is_completed())
                {
                    err_fmt!("No suitable job: %d", pid)
                        .cmd(cmd)
                        .finish(streams);
                    job_pos = None;
                    job = None;
                } else {
                    let (pos, j) = j.unwrap();
                    job_pos = Some(pos);
                    job = if !j.wants_job_control() {
                        err_fmt!("Can't put job %d, '%s' to foreground because it is not under job control",
                                        pid,
                                        j.command()
                                    )
                    .cmd(cmd)
                    .finish(streams);
                        None
                    } else {
                        Some(j)
                    };
                }
            }
            Err(_err) => {
                job_pos = None;
                job = None;
                builtin_print_error_trailer(parser, streams.err, cmd);
            }
        }
    }

    let Some(job) = job else {
        return Err(STATUS_INVALID_ARGS);
    };
    let job_pos = job_pos.unwrap();

    if streams.err_is_redirected {
        streams
            .err
            .appendln(&wgettext_fmt!(FG_MSG, job.job_id(), job.command()));
    } else {
        // If we aren't redirecting, send output to real stderr, since stuff in sb_err won't get
        // printed until the command finishes.
        eprintf!("%s\n", wgettext_fmt!(FG_MSG, job.job_id(), job.command()));
    }

    let ft = tok_command(job.command());
    if !ft.is_empty() {
        // Provide value for `status current-command`
        parser.libdata_mut().status_vars.command = ft.clone();
        // Also provide a value for the deprecated fish 2.0 $_ variable
        parser.set_var_and_fire(L!("_"), ParserEnvSetMode::new(EnvMode::EXPORT), vec![ft]);
        // Provide value for `status current-commandline`
        parser.libdata_mut().status_vars.commandline = job.command().to_owned();
    }
    reader_write_title(job.command(), parser, true);

    // Note if tty transfer fails, we still try running the job.
    parser.job_promote_at(job_pos);
    let mut handoff = TtyHandoff::new(reader_save_screen_state);
    let _ = make_fd_blocking(STDIN_FILENO);
    {
        let job_group = job.group();
        job_group.set_is_foreground(true);
        if job.entitled_to_terminal() {
            handoff.disable_tty_protocols();
        }
        let tmodes = job_group.tmodes.borrow();
        if job_group.wants_terminal() && tmodes.is_some() {
            let tmodes = tmodes.as_ref().unwrap();
            if tcsetattr(
                unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) },
                termios::SetArg::TCSADRAIN,
                tmodes,
            )
            .is_err()
            {
                perror("tcsetattr");
            }
        }
    }
    handoff.to_job_group(job.group.as_ref().unwrap());
    let resumed = job.resume();
    if resumed {
        job.continue_job(parser, /*block_io=*/ None);
    }
    if job.is_stopped() {
        handoff.save_tty_modes();
    }
    if resumed {
        Ok(SUCCESS)
    } else {
        Err(STATUS_CMD_ERROR)
    }
}
