use std::{fmt, process::ExitStatus};
use log::*;
use nix::{
    sys::{
        signal::{self, Signal},
        termios::{self, Termios},
    },
    unistd::{self, Pid},
};
use thiserror::Error;
use super::{
    process::{Process, ProcessGroup, ProcessStatus},
    util,
};
use crate::log_if_err;
pub type pid_t = i32;
#[derive(Error, Debug)]
pub enum Error {
    #[error("no such job {0}")]
    NoSuchJob(String),
}
pub trait Job {
    fn id(&self) -> JobId;
    fn input(&self) -> String;
    fn display(&self) -> String;
    fn processes(&self) -> &Vec<Box<dyn Process>>;
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct JobId(pub u32);
impl fmt::Display for JobId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}
#[derive(Copy, Clone, Debug)]
pub enum JobStatus {
    Running,
    Stopped,
    Completed,
}
trait JobExt: Job {
    fn tmodes(&self) -> &Option<Termios>;
    fn status(&self) -> JobStatus;
}
trait AsJob {
    fn as_job(&self) -> &dyn Job;
}
impl<T: Job> AsJob for T {
    fn as_job(&self) -> &dyn Job {
        self
    }
}
#[derive(Default)]
pub struct JobManager {
    jobs: Vec<JobImpl>,
    job_count: u32,
    current_job: Option<JobId>,
}
impl JobManager {
    pub fn create_job(&mut self, input: &str, process_group: ProcessGroup) -> JobId {
        let job_id = self.get_next_job_id();
        self.jobs.push(JobImpl::new(
            job_id,
            input,
            process_group.id.map(|pgid| pgid as pid_t),
            process_group.processes,
        ));
        job_id
    }
    pub fn has_jobs(&self) -> bool {
        !self.jobs.is_empty()
    }
    pub fn get_jobs(&self) -> Vec<&dyn Job> {
        self.jobs.iter().map(|j| j.as_job()).collect()
    }
    pub fn wait_for_job(&mut self, job_id: JobId) -> anyhow::Result<Option<ExitStatus>> {
        while self.job_is_running(job_id) {
            for job in &mut self.jobs {
                job.try_wait()?;
            }
        }
        let job_index = self.find_job(job_id).expect("job not found");
        Ok(self.jobs[job_index].last_status_code())
    }
    pub fn put_job_in_foreground(
        &mut self,
        job_id: Option<JobId>,
        cont: bool,
    ) -> anyhow::Result<Option<ExitStatus>> {
        let job_id = job_id
            .or(self.current_job)
            .ok_or_else(|| Error::NoSuchJob("current".into()))?;
        debug!("putting job [{}] in foreground", job_id);
        let _terminal_state = {
            let job_index = self
                .find_job(job_id)
                .ok_or_else(|| Error::NoSuchJob(format!("{job_id}")))?;
            self.jobs[job_index].set_last_running_in_foreground(true);
            let job_pgid = self.jobs[job_index].pgid();
            let job_tmodes = self.jobs[job_index].tmodes().clone();
            let _terminal_state = job_pgid.map(|pgid| TerminalState::new(Pid::from_raw(pgid)));
            if cont {
                if let Some(ref tmodes) = job_tmodes {
                    let temp_result = termios::tcsetattr(
                        util::get_terminal(),
                        termios::SetArg::TCSADRAIN,
                        tmodes,
                    );
                    log_if_err!(
                        temp_result,
                        "error setting terminal configuration for job ({})",
                        job_id
                    );
                }
                if let Some(ref pgid) = job_pgid {
                    signal::kill(Pid::from_raw(-pgid), Signal::SIGCONT)?;
                }
            }
            _terminal_state
        };
        self.wait_for_job(job_id)
    }
    pub fn put_job_in_background(
        &mut self,
        job_id: Option<JobId>,
        cont: bool,
    ) -> anyhow::Result<()> {
        let job_id = job_id
            .or(self.current_job)
            .ok_or_else(|| Error::NoSuchJob("current".into()))?;
        debug!("putting job [{}] in background", job_id);
        let job_pgid = {
            let job_index = self
                .find_job(job_id)
                .ok_or_else(|| Error::NoSuchJob(format!("{job_id}")))?;
            self.jobs[job_index].set_last_running_in_foreground(false);
            self.jobs[job_index].pgid()
        };
        if cont {
            if let Some(ref pgid) = job_pgid {
                signal::kill(Pid::from_raw(-pgid), Signal::SIGCONT)?;
            }
        }
        self.current_job = Some(job_id);
        Ok(())
    }
    pub fn kill_job(&mut self, job_id: JobId) -> anyhow::Result<Option<&dyn Job>> {
        if let Some(job_index) = self.find_job(job_id) {
            self.jobs[job_index].kill()?;
            Ok(Some(&self.jobs[job_index]))
        } else {
            Ok(None)
        }
    }
    pub fn update_job_statues(&mut self) -> anyhow::Result<()> {
        for job in &mut self.jobs {
            job.try_wait()?;
        }
        Ok(())
    }
    pub fn do_job_notification(&mut self) {
        let temp_result = self.update_job_statues();
        log_if_err!(temp_result, "do_job_notification");
        for job in &mut self.jobs.iter_mut() {
            if job.is_completed() && !job.last_running_in_foreground() {
                println!("{}", *job);
            } else if job.is_stopped() && !job.notified_stopped_job() {
                println!("{}", *job);
                job.set_notified_stopped_job(true);
            }
        }
        self.jobs.retain(|j| !j.is_completed());
    }
    fn get_next_job_id(&mut self) -> JobId {
        self.job_count += 1;
        JobId(self.job_count)
    }
    fn job_is_running(&self, job_id: JobId) -> bool {
        let job_index = self.find_job(job_id).expect("job not found");
        !self.jobs[job_index].is_stopped() && !self.jobs[job_index].is_completed()
    }
    fn find_job(&self, job_id: JobId) -> Option<usize> {
        self.jobs.iter().position(|job| job.id() == job_id)
    }
}
impl fmt::Debug for JobManager {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(f, "{} jobs\tjob_count: {}", self.jobs.len(), self.job_count)?;
        for job in &self.jobs {
            write!(f, "{job:?}")?;
        }
        Ok(())
    }
}
impl fmt::Display for JobStatus {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            JobStatus::Running => write!(f, "Running"),
            JobStatus::Stopped => write!(f, "Stopped"),
            JobStatus::Completed => write!(f, "Completed"),
        }
    }
}
pub struct JobImpl {
    id: JobId,
    input: String,
    pgid: Option<pid_t>,
    processes: Vec<Box<dyn Process>>,
    last_status_code: Option<ExitStatus>,
    last_running_in_foreground: bool,
    notified_stopped_job: bool,
    tmodes: Option<Termios>,
}
impl JobImpl {
    pub fn new(
        id: JobId,
        input: &str,
        pgid: Option<pid_t>,
        processes: Vec<Box<dyn Process>>,
    ) -> Self {
        let last_status_code = processes.iter().rev().find_map(|p| p.status_code());
        Self {
            id,
            input: input.to_string(),
            pgid,
            processes,
            last_status_code,
            last_running_in_foreground: true,
            notified_stopped_job: false,
            tmodes: termios::tcgetattr(util::get_terminal()).ok(),
        }
    }
    fn pgid(&self) -> Option<pid_t> {
        self.pgid
    }
    fn last_status_code(&self) -> Option<ExitStatus> {
        self.last_status_code
    }
    fn last_running_in_foreground(&self) -> bool {
        self.last_running_in_foreground
    }
    fn set_last_running_in_foreground(&mut self, last_running_in_foreground: bool) {
        self.last_running_in_foreground = last_running_in_foreground;
    }
    fn kill(&mut self) -> anyhow::Result<()> {
        for process in &mut self.processes {
            process.kill()?;
        }
        Ok(())
    }
    fn try_wait(&mut self) -> anyhow::Result<Option<ExitStatus>> {
        for process in &mut self.processes {
            if let Some(exit_status) = process.try_wait()? {
                self.last_status_code = Some(exit_status);
            }
        }
        Ok(self.last_status_code)
    }
    fn notified_stopped_job(&self) -> bool {
        self.notified_stopped_job
    }
    fn set_notified_stopped_job(&mut self, notified_stopped_job: bool) {
        self.notified_stopped_job = notified_stopped_job;
    }
    fn is_stopped(&self) -> bool {
        self.processes
            .iter()
            .all(|p| p.status() == ProcessStatus::Stopped)
    }
    fn is_completed(&self) -> bool {
        self.processes
            .iter()
            .all(|p| p.status() == ProcessStatus::Completed)
    }
}
impl Job for JobImpl {
    fn id(&self) -> JobId {
        self.id
    }
    fn input(&self) -> String {
        self.input.clone()
    }
    fn display(&self) -> String {
        format!("[{}] {}\t{}", self.id, self.status(), self.input)
    }
    fn processes(&self) -> &Vec<Box<dyn Process>> {
        &self.processes
    }
}
impl JobExt for JobImpl {
    fn tmodes(&self) -> &Option<Termios> {
        &self.tmodes
    }
    fn status(&self) -> JobStatus {
        if self.is_stopped() {
            JobStatus::Stopped
        } else if self.is_completed() {
            JobStatus::Completed
        } else {
            JobStatus::Running
        }
    }
}
impl fmt::Display for JobImpl {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "[{}] {}\t{}", self.id, self.status(), self.input)
    }
}
impl fmt::Debug for JobImpl {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "id: {}\tinput: {}", self.id, self.input)
    }
}
struct TerminalState {
    prev_pgid: Pid,
    prev_tmodes: Option<Termios>,
}
impl TerminalState {
    fn new(new_pgid: Pid) -> TerminalState {
        debug!("setting terminal process group to job's process group");
        let shell_terminal = util::get_terminal();
        unistd::tcsetpgrp(shell_terminal, new_pgid).unwrap();
        TerminalState {
            prev_pgid: unistd::getpgrp(),
            prev_tmodes: termios::tcgetattr(shell_terminal).ok(),
        }
    }
}
impl Drop for TerminalState {
    fn drop(&mut self) {
        debug!("putting shell back into foreground and restoring shell's terminal modes");
        let shell_terminal = util::get_terminal();
        unistd::tcsetpgrp(shell_terminal, self.prev_pgid).unwrap();
        if let Some(ref prev_tmodes) = self.prev_tmodes {
            let temp_result =
                termios::tcsetattr(shell_terminal, termios::SetArg::TCSADRAIN, prev_tmodes);
            log_if_err!(
                temp_result,
                "error restoring terminal configuration for shell"
            );
        }
    }
}