use crate::events::Event;
use portable_pty::{CommandBuilder, MasterPty, PtySize};
use std::collections::HashMap;
use std::io::Read;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio::sync::{broadcast, mpsc};

/// vt100 callbacks to capture bell, title, and OSC events.
struct VtCallbacks {
    bell: bool,
    title: Option<String>,
    command_done: Option<i32>,
}

impl VtCallbacks {
    fn new() -> Self {
        Self {
            bell: false,
            title: None,
            command_done: None,
        }
    }
    fn take_bell(&mut self) -> bool {
        std::mem::take(&mut self.bell)
    }
    fn take_title(&mut self) -> Option<String> {
        self.title.take()
    }
    fn take_command_done(&mut self) -> Option<i32> {
        self.command_done.take()
    }
}

impl vt100::Callbacks for VtCallbacks {
    fn audible_bell(&mut self, _: &mut vt100::Screen) {
        self.bell = true;
    }
    fn set_window_title(
        &mut self,
        _: &mut vt100::Screen,
        title: &[u8],
    ) {
        self.title =
            Some(String::from_utf8_lossy(title).into());
    }
    fn unhandled_osc(
        &mut self,
        _: &mut vt100::Screen,
        params: &[&[u8]],
    ) {
        if params.len() >= 3
            && params[0] == b"7777"
            && params[1] == b"done"
        {
            let code = std::str::from_utf8(params[2])
                .ok()
                .and_then(|s| s.parse().ok())
                .unwrap_or(-1);
            self.command_done = Some(code);
        }
    }
}

#[derive(Clone)]
pub struct Terminal(Arc<Inner>);

struct Inner {
    id: String,
    pid: u32,
    parser: Mutex<vt100::Parser<VtCallbacks>>,
    writer: Mutex<Box<dyn std::io::Write + Send>>,
    master: Mutex<Box<dyn MasterPty + Send>>,
    alive: AtomicBool,
    title: Mutex<String>,
    shell_init_dir: Option<String>,
    raw_tx: broadcast::Sender<Vec<u8>>,
}

impl Terminal {
    pub fn id(&self) -> &str {
        &self.0.id
    }
    pub fn pid(&self) -> u32 {
        self.0.pid
    }
    pub fn alive(&self) -> bool {
        self.0.alive.load(Ordering::Relaxed)
    }
    pub fn title(&self) -> String {
        self.0.title.lock().unwrap().clone()
    }

    pub fn with_screen<F, R>(&self, f: F) -> R
    where
        F: FnOnce(&vt100::Screen) -> R,
    {
        let parser = self.0.parser.lock().unwrap();
        f(parser.screen())
    }

    pub fn send_input(&self, data: &[u8]) {
        let mut w = self.0.writer.lock().unwrap();
        let _ = w.write_all(data);
    }

    pub fn subscribe_raw(
        &self,
    ) -> broadcast::Receiver<Vec<u8>> {
        self.0.raw_tx.subscribe()
    }

    pub fn resize(&self, rows: u16, cols: u16) {
        let master = self.0.master.lock().unwrap();
        let _ = master.resize(PtySize {
            rows,
            cols,
            pixel_width: 0,
            pixel_height: 0,
        });
        let mut parser = self.0.parser.lock().unwrap();
        parser.screen_mut().set_size(rows, cols);
    }

    pub fn kill(&self) {
        self.0.alive.store(false, Ordering::Relaxed);
        let pid = self.0.pid;
        if pid > 0 {
            let _ = nix::sys::signal::kill(
                nix::unistd::Pid::from_raw(-(pid as i32)),
                nix::sys::signal::Signal::SIGHUP,
            );
        }
        if let Some(ref dir) = self.0.shell_init_dir {
            let _ = std::fs::remove_dir_all(dir);
        }
    }

    #[allow(clippy::too_many_arguments)]
    pub fn spawn(
        id: String,
        cols: u16,
        rows: u16,
        cmd: Vec<String>,
        cwd: Option<String>,
        env: HashMap<String, String>,
        event_tx: broadcast::Sender<Event>,
        idle_timeout: Duration,
    ) -> Result<Self, Box<dyn std::error::Error>> {
        let pty_system = portable_pty::native_pty_system();
        let pair = pty_system.openpty(PtySize {
            rows,
            cols,
            pixel_width: 0,
            pixel_height: 0,
        })?;

        let shell = if cmd.is_empty() {
            std::env::var("SHELL")
                .unwrap_or_else(|_| "bash".into())
        } else {
            cmd[0].clone()
        };

        // Shell integration setup
        let init_dir =
            format!("/tmp/t4a_shell_{id}");
        let _ = std::fs::create_dir_all(&init_dir);

        let shell_base =
            std::path::Path::new(&shell)
                .file_name()
                .and_then(|n| n.to_str())
                .unwrap_or("bash");

        let mut cmd_builder;
        let mut extra_env: Vec<(String, String)> = Vec::new();

        match shell_base {
            "zsh" => {
                let orig_zdotdir = std::env::var("ZDOTDIR")
                    .unwrap_or_else(|_| {
                        std::env::var("HOME")
                            .unwrap_or_default()
                    });
                let rc = format!(
                    "ZDOTDIR=\"${{T4A_ORIG_ZDOTDIR:-$HOME}}\"\n\
                     [ -f \"$ZDOTDIR/.zshrc\" ] && \
                     source \"$ZDOTDIR/.zshrc\"\n\
                     __t4a_precmd() {{ \
                     printf '\\033]7777;done;%d\\007' \"$?\" }}\n\
                     precmd_functions+=(__t4a_precmd)\n"
                );
                let rc_path =
                    format!("{init_dir}/.zshrc");
                std::fs::write(&rc_path, rc)?;

                extra_env.push((
                    "T4A_ORIG_ZDOTDIR".into(),
                    orig_zdotdir,
                ));
                extra_env.push((
                    "ZDOTDIR".into(),
                    init_dir.clone(),
                ));

                cmd_builder = CommandBuilder::new(&shell);
                for arg in cmd.iter().skip(1) {
                    cmd_builder.arg(arg);
                }
            }
            _ => {
                // bash (and fallback)
                let rc = format!(
                    "[ -f ~/.bashrc ] && source ~/.bashrc\n\
                     __t4a_done() {{ \
                     printf '\\033]7777;done;%d\\007' \"$?\"; }}\n\
                     PROMPT_COMMAND=\"__t4a_done\
                     ${{PROMPT_COMMAND:+;$PROMPT_COMMAND}}\"\n"
                );
                let rc_path =
                    format!("{init_dir}/bashrc");
                std::fs::write(&rc_path, rc)?;

                cmd_builder = CommandBuilder::new(&shell);
                // Only inject --rcfile if user didn't
                // pass custom args
                if cmd.len() <= 1 {
                    cmd_builder.arg("--rcfile");
                    cmd_builder.arg(&rc_path);
                } else {
                    for arg in cmd.iter().skip(1) {
                        cmd_builder.arg(arg);
                    }
                }
            }
        }

        if let Some(ref d) = cwd {
            cmd_builder.cwd(d);
        }
        for (k, v) in &env {
            cmd_builder.env(k, v);
        }
        for (k, v) in &extra_env {
            cmd_builder.env(k, v);
        }

        let mut child =
            pair.slave.spawn_command(cmd_builder)?;
        let pid = child.process_id().unwrap_or(0);
        drop(pair.slave);

        let reader = pair.master.try_clone_reader()?;
        let writer = pair.master.take_writer()?;

        let parser = vt100::Parser::new_with_callbacks(
            rows,
            cols,
            1000,
            VtCallbacks::new(),
        );

        let (raw_tx, _) = broadcast::channel(64);

        let inner = Arc::new(Inner {
            id: id.clone(),
            pid,
            parser: Mutex::new(parser),
            writer: Mutex::new(writer),
            master: Mutex::new(pair.master),
            alive: AtomicBool::new(true),
            title: Mutex::new(String::new()),
            shell_init_dir: Some(init_dir),
            raw_tx,
        });

        let (byte_tx, mut byte_rx) =
            mpsc::channel::<Vec<u8>>(64);

        std::thread::spawn(move || {
            let mut reader = reader;
            let mut buf = [0u8; 4096];
            loop {
                match reader.read(&mut buf) {
                    Ok(0) | Err(_) => break,
                    Ok(n) => {
                        if byte_tx
                            .blocking_send(
                                buf[..n].to_vec(),
                            )
                            .is_err()
                        {
                            break;
                        }
                    }
                }
            }
        });

        let inner_ = inner.clone();
        let id_ = id.clone();
        tokio::spawn(async move {
            let inner = inner_;
            let mut is_idle = true;
            let mut last_activity = Instant::now();

            loop {
                tokio::select! {
                    result = byte_rx.recv() => {
                        match result {
                            Some(bytes) => {
                                if is_idle {
                                    let _ = event_tx.send(
                                        Event::Activity {
                                            terminal:
                                                id_.clone(),
                                        },
                                    );
                                    is_idle = false;
                                }
                                last_activity = Instant::now();
                                let _ = inner.raw_tx
                                    .send(bytes.clone());

                                let mut p =
                                    inner.parser
                                        .lock().unwrap();
                                p.process(&bytes);

                                if p.callbacks_mut()
                                    .take_bell()
                                {
                                    let _ = event_tx.send(
                                        Event::Bell {
                                            terminal:
                                                id_.clone(),
                                        },
                                    );
                                }
                                if let Some(title) =
                                    p.callbacks_mut()
                                        .take_title()
                                {
                                    *inner.title
                                        .lock().unwrap() =
                                        title.clone();
                                    let _ = event_tx.send(
                                        Event::Title {
                                            terminal:
                                                id_.clone(),
                                            title,
                                        },
                                    );
                                }
                                if let Some(code) =
                                    p.callbacks_mut()
                                        .take_command_done()
                                {
                                    let _ = event_tx.send(
                                        Event::CommandDone {
                                            terminal:
                                                id_.clone(),
                                            code,
                                        },
                                    );
                                }
                            }
                            None => {
                                inner.alive.store(
                                    false,
                                    Ordering::Relaxed,
                                );
                                let code = child
                                    .wait()
                                    .map(|s| s.exit_code())
                                    .unwrap_or(1);
                                let _ = event_tx.send(
                                    Event::Exit {
                                        terminal:
                                            id_.clone(),
                                        code,
                                    },
                                );
                                break;
                            }
                        }
                    }
                    _ = async {
                        if is_idle {
                            std::future::pending::<()>()
                                .await
                        } else {
                            let remaining =
                                idle_timeout
                                    .saturating_sub(
                                        last_activity
                                            .elapsed(),
                                    );
                            tokio::time::sleep(remaining)
                                .await;
                        }
                    } => {
                        is_idle = true;
                        let after_ms = last_activity
                            .elapsed()
                            .as_millis()
                            as u64;
                        let _ = event_tx.send(
                            Event::Idle {
                                terminal: id_.clone(),
                                after_ms,
                            },
                        );
                    }
                }
            }
        });

        Ok(Terminal(inner))
    }
}
