use portable_pty::{native_pty_system, CommandBuilder, PtyPair, PtySize};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io::{Read, Write};
use tauri::{AppHandle, Emitter};
use uuid::Uuid;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PtySpawnOptions {
    pub cwd: Option<String>,
    pub shell: Option<String>,
    pub cols: u16,
    pub rows: u16,
    pub env: Option<HashMap<String, String>>,
    pub args: Option<Vec<String>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PtyWritePayload {
    pub id: String,
    pub data: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PtyOutputEvent {
    pub id: String,
    pub data: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PtyExitEvent {
    pub id: String,
    pub code: Option<i32>,
}

struct PtySession {
    writer: Box<dyn Write + Send>,
    _pair: PtyPair,
}

/// Floor below which a PTY size is treated as an implausible measurement
/// (mid-layout glitch, hidden pane) rather than a real terminal size. Mirrors
/// the frontend's MIN_TERMINAL_COLS/MIN_TERMINAL_ROWS in XTermPane.tsx so the
/// real ConPTY console can never desync from what xterm.js believes -- a
/// mismatch there is what causes shells to wrap/redraw output as if the
/// terminal were still tiny even though xterm.js renders it as wide.
const MIN_PTY_COLS: u16 = 30;
const MIN_PTY_ROWS: u16 = 4;

pub struct PtyManager {
    sessions: parking_lot::Mutex<HashMap<String, PtySession>>,
}

impl Default for PtyManager {
    fn default() -> Self {
        Self {
            sessions: parking_lot::Mutex::new(HashMap::new()),
        }
    }
}

impl PtyManager {
    pub fn spawn(&self, app: AppHandle, options: PtySpawnOptions) -> Result<String, String> {
        let id = Uuid::new_v4().to_string();
        let pty_system = native_pty_system();
        let size = PtySize {
            rows: options.rows.max(MIN_PTY_ROWS),
            cols: options.cols.max(MIN_PTY_COLS),
            pixel_width: 0,
            pixel_height: 0,
        };

        let pair = pty_system
            .openpty(size)
            .map_err(|e| format!("Failed to open PTY: {e}"))?;

        let shell = options.shell.unwrap_or_else(default_shell_for_platform);

        let mut cmd = CommandBuilder::new(&shell);

        let resolved_cwd = resolve_spawn_cwd(options.cwd.as_ref());
        let used_shell_cwd =
            apply_shell_initial_directory(&mut cmd, &shell, resolved_cwd.as_deref());

        if !used_shell_cwd {
            if let Some(ref cwd) = resolved_cwd {
                cmd.cwd(cwd);
            }
        }

        if let Some(args) = options.args.as_ref() {
            for arg in args {
                cmd.arg(arg);
            }
        }

        if let Some(env) = options.env.as_ref() {
            for (key, value) in env {
                cmd.env(key, value);
            }
        }

        // Ensure a usable terminal inside the PTY
        cmd.env("TERM", "xterm-256color");
        cmd.env("COLORTERM", "truecolor");

        let _child = pair
            .slave
            .spawn_command(cmd)
            .map_err(|e| format!("Failed to spawn shell: {e}"))?;

        let mut reader = pair
            .master
            .try_clone_reader()
            .map_err(|e| format!("Failed to clone PTY reader: {e}"))?;

        let writer = pair
            .master
            .take_writer()
            .map_err(|e| format!("Failed to take PTY writer: {e}"))?;

        let session_id = id.clone();
        let app_handle = app.clone();

        std::thread::spawn(move || {
            let mut buffer = [0u8; 8192];
            // Bytes left over from a previous read that did not form a complete
            // UTF-8 sequence (e.g. a multi-byte box-drawing character such as
            // U+2550 '=' split across two PTY reads). Without carrying these
            // over, `String::from_utf8_lossy` replaces the dangling partial
            // sequence with the replacement character independently on each
            // side of the split, corrupting box-drawing borders deterministically
            // at whatever byte offset the OS happens to split the read on.
            let mut pending: Vec<u8> = Vec::new();
            loop {
                match reader.read(&mut buffer) {
                    Ok(0) => break,
                    Ok(n) => {
                        pending.extend_from_slice(&buffer[..n]);

                        let (valid, rest) = match std::str::from_utf8(&pending) {
                            Ok(s) => (s.to_owned(), Vec::new()),
                            Err(e) => {
                                let valid_up_to = e.valid_up_to();
                                let valid = String::from_utf8_lossy(&pending[..valid_up_to]).into_owned();
                                let rest = pending[valid_up_to..].to_vec();
                                // Guard against a truly invalid (not just incomplete)
                                // byte sequence permanently stalling the stream: only
                                // keep the tail as "pending" if it's short enough to be
                                // a legitimate incomplete UTF-8 char (max 4 bytes) and
                                // actually looks like a valid sequence start.
                                if rest.len() > 4 || e.error_len().is_some() {
                                    let flushed = String::from_utf8_lossy(&pending[valid_up_to..]).into_owned();
                                    (valid + &flushed, Vec::new())
                                } else {
                                    (valid, rest)
                                }
                            }
                        };

                        pending = rest;

                        if !valid.is_empty() {
                            let _ = app_handle.emit(
                                "pty-output",
                                PtyOutputEvent {
                                    id: session_id.clone(),
                                    data: valid,
                                },
                            );
                        }
                    }
                    Err(e) => {
                        let _ = app_handle
                            .emit("pty-error", format!("PTY read error ({session_id}): {e}"));
                        break;
                    }
                }
            }

            if !pending.is_empty() {
                let flushed = String::from_utf8_lossy(&pending).into_owned();
                let _ = app_handle.emit(
                    "pty-output",
                    PtyOutputEvent {
                        id: session_id.clone(),
                        data: flushed,
                    },
                );
            }

            let _ = app_handle.emit(
                "pty-exit",
                PtyExitEvent {
                    id: session_id,
                    code: None,
                },
            );
        });

        self.sessions.lock().insert(
            id.clone(),
            PtySession {
                writer,
                _pair: pair,
            },
        );

        Ok(id)
    }

    pub fn write(&self, id: &str, data: &str) -> Result<(), String> {
        let mut sessions = self.sessions.lock();
        let session = sessions
            .get_mut(id)
            .ok_or_else(|| format!("PTY session not found: {id}"))?;
        session
            .writer
            .write_all(data.as_bytes())
            .map_err(|e| format!("Failed to write to PTY: {e}"))?;
        session
            .writer
            .flush()
            .map_err(|e| format!("Failed to flush PTY: {e}"))?;
        Ok(())
    }

    pub fn resize(&self, id: &str, cols: u16, rows: u16) -> Result<(), String> {
        let sessions = self.sessions.lock();
        let session = sessions
            .get(id)
            .ok_or_else(|| format!("PTY session not found: {id}"))?;
        session
            ._pair
            .master
            .resize(PtySize {
                rows: rows.max(MIN_PTY_ROWS),
                cols: cols.max(MIN_PTY_COLS),
                pixel_width: 0,
                pixel_height: 0,
            })
            .map_err(|e| format!("Failed to resize PTY: {e}"))?;
        Ok(())
    }

    pub fn kill(&self, id: &str) -> Result<(), String> {
        self.sessions.lock().remove(id);
        Ok(())
    }
}

/// Returns true when the shell was configured with an explicit startup directory
/// (Windows PowerShell/cmd often ignore PTY cwd).
fn apply_shell_initial_directory(cmd: &mut CommandBuilder, shell: &str, cwd: Option<&str>) -> bool {
    let Some(cwd) = cwd else {
        return false;
    };
    let shell_lc = shell.to_lowercase().replace('\\', "/");

    if cfg!(windows) {
        if shell_lc.contains("pwsh") {
            cmd.arg("-NoLogo");
            cmd.arg("-NoExit");
            cmd.arg("-WorkingDirectory");
            cmd.arg(normalize_windows_path(cwd));
            return true;
        }
        if shell_lc.contains("powershell") {
            let escaped = normalize_windows_path(cwd).replace('\'', "''");
            cmd.arg("-NoLogo");
            cmd.arg("-NoExit");
            cmd.arg("-Command");
            cmd.arg(format!("Set-Location -LiteralPath '{}'", escaped));
            return true;
        }
        if shell_lc.ends_with("cmd.exe")
            || shell_lc.contains("/cmd.exe")
            || shell_lc.contains("/cmd")
        {
            let normalized = normalize_windows_path(cwd);
            cmd.cwd(&normalized);
            cmd.arg("/Q");
            cmd.arg("/K");
            return true;
        }
    }

    false
}

fn normalize_windows_path(path: &str) -> String {
    let mut normalized = path.replace('/', "\\");
    if let Some(stripped) = normalized.strip_prefix(r"\\?\") {
        normalized = stripped.to_string();
    }
    normalized
}

fn resolve_spawn_cwd(cwd: Option<&String>) -> Option<String> {
    let raw = cwd?.trim();
    if raw.is_empty() || raw == "." {
        return None;
    }

    if cfg!(windows) {
        // Use the path the user picked; canonicalize() can yield \\?\ prefixes cmd rejects.
        return Some(normalize_windows_path(raw));
    }

    let path = std::path::Path::new(raw);
    let resolved = if path.is_absolute() {
        if let Ok(canonical) = path.canonicalize() {
            canonical.to_string_lossy().into_owned()
        } else {
            raw.to_string()
        }
    } else if let Ok(current) = std::env::current_dir() {
        let joined = current.join(path);
        if joined.exists() {
            if let Ok(canonical) = joined.canonicalize() {
                canonical.to_string_lossy().into_owned()
            } else {
                joined.to_string_lossy().into_owned()
            }
        } else {
            joined.to_string_lossy().into_owned()
        }
    } else {
        raw.to_string()
    };

    Some(resolved)
}

fn windows_pty_shell() -> String {
    if let Ok(comspec) = std::env::var("COMSPEC") {
        let trimmed = comspec.trim();
        if !trimmed.is_empty() {
            return trimmed.to_string();
        }
    }
    r"C:\Windows\System32\cmd.exe".into()
}

fn default_shell_for_platform() -> String {
    if cfg!(windows) {
        // cmd.exe is the most reliable ConPTY child on Windows; PowerShell can fail
        // with 0xc0000142 when spawned directly as the PTY process on some systems.
        windows_pty_shell()
    } else if cfg!(target_os = "macos") {
        std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".into())
    } else {
        std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".into())
    }
}
