use std::error::Error;
use std::fs;
use std::io::{BufRead, BufReader, Write};
use std::os::unix::fs::PermissionsExt;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::PathBuf;
use std::time::Duration;

use forge_ipc::{AppIdMatcher, CliToggle, IpcMessage, Keysym, WindowScale};
use serde_json::Value;
use smithay::input::keyboard::XkbConfig;
use smithay::reexports::calloop::LoopHandle;
use tracing::{info, warn};

use crate::config::{GestureAction, GestureBinding, KeyBinding};
use crate::forge::Forge;
use crate::socket::SocketSource;

pub fn spawn_ipc_socket(
    event_loop: &LoopHandle<'static, Forge>,
    socket_name: &str,
) -> Result<PathBuf, Box<dyn Error>> {
    let socket_path = forge_ipc::socket_path(socket_name);

    if socket_path.exists() {
        fs::remove_file(&socket_path)?;
    }

    let listener = UnixListener::bind(&socket_path)?;
    // Restrict socket to the session owner — crustace and forge-msg run as
    // the same user, so 0o600 is sufficient and prevents other local users
    // from sending lock/screenshot/quit commands.
    fs::set_permissions(&socket_path, fs::Permissions::from_mode(0o600))?;
    let socket = SocketSource::new(listener)?;

    let mut buffer = String::new();
    event_loop.insert_source(socket, move |stream, _, forge| {
        handle_message(&mut buffer, stream, forge);
    })?;

    Ok(socket_path)
}

fn handle_message(buffer: &mut String, mut stream: UnixStream, forge: &mut Forge) {
    // Cap read time so a slow or broken client cannot stall the render loop.
    // Legitimate callers (forge msg, crustace IPC) write the full JSON line in
    // a single syscall so this timeout is never hit in practice.
    stream.set_read_timeout(Some(Duration::from_millis(5))).ok();

    buffer.clear();
    let mut reader = BufReader::new(&stream);
    if let Ok(0) | Err(_) = reader.read_line(buffer) {
        return;
    }

    // Try Pedro's action-based JSON protocol first
    if let Ok(val) = serde_json::from_str::<Value>(buffer) {
        if let Some(action) = val.get("action").and_then(|a| a.as_str()) {
            handle_action(action, &val, &mut stream, forge);
            return;
        }
    }

    // Fall back to forge_ipc::IpcMessage enum format
    let message: IpcMessage = match serde_json::from_str(buffer) {
        Ok(m) => m,
        Err(_) => return,
    };
    handle_ipc_message(message, &mut stream, forge);
}

// ── Pedro's action-based protocol ──────────────────────────────────────

fn handle_action(action: &str, val: &Value, stream: &mut UnixStream, forge: &mut Forge) {
    match action {
        "toggle_launcher" => {
            info!("IPC: toggle_launcher");
            // Forge does nothing — crustace handles this directly.
            // The keybinding sends this to crustace's socket.
            send_json(stream, r#"{"success":true}"#);
            forge.unstall();
        }


        "show_homescreen" => {
            info!("IPC: show_homescreen");
            // Minimize all windows, give focus to crustace layer
            forge.windows.minimize_all();
            forge.request_redraw();
            send_json(stream, r#"{"success":true}"#);
        }

        "list_windows" => {
            let windows = forge.windows.list_toplevel_windows();
            let json = serde_json::to_string(&windows).unwrap_or_else(|_| "[]".into());
            send_json(stream, &json);
        }

        "activate_window" => {
            if let Some(id) = val.get("window_id").and_then(|v| v.as_u64()) {
                forge.windows.activate_window_by_id(id);
                forge.unstall();
                send_json(stream, r#"{"success":true}"#);
            } else {
                send_json(stream, r#"{"error":"missing_window_id"}"#);
            }
        }

        "activate_app_id" => {
            if let Some(app_id) = val.get("app_id").and_then(|v| v.as_str()) {
                forge.windows.activate_window_by_app_id(app_id);
                forge.unstall();
                send_json(stream, r#"{"success":true}"#);
            } else {
                send_json(stream, r#"{"error":"missing_app_id"}"#);
            }
        }

        "close_window" => {
            if let Some(id) = val.get("window_id").and_then(|v| v.as_u64()) {
                forge.windows.close_window_by_id(id);
                forge.unstall();
                send_json(stream, r#"{"success":true}"#);
            } else {
                send_json(stream, r#"{"error":"missing_window_id"}"#);
            }
        }

        "close_app_id" => {
            if let Some(app_id) = val.get("app_id").and_then(|v| v.as_str()) {
                forge.windows.close_windows_by_app_id(app_id);
                forge.unstall();
                send_json(stream, r#"{"success":true}"#);
            } else {
                send_json(stream, r#"{"error":"missing_app_id"}"#);
            }
        }

        "resize_app_id" => {
            let app_id = val.get("app_id").and_then(|v| v.as_str());
            let w = val.get("w").and_then(|v| v.as_i64()).map(|v| v as i32);
            let h = val.get("h").and_then(|v| v.as_i64()).map(|v| v as i32);
            match (app_id, w, h) {
                (Some(app_id), Some(w), Some(h)) if w > 0 && h > 0 => {
                    forge.windows.resize_window_by_app_id(app_id, w, h);
                    forge.unstall();
                    send_json(stream, r#"{"success":true}"#);
                }
                _ => send_json(stream, r#"{"error":"missing_or_invalid_fields"}"#),
            }
        }

        "get_window_thumbnail" => {
            let id = val.get("window_id").and_then(|v| v.as_u64()).unwrap_or(0);
            let width = val.get("width").and_then(|v| v.as_u64()).unwrap_or(300) as u32;
            let height = val.get("height").and_then(|v| v.as_u64()).unwrap_or(200) as u32;

            match forge.capture_window_thumbnail(id, width, height) {
                Ok(thumb) => {
                    let json = serde_json::to_string(&thumb).unwrap_or_else(|_| {
                        r#"{"error":"serialize_failed"}"#.into()
                    });
                    send_json(stream, &json);
                }
                Err(e) => {
                    let err = serde_json::json!({
                        "error": "capture_failed",
                        "message": e
                    });
                    send_json(stream, &err.to_string());
                }
            }
        }

        "screenshot" | "screenshot_region" => {
            forge.take_screenshot();
            send_json(stream, r#"{"success":true}"#);
        }

        "screenshot_full" => {
            forge.screenshot_full_to_clipboard();
            send_json(stream, r#"{"success":true}"#);
        }

        "screenshot_window" => {
            let rect = match val.get("window_id").and_then(|v| v.as_u64()) {
                Some(id) => forge.windows.window_rect_by_id(id),
                None => forge.windows.focused_window_capture_rect(),
            };
            match rect {
                Some(region) => {
                    forge.screenshot_region_to_clipboard(region);
                    send_json(stream, r#"{"success":true}"#);
                }
                None => send_json(stream, r#"{"error":"no_window"}"#),
            }
        }

        "lock" => {
            forge.lock_session();
            forge.request_redraw();
            send_json(stream, r#"{"success":true}"#);
        }

        "switch_workspace" => {
            if let Some(idx) = val.get("index").and_then(|v| v.as_u64()) {
                forge.windows.switch_workspace(idx as usize);
                forge.request_redraw();
                send_json(stream, r#"{"success":true}"#);
            } else if let Some(dir) = val.get("direction").and_then(|v| v.as_str()) {
                let current = forge.windows.active_workspace().unwrap_or(0);
                let count = forge.windows.workspace_count();
                if count > 0 {
                    let target = match dir {
                        "next" => (current + 1) % count,
                        "prev" => if current == 0 { count - 1 } else { current - 1 },
                        _ => current,
                    };
                    forge.windows.switch_workspace(target);
                    forge.request_redraw();
                }
                send_json(stream, r#"{"success":true}"#);
            }
        }

        "get_workspaces" => {
            let current = forge.windows.active_workspace().unwrap_or(0);
            let count = forge.windows.workspace_count();
            let json = serde_json::json!({
                "current": current,
                "count": count,
            });
            send_json(stream, &json.to_string());
        }

        "configure_topbar" => {
            if let Some(height) = val.get("height").and_then(|v| v.as_i64()) {
                info!("IPC: configure_topbar height={height}");
                forge.windows.configure_topbar(height as i32);
                forge.unstall();
                send_json(stream, r#"{"success":true}"#);
            } else {
                send_json(stream, r#"{"error":"missing_height"}"#);
            }
        }

        "set_notification_count" => {
            let count = val.get("count").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
            forge.topbar.set_notification_count(count);
            forge.request_redraw();
            send_json(stream, r#"{"success":true}"#);
        }

        "set_dock_zone" => {
            let height = val.get("height").and_then(|v| v.as_u64()).unwrap_or(0) as f64;
            forge.edge_recognizer.set_leave_band(height);
            // Alt+Tab needs the same value to keep its panel out of the
            // dock strip (eydos #6 spec'd bottom margin = dock height
            // + 2% screen).
            forge.app_switcher.set_dock_height(height as i32);
            send_json(stream, r#"{"success":true}"#);
        }

        "set_mobile" => {
            let enabled = val.get("enabled").and_then(|v| v.as_bool());
            info!("IPC: set_mobile enabled={enabled:?}");
            forge.windows.set_mobile_override(enabled);
            forge.unstall();
            send_json(stream, r#"{"success":true}"#);
        }

        // Cross-process theme sync. Crustace resolves its theme preference
        // (config + CLI --dark / --light) in its own process and calls this
        // action so forge's topbar / SSD render against the same variant,
        // without having to wait for the next sysmon poll to pick up the
        // gsettings value (or, in the --dark / --light case, without the
        // user ever having set gsettings at all).
        //
        // Payload: `{"mode":"dark"}` or `{"mode":"light"}`.
        "set_theme" => {
            let mode = val.get("mode").and_then(|v| v.as_str()).unwrap_or("");
            let is_dark = match mode {
                "dark"  => true,
                "light" => false,
                other   => {
                    warn!("IPC: set_theme got unknown mode {other:?}");
                    send_json(stream, r#"{"error":"invalid_mode"}"#);
                    return;
                }
            };
            info!("IPC: set_theme mode={mode}");
            forge.sys_state.dark_mode = is_dark;
            // Topbar reconciles `sys.dark_mode` against `ltk::active_mode()`
            // on the next render and flushes its own caches when it flips,
            // so a single `set_dirty` is enough to make this visible.
            forge.request_redraw();
            send_json(stream, r#"{"success":true}"#);
        }

        _ => {
            warn!("Unknown IPC action: {action}");
            send_json(stream, r#"{"error":"unknown_action"}"#);
        }
    }
}

// ── Legacy forge_ipc::IpcMessage protocol ──────────────────────────────

fn handle_ipc_message(message: IpcMessage, stream: &mut UnixStream, forge: &mut Forge) {
    match message {
        IpcMessage::Orientation { unlock: true, .. } => forge.unlock_orientation(),
        IpcMessage::Orientation { lock: orientation, .. } => forge.lock_orientation(orientation),

        IpcMessage::Scale { scale, app_id: Some(app_id) } => {
            let Ok(matcher) = AppIdMatcher::try_from(app_id).inspect_err(|e| {
                warn!("Invalid App ID regex in scale message: {e}");
            }) else {
                return;
            };
            forge.windows.add_window_scale(matcher, scale);
        },

        IpcMessage::Scale { scale, app_id: None } => {
            let WindowScale::Fixed(scale) = scale else {
                warn!("Expected fixed scale, got {scale:?}");
                return;
            };
            forge.windows.set_scale(scale);
            forge.unstall();
        },

        IpcMessage::BindGesture { app_id, start, end, program, arguments } => {
            let Ok(app_id) = AppIdMatcher::try_from(app_id).inspect_err(|e| {
                warn!("Invalid App ID regex in gesture binding: {e}");
            }) else {
                return;
            };
            forge.touch_state.user_gestures.push(GestureBinding {
                app_id, start, end,
                action: GestureAction::Exec { program, args: arguments },
            });
        },

        IpcMessage::BindGestureKey { app_id, start, end, mods, key } => {
            let Ok(app_id) = AppIdMatcher::try_from(app_id).inspect_err(|e| {
                warn!("Invalid App ID regex in gesture binding: {e}");
            }) else {
                return;
            };
            let Keysym::Xkb(keysym) = key else {
                warn!("Gestures must be bound to real keysyms");
                return;
            };
            forge.touch_state.user_gestures.push(GestureBinding {
                app_id, start, end,
                action: GestureAction::Key { keysym, mods: mods.unwrap_or_default() },
            });
        },

        IpcMessage::UnbindGesture { app_id, start, end } => {
            forge.touch_state.user_gestures
                .retain(|g| g.app_id.base() != app_id || g.start != start || g.end != end);
        },

        IpcMessage::BindKey { app_id, mods, trigger, keys, program, arguments } => {
            let Ok(app_id) = AppIdMatcher::try_from(app_id).inspect_err(|e| {
                warn!("Invalid App ID regex in key binding: {e}");
            }) else {
                return;
            };
            forge.key_bindings.push(KeyBinding {
                app_id, mods: mods.unwrap_or_default(), keys, trigger, program, arguments,
            });
        },

        IpcMessage::UnbindKey { app_id, mods, keys } => {
            let mods = mods.unwrap_or_default();
            forge.key_bindings
                .retain(|b| b.app_id.base() != app_id || b.keys != keys || b.mods != mods);
        },

        IpcMessage::KeyboardConfig { model, layout, variant, options } => {
            forge.set_xkb_config(XkbConfig {
                model: model.as_deref().unwrap_or_default(),
                layout: layout.as_deref().unwrap_or_default(),
                variant: variant.as_deref().unwrap_or_default(),
                options,
                rules: "",
            });
        },

        IpcMessage::Dpms { state: Some(state) } => {
            forge.set_display_status(state == CliToggle::On);
        },
        IpcMessage::Dpms { state: None } => {
            let state = if forge.display_on { CliToggle::On } else { CliToggle::Off };
            send_reply(stream, &IpcMessage::DpmsReply { state });
        },

        IpcMessage::Cursor { state } => {
            forge.draw_cursor = state == CliToggle::On;
        },
        IpcMessage::Lock => {
            forge.lock_session();
            forge.request_redraw();
        },
        IpcMessage::Unlock => {
            forge.unstall();
        },
        IpcMessage::Launcher => forge.unstall(),
        IpcMessage::PowerMenu => {
            forge.power_menu.show();
            forge.request_redraw();
        },
        IpcMessage::Wallpaper { .. } => {
            forge.request_redraw();
        },
        IpcMessage::Workspace { target } => {
            if target == "+1" {
                let current = forge.windows.active_workspace().unwrap_or(0);
                let count = forge.windows.workspace_count();
                if count > 0 { forge.windows.switch_workspace((current + 1) % count); }
            } else if target == "-1" {
                let current = forge.windows.active_workspace().unwrap_or(0);
                let count = forge.windows.workspace_count();
                if count > 0 { forge.windows.switch_workspace(if current == 0 { count - 1 } else { current - 1 }); }
            } else if let Ok(idx) = target.parse::<usize>() {
                forge.windows.switch_workspace(idx);
            }
            forge.request_redraw();
        },
        IpcMessage::ShowQuickSettings | IpcMessage::HideQuickSettings |
        IpcMessage::ShowNotifications | IpcMessage::HideNotifications => forge.unstall(),
        IpcMessage::Screenshot => forge.take_screenshot(),
        // Overview was removed; the app switcher replaces it. Kept as a no-op
        // so the typed IPC and the hot-corner that emits it don't error.
        IpcMessage::ToggleOverview => forge.unstall(),
        IpcMessage::Quit => {
            forge.terminated = true;
        },
        IpcMessage::SystemStateUpdate { .. } | IpcMessage::DpmsReply { .. } => (),
    }
}

fn send_json(stream: &mut UnixStream, json: &str) {
    let _ = writeln!(stream, "{json}");
    let _ = stream.flush();
}

fn send_reply(stream: &mut UnixStream, message: &IpcMessage) {
    if let Ok(json) = serde_json::to_string(message) {
        let _ = writeln!(stream, "{json}");
        let _ = stream.flush();
    }
}
