use std::{
    fmt,
    fs::File,
    io::{self, BufRead, BufReader, Stderr, Stdout, Write},
    marker::PhantomData,
    os::fd::{FromRawFd, IntoRawFd},
    pipe::{PipeReader, PipeWriter},
    process::{self, Child, ExitCode, ExitStatus, Stdio},
    thread::{self, JoinHandle},
};

#[cfg(any(rust_analyzer, target_os = "yggdrasil"))]
use std::os::yggdrasil::{
    io::terminal::set_terminal_group,
    process::{create_process_group, CommandExt},
};

#[cfg(any(rust_analyzer, unix))]
use std::os::unix::process::CommandExt;

use crate::{builtin, command::env::Environment, error::Error};

#[derive(Debug)]
pub enum Outcome {
    Process(ExitStatus),
    Builtin(ExitCode),
    ExitShell(ExitCode),
}

pub enum Handle {
    Process(Child),
    Thread(JoinHandle<Outcome>),
}

pub trait InheritOutput: fmt::Debug {
    type Stream: Write;

    fn inherited() -> Self::Stream;
}
#[derive(Debug)]
pub struct InheritStdout;
#[derive(Debug)]
pub struct InheritStderr;

impl InheritOutput for InheritStdout {
    type Stream = Stdout;
    fn inherited() -> Self::Stream {
        io::stdout()
    }
}
impl InheritOutput for InheritStderr {
    type Stream = Stderr;
    fn inherited() -> Self::Stream {
        io::stderr()
    }
}

impl io::Read for Input {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        match self {
            Self::Pipe(pipe) => pipe.read(buf),
            Self::File(file) => file.read(buf),
            Self::Inherit => io::stdin().read(buf),
        }
    }
}
impl From<Input> for Stdio {
    fn from(value: Input) -> Self {
        match value {
            Input::Inherit => Stdio::inherit(),
            Input::File(file) => Stdio::from(file.into_inner()),
            Input::Pipe(pipe) => unsafe { Stdio::from_raw_fd(pipe.into_inner().into_raw_fd()) },
        }
    }
}
impl Input {
    pub fn read_line(&mut self, line: &mut String) -> Result<usize, io::Error> {
        match self {
            Self::Pipe(pipe) => pipe.read_line(line),
            Self::File(file) => file.read_line(line),
            Self::Inherit => io::stdin().read_line(line),
        }
    }
}

impl<I: InheritOutput> io::Write for Output<I> {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        match self {
            Self::Pipe(pipe) => pipe.write(buf),
            Self::File(file) => file.write(buf),
            Self::Inherit(_) => I::inherited().write(buf),
        }
    }

    fn flush(&mut self) -> io::Result<()> {
        match self {
            Self::Pipe(pipe) => pipe.flush(),
            Self::File(file) => file.flush(),
            Self::Inherit(_) => I::inherited().flush(),
        }
    }
}
impl<I: InheritOutput> From<Output<I>> for Stdio {
    fn from(value: Output<I>) -> Self {
        match value {
            Output::Inherit(_) => Stdio::inherit(),
            Output::File(file) => Stdio::from(file),
            Output::Pipe(pipe) => unsafe { Stdio::from_raw_fd(pipe.into_raw_fd()) },
        }
    }
}
impl<I: InheritOutput> Output<I> {
    pub fn try_clone(&self) -> Result<Self, Error> {
        match self {
            Self::File(file) => Ok(Self::File(file.try_clone()?)),
            Self::Pipe(pipe) => Ok(Self::Pipe(pipe.try_clone()?)),
            Self::Inherit(_) => Ok(Self::Inherit(PhantomData)),
        }
    }
}

#[derive(Debug)]
pub enum Input {
    Pipe(BufReader<PipeReader>),
    File(BufReader<File>),
    Inherit,
}

pub enum Output<I: InheritOutput> {
    Pipe(PipeWriter),
    File(File),
    Inherit(PhantomData<I>),
}

pub struct Execution {
    pub program: String,
    pub arguments: Vec<String>,
    pub envs: Vec<(String, String)>,
    pub stdin: Input,
    pub stdout: Output<InheritStdout>,
    pub stderr: Output<InheritStderr>,
}

impl Outcome {
    pub fn is_success(&self) -> bool {
        match self {
            Self::Process(status) => status.success(),
            Self::Builtin(status) => *status == ExitCode::SUCCESS,
            Self::ExitShell(_code) => true,
        }
    }

    pub fn err() -> Self {
        Self::Builtin(ExitCode::FAILURE)
    }

    pub fn ok() -> Self {
        Self::Builtin(ExitCode::SUCCESS)
    }
}

// TODO move pipelines into process groups
#[cfg(any(rust_analyzer, target_os = "yggdrasil"))]
fn spawn_command(
    execution: Execution,
    pgid: runtime::rt::process::ProcessGroupId,
    env: &Environment,
) -> Result<Child, Error> {
    let mut command = process::Command::new(execution.program);

    command
        .args(execution.arguments)
        .stdin(execution.stdin)
        .stdout(execution.stdout)
        .stderr(execution.stderr)
        .process_group(pgid);

    env.to_command(&mut command);
    for (key, value) in execution.envs {
        command.env(key, value);
    }

    let child = command.spawn()?;

    Ok(child)
}

#[cfg(any(rust_analyzer, unix))]
fn spawn_command(
    execution: Execution,
    pgid: &mut Option<i32>,
    env: &Environment,
) -> Result<Child, Error> {
    let mut command = process::Command::new(execution.program);

    command
        .args(execution.arguments)
        .process_group((*pgid).unwrap_or(0))
        .stdin(execution.stdin)
        .stdout(execution.stdout)
        .stderr(execution.stderr);

    env.to_command(&mut command);
    for (key, value) in execution.envs {
        command.env(key, value);
    }

    let child = command.spawn()?;

    if pgid.is_none() {
        *pgid = Some(child.id() as i32);
    }

    Ok(child)
}

#[cfg(any(unix, rust_analyzer))]
pub fn exec_pipeline<I: IntoIterator<Item = Execution>>(
    pipeline: I,
    env: &Environment,
) -> Result<Vec<Handle>, Error> {
    let mut handles = vec![];
    let mut pgid = None;

    for element in pipeline.into_iter() {
        let handle = if let Some(builtin) = builtin::get(&element.program) {
            let io = builtin::Args {
                stdin: element.stdin,
                stdout: element.stdout,
                stderr: element.stderr,
            };

            Handle::Thread(thread::spawn(move || {
                builtin(io, element.arguments, element.envs.into())
            }))
        } else {
            let child = spawn_command(element, &mut pgid, env)?;

            Handle::Process(child)
        };

        handles.push(handle);
    }
    // unsafe {
    //     libc::tcsetpgrp(stdout().as_raw_fd(), pgid.unwrap());
    //     // set_terminal_group(&stdin(), pgid)?;
    // }
    Ok(handles)
}

#[cfg(any(target_os = "yggdrasil", rust_analyzer))]
pub fn exec_pipeline<I: IntoIterator<Item = Execution>>(
    pipeline: I,
    env: &Environment,
) -> Result<Vec<Handle>, Error> {
    use std::io::stdin;

    let mut handles = vec![];
    let pgid = create_process_group();
    for element in pipeline.into_iter() {
        let handle = if let Some(builtin) = builtin::get(&element.program) {
            let io = builtin::Args {
                stdin: element.stdin,
                stdout: element.stdout,
                stderr: element.stderr,
            };

            Handle::Thread(thread::spawn(move || {
                builtin(io, element.arguments, element.envs.into())
            }))
        } else {
            let child = spawn_command(element, pgid, env)?;

            Handle::Process(child)
        };

        handles.push(handle);
    }
    unsafe {
        set_terminal_group(&stdin(), pgid)?;
    }
    Ok(handles)
}

pub fn wait_for_pipeline(handles: Vec<Handle>) -> Result<(Outcome, Option<ExitCode>), Error> {
    let mut outcomes = vec![];

    for element in handles {
        match element {
            Handle::Thread(thread) => {
                let result = match thread.join() {
                    Ok(result) => result,
                    Err(_error) => {
                        eprintln!("Builtin thread panicked");
                        Outcome::err()
                    }
                };
                outcomes.push(result);
            }
            Handle::Process(mut child) => {
                let result = child.wait()?;
                outcomes.push(Outcome::Process(result));
            }
        }
    }

    // Find first non-successful outcome
    let mut exit = None;
    let mut error = None;

    for outcome in outcomes {
        if let Outcome::ExitShell(code) = outcome {
            exit = Some(code);
        } else if error.is_none() && !outcome.is_success() {
            error = Some(outcome);
        }
    }

    #[cfg(any(unix, rust_analyzer))]
    unsafe {
        use std::os::fd::AsRawFd;
        libc::tcsetpgrp(io::stdout().as_raw_fd(), libc::getpgrp());
    }
    // set_terminal_group(&stdin(), pgid)?;

    let error = match error {
        Some(exit) => exit,
        None => Outcome::ok(),
    };

    Ok((error, exit))
}
