use crate::protocol::{ClientMsg, DaemonState, SessionInfo, StyledSpan, TabInfo, TermColor, MouseMode, MouseEncoding, socket_path};
use portable_pty::{native_pty_system, CommandBuilder, MasterPty, PtySize};
use std::{
    io::{self, BufRead, BufReader, Read, Write},
    os::unix::net::{UnixListener, UnixStream},
    sync::{
        atomic::{AtomicBool, Ordering},
        Arc, Mutex,
    },
    thread,
    time::Duration,
};

struct Tab {
    name: String,
    parser: Arc<Mutex<vt100::Parser>>,
    master: Box<dyn MasterPty + Send>,
    writer: Box<dyn Write + Send>,
    exited: Arc<AtomicBool>,
}

struct Session {
    name: String,
    tabs: Vec<Tab>,
    active_tab: usize,
}

struct DaemonApp {
    sessions: Vec<Session>,
    active_session: usize,
}


#[derive(Clone)]
struct ClientViewState {
    active_session: usize,
    active_tabs: Vec<usize>,
}

impl DaemonApp {
    fn snapshot_for_client(&self, view: &ClientViewState) -> DaemonState {
        let sess_count = self.sessions.len();
        let active_session = if view.active_session < sess_count { view.active_session } else { self.active_session };
        let sessions_info: Vec<SessionInfo> = self
            .sessions
            .iter()
            .enumerate()
            .map(|(idx, s)| {
                let active_tab = if idx < view.active_tabs.len() {
                    let t = view.active_tabs[idx];
                    if t < s.tabs.len() { t } else { s.active_tab }
                } else {
                    s.active_tab
                };
                SessionInfo {
                    name: s.name.clone(),
                    tabs: s.tabs.iter().map(|t| TabInfo { name: t.name.clone() }).collect(),
                    active_tab,
                }
            })
            .collect();

        let (spans, cursor_row, cursor_col, cursor_hidden, mouse_mode, mouse_encoding) = {
            let sess = &self.sessions[active_session];
            let tab_idx = if active_session < view.active_tabs.len() {
                std::cmp::min(view.active_tabs[active_session], sess.tabs.len().saturating_sub(1))
            } else {
                std::cmp::min(sess.active_tab, sess.tabs.len().saturating_sub(1))
            };
            let parser = sess.tabs[tab_idx].parser.lock().unwrap();
            let scr = parser.screen();
            let (cursor_row, cursor_col) = scr.cursor_position();
            let cursor_hidden = scr.hide_cursor();
            let spans = screen_to_spans(scr);
            let mouse_mode = vt100_mouse_mode_to_proto(scr.mouse_protocol_mode());
            let mouse_encoding = vt100_mouse_encoding_to_proto(scr.mouse_protocol_encoding());
            (spans, cursor_row, cursor_col, cursor_hidden, mouse_mode, mouse_encoding)
        };

        DaemonState {
            sessions: sessions_info,
            active_session,
            screen: spans,
            cursor_row,
            cursor_col,
            cursor_hidden,
            mouse_mode,
            mouse_encoding,
        }
    }
}

/// Convert a vt100 screen into rows of styled spans.  Each span is a run of
/// characters that share the same fg/bg/bold style, so the common case of a
/// uniform-coloured terminal compresses to one span per row.
fn screen_to_spans(screen: &vt100::Screen) -> Vec<Vec<StyledSpan>> {
    let (rows, cols) = screen.size();
    let mut result = Vec::with_capacity(rows as usize);

    for row in 0..rows {
        let mut spans: Vec<StyledSpan> = Vec::new();
        let mut cur_text = String::new();
        let mut cur_fg = TermColor::Default;
        let mut cur_bg = TermColor::Default;
        let mut cur_bold = false;

        for col in 0..cols {
            let (fg, bg, bold, ch) = match screen.cell(row, col) {
                Some(c) => (
                    vt100_color(c.fgcolor()),
                    vt100_color(c.bgcolor()),
                    c.bold(),
                    {
                        let s = c.contents();
                        if s.is_empty() { " ".to_string() } else { s.to_string() }
                    },
                ),
                None => (TermColor::Default, TermColor::Default, false, " ".to_string()),
            };

            if fg != cur_fg || bg != cur_bg || bold != cur_bold {
                if !cur_text.is_empty() {
                    spans.push(StyledSpan {
                        text: std::mem::take(&mut cur_text),
                        fg: cur_fg,
                        bg: cur_bg,
                        bold: cur_bold,
                    });
                }
                cur_fg = fg;
                cur_bg = bg;
                cur_bold = bold;
            }
            cur_text.push_str(&ch);
        }

        if !cur_text.is_empty() {
            spans.push(StyledSpan { text: cur_text, fg: cur_fg, bg: cur_bg, bold: cur_bold });
        }

        result.push(spans);
    }
    result
}

fn vt100_color(c: vt100::Color) -> TermColor {
    match c {
        vt100::Color::Default => TermColor::Default,
        vt100::Color::Idx(n) => TermColor::Indexed(n),
        vt100::Color::Rgb(r, g, b) => TermColor::Rgb(r, g, b),
    }
}

fn vt100_mouse_mode_to_proto(m: vt100::MouseProtocolMode) -> MouseMode {
    match m {
        vt100::MouseProtocolMode::None => MouseMode::None,
        vt100::MouseProtocolMode::Press => MouseMode::Press,
        vt100::MouseProtocolMode::PressRelease => MouseMode::PressRelease,
        vt100::MouseProtocolMode::ButtonMotion => MouseMode::ButtonMotion,
        vt100::MouseProtocolMode::AnyMotion => MouseMode::AnyMotion,
    }
}

fn vt100_mouse_encoding_to_proto(e: vt100::MouseProtocolEncoding) -> MouseEncoding {
    match e {
        vt100::MouseProtocolEncoding::Default => MouseEncoding::Default,
        vt100::MouseProtocolEncoding::Utf8 => MouseEncoding::Utf8,
        vt100::MouseProtocolEncoding::Sgr => MouseEncoding::Sgr,
    }
}

pub fn run() -> io::Result<()> {
    let path = socket_path();

    // Exit immediately if another daemon is already running.
    if UnixStream::connect(&path).is_ok() {
        return Ok(());
    }

    let _ = std::fs::remove_file(&path);
    let listener = UnixListener::bind(&path)?;
    listener.set_nonblocking(true)?;

    // Detach from controlling terminal so we survive SSH disconnects.
    unsafe { libc::setsid() };

    let app = Arc::new(Mutex::new(DaemonApp {
        sessions: vec![new_session("Main")],
        active_session: 0,
    }));
    let exit_flag = Arc::new(AtomicBool::new(false));

    loop {
        if exit_flag.load(Ordering::SeqCst) {
            break;
        }
        match listener.accept() {
            Ok((stream, _)) => {
                handle_client(stream, Arc::clone(&app), Arc::clone(&exit_flag));
            }
            Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
                // Periodically clean up tabs whose underlying PTY has exited.
                {
                    let mut guard = app.lock().unwrap();
                    for sess in guard.sessions.iter_mut() {
                        // Remove tabs where the reader noted the child exited.
                        sess.tabs.retain(|t| !t.exited.load(Ordering::SeqCst));
                        if sess.tabs.is_empty() {
                            // Ensure there's always at least one tab to avoid panics
                            // in snapshot() which assumes an active tab exists.
                            sess.tabs.push(spawn_tab("Tab1"));
                        }
                        if sess.active_tab >= sess.tabs.len() {
                            sess.active_tab = sess.tabs.len().saturating_sub(1);
                        }
                    }
                }
                thread::sleep(Duration::from_millis(50));
            }
            Err(e) => {
                eprintln!("daemon accept error: {e}");
                break;
            }
        }
    }

    let _ = std::fs::remove_file(&path);
    Ok(())
}

fn handle_client(
    stream: UnixStream,
    app: Arc<Mutex<DaemonApp>>,
    exit_flag: Arc<AtomicBool>,
) {
    let stream_write = stream.try_clone().expect("try_clone failed");
    let app_w = Arc::clone(&app);
    let exit_w = Arc::clone(&exit_flag);

    // Initialize per-client view state from current app state.
    let initial_view = {
        let guard = app.lock().unwrap();
        let active_tabs: Vec<usize> = guard.sessions.iter().map(|s| s.active_tab).collect();
        ClientViewState { active_session: guard.active_session, active_tabs }
    };
    let client_view = Arc::new(Mutex::new(initial_view));
    let client_view_writer = Arc::clone(&client_view);

    // Writer thread: push full state snapshot every 16 ms tailored to this client.
    thread::spawn(move || {
        let mut w = io::BufWriter::new(stream_write);
        loop {
            if exit_w.load(Ordering::SeqCst) {
                break;
            }
            // Copy view so we don't hold its lock while locking the app.
            let view = { client_view_writer.lock().unwrap().clone() };
            let state = {
                let guard = app_w.lock().unwrap();
                guard.snapshot_for_client(&view)
            };
            let mut line = serde_json::to_string(&state).unwrap();
            line.push('\n');
            if w.write_all(line.as_bytes()).is_err() || w.flush().is_err() {
                break;
            }
            thread::sleep(Duration::from_millis(16));
        }
    });

    // Reader thread: process commands from this client.
    let exit_r = Arc::clone(&exit_flag);
    let app_r = Arc::clone(&app);
    let client_view_r = Arc::clone(&client_view);
    thread::spawn(move || {
        let reader = BufReader::new(stream);
        for line in reader.lines() {
            match line {
                Ok(l) => {
                    if let Ok(msg) = serde_json::from_str::<ClientMsg>(&l) {
                        match msg {
                            ClientMsg::SwitchSession(i) => {
                                let mut cv = client_view_r.lock().unwrap();
                                let sidx = i;
                                cv.active_session = sidx;
                                if cv.active_tabs.len() <= sidx {
                                    cv.active_tabs.resize(sidx + 1, 0);
                                }
                            }
                            ClientMsg::SwitchTab(i) => {
                                let mut cv = client_view_r.lock().unwrap();
                                let sidx = cv.active_session;
                                if cv.active_tabs.len() <= sidx {
                                    cv.active_tabs.resize(sidx + 1, 0);
                                }
                                cv.active_tabs[sidx] = i;
                            }
                            other => {
                                // Determine target session/tab from client view (snapshot_for_client will clamp against actual counts).
                                let (ts, tt) = {
                                    let cv = client_view_r.lock().unwrap().clone();
                                    let ts_usize = cv.active_session;
                                    let tt_usize = if ts_usize < cv.active_tabs.len() { cv.active_tabs[ts_usize] } else { 0 };
                                    (ts_usize, tt_usize)
                                };
                                let mut guard = app_r.lock().unwrap();
                                apply_command(&mut guard, other, &exit_r, ts, tt);
                            }
                        }
                    }
                }
                Err(_) => break,
            }
        }
    });
}

fn apply_command(app: &mut DaemonApp, msg: ClientMsg, exit_flag: &AtomicBool, target_session: usize, target_tab: usize) {
    match msg {
        ClientMsg::Input(bytes) => {
            if app.sessions.is_empty() { return; }
            let sidx = std::cmp::min(target_session, app.sessions.len().saturating_sub(1));
            let s = &mut app.sessions[sidx];
            if s.tabs.is_empty() { return; }
            let tidx = std::cmp::min(target_tab, s.tabs.len().saturating_sub(1));
            let t = &mut s.tabs[tidx];
            let _ = t.writer.write_all(&bytes);
        }
        ClientMsg::NewTab => {
            if app.sessions.is_empty() { return; }
            let sidx = std::cmp::min(target_session, app.sessions.len().saturating_sub(1));
            let s = &mut app.sessions[sidx];
            let n = s.tabs.len() + 1;
            s.tabs.push(spawn_tab(&format!("Tab{}", n)));
            // Do not change server's active_tab so other clients aren't forced to switch.
        }
        ClientMsg::NewSession(name) => {
            app.sessions.push(new_session(&name));
            // Do not change app.active_session; clients choose their view independently.
        }
        ClientMsg::SwitchTab(i) => {
            if app.sessions.is_empty() { return; }
            let sidx = std::cmp::min(target_session, app.sessions.len().saturating_sub(1));
            let s = &mut app.sessions[sidx];
            if i < s.tabs.len() {
                s.active_tab = i;
            }
        }
        ClientMsg::SwitchSession(i) => {
            if i < app.sessions.len() {
                app.active_session = i;
            }
        }
        ClientMsg::Resize { cols, rows } => {
            if app.sessions.is_empty() { return; }
            let sidx = std::cmp::min(target_session, app.sessions.len().saturating_sub(1));
            let s = &mut app.sessions[sidx];
            if s.tabs.is_empty() { return; }
            let tidx = std::cmp::min(target_tab, s.tabs.len().saturating_sub(1));
            let _ = s.tabs[tidx].master.resize(PtySize { rows, cols, pixel_width: 0, pixel_height: 0 });
            s.tabs[tidx].parser.lock().unwrap().set_size(rows, cols);
        }
        ClientMsg::Scroll(delta) => {
            if app.sessions.is_empty() { return; }
            let sidx = std::cmp::min(target_session, app.sessions.len().saturating_sub(1));
            let s = &mut app.sessions[sidx];
            if s.tabs.is_empty() { return; }
            let tidx = std::cmp::min(target_tab, s.tabs.len().saturating_sub(1));
            let mut parser = s.tabs[tidx].parser.lock().unwrap();
            let cur = parser.screen().scrollback();
            if delta >= 0 {
                parser.set_scrollback(cur.saturating_add(delta as usize));
            } else {
                parser.set_scrollback(cur.saturating_sub((-delta) as usize));
            }
        }
        ClientMsg::RenameTab { index, name } => {
            if app.sessions.is_empty() { return; }
            let sidx = std::cmp::min(target_session, app.sessions.len().saturating_sub(1));
            let s = &mut app.sessions[sidx];
            if index < s.tabs.len() {
                s.tabs[index].name = name;
            }
        }
        ClientMsg::CloseTab(index) => {
            if app.sessions.is_empty() { return; }
            let sidx = std::cmp::min(target_session, app.sessions.len().saturating_sub(1));
            let s = &mut app.sessions[sidx];
            if index < s.tabs.len() {
                s.tabs.remove(index);
                if s.tabs.is_empty() {
                    s.tabs.push(spawn_tab("Tab1"));
                }
                if s.active_tab >= s.tabs.len() {
                    s.active_tab = s.tabs.len().saturating_sub(1);
                }
            }
        }
        ClientMsg::RenameSession { index, name } => {
            if index < app.sessions.len() {
                app.sessions[index].name = name;
            }
        }
        ClientMsg::Exit => {
            exit_flag.store(true, Ordering::SeqCst);
        }
        ClientMsg::Detach => {} // client just disconnects; daemon keeps running
    }
}

fn new_session(name: &str) -> Session {
    Session {
        name: name.to_string(),
        tabs: vec![spawn_tab("Tab1")],
        active_tab: 0,
    }
}

fn spawn_tab(name: &str) -> Tab {
    let pty = native_pty_system();
    let pair = pty
        .openpty(PtySize { rows: 24, cols: 80, pixel_width: 0, pixel_height: 0 })
        .expect("openpty failed");

    let shell = std::env::var("SHELL").unwrap_or_else(|_| "sh".to_string());
    let _child = pair.slave.spawn_command(CommandBuilder::new(&shell)).expect("spawn_command failed");

    let writer = pair.master.take_writer().expect("take_writer failed");
    let mut reader = pair.master.try_clone_reader().expect("try_clone_reader failed");

    let parser = Arc::new(Mutex::new(vt100::Parser::new(24, 80, 10000)));
    let parser_clone = Arc::clone(&parser);

    let exited = Arc::new(AtomicBool::new(false));
    let exited_clone = Arc::clone(&exited);

    thread::spawn(move || {
        let mut tmp = [0u8; 4096];
        loop {
            match reader.read(&mut tmp) {
                Ok(0) | Err(_) => {
                    exited_clone.store(true, Ordering::SeqCst);
                    break;
                }
                Ok(n) => {
                    parser_clone.lock().unwrap().process(&tmp[..n]);
                }
            }
        }
    });

    Tab { name: name.to_string(), parser, master: pair.master, writer, exited }
}
